diff --git a/.gitattributes b/.gitattributes index 5bb8d8b736..c4b5fa0751 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,3 +4,4 @@ crates/ruff/resources/test/fixtures/isort/line_ending_crlf.py text eol=crlf crates/ruff/resources/test/fixtures/pycodestyle/W605_1.py text eol=crlf ruff.schema.json linguist-generated=true text=auto eol=lf +*.md.snap linguist-language=Markdown diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000000..9cd29adcda --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,9 @@ +# GitHub code owners file. For more info: https://help.github.com/articles/about-codeowners/ +# +# - Comment lines begin with `#` character. +# - Each line is a file pattern followed by one or more owners. +# - The '*' pattern is global owners. +# - Order is important. The last matching pattern has the most precedence. + +# Jupyter +/crates/ruff/src/jupyter/ @dhruvmanila diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 46ad6cd981..48f532299a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -16,7 +16,7 @@ env: CARGO_TERM_COLOR: always RUSTUP_MAX_RETRIES: 10 PACKAGE_NAME: ruff - PYTHON_VERSION: "3.7" # to build abi3 wheels + PYTHON_VERSION: "3.11" # to build abi3 wheels jobs: cargo-fmt: @@ -31,17 +31,6 @@ jobs: cargo-clippy: name: "cargo clippy" runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: "Install Rust toolchain" - run: | - rustup component add clippy - - uses: Swatinem/rust-cache@v2 - - run: cargo clippy --workspace --all-targets --all-features -- -D warnings - - cargo-clippy-wasm: - name: "cargo clippy (wasm)" - runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: "Install Rust toolchain" @@ -49,7 +38,10 @@ jobs: rustup component add clippy rustup target add wasm32-unknown-unknown - uses: Swatinem/rust-cache@v2 - - run: cargo clippy -p ruff_wasm --target wasm32-unknown-unknown --all-features -- -D warnings + - name: "Clippy" + run: cargo clippy --workspace --all-targets --all-features -- -D warnings + - name: "Clippy (wasm)" + run: cargo clippy -p ruff_wasm --target wasm32-unknown-unknown --all-features -- -D warnings cargo-test: strategy: @@ -62,20 +54,21 @@ jobs: - name: "Install Rust toolchain" run: rustup show - uses: Swatinem/rust-cache@v2 - - run: cargo install cargo-insta + # cargo insta 1.30.0 fails for some reason (https://github.com/mitsuhiko/insta/issues/392) + - run: cargo install cargo-insta@=1.29.0 - run: pip install black[d]==23.1.0 - name: "Run tests (Ubuntu)" if: ${{ matrix.os == 'ubuntu-latest' }} - run: | - cargo insta test --all --all-features --delete-unreferenced-snapshots - git diff --exit-code + run: cargo insta test --all --all-features --unreferenced reject - name: "Run tests (Windows)" if: ${{ matrix.os == 'windows-latest' }} shell: bash - run: | - cargo insta test --all --all-features - git diff --exit-code + # We can't reject unreferenced snapshots on windows because flake8_executable can't run on windows + run: cargo insta test --all --all-features - run: cargo test --package ruff_cli --test black_compatibility_test -- --ignored + # TODO: Skipped as it's currently broken. The resource were moved from the + # ruff_cli to ruff crate, but this test was not updated. + if: false # Check for broken links in the documentation. - run: cargo doc --all --no-deps env: @@ -149,7 +142,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: - python-version: "3.11" + python-version: ${{ env.PYTHON_VERSION }} - uses: actions/download-artifact@v3 name: Download Ruff binary @@ -217,11 +210,10 @@ jobs: - name: "Build wheels" uses: PyO3/maturin-action@v1 with: - manylinux: auto args: --out dist - name: "Test wheel" run: | - pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall + pip install --force-reinstall --find-links dist ${{ env.PACKAGE_NAME }} ruff --help python -m ruff --help - name: "Remove wheels from cache" @@ -234,7 +226,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: - python-version: "3.11" + python-version: ${{ env.PYTHON_VERSION }} - name: "Install Rust toolchain" run: rustup show - uses: Swatinem/rust-cache@v2 @@ -258,13 +250,24 @@ jobs: docs: name: "mkdocs" runs-on: ubuntu-latest + env: + MKDOCS_INSIDERS_SSH_KEY_EXISTS: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY != '' }} steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 + - name: "Add SSH key" + if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }} + uses: webfactory/ssh-agent@v0.8.0 + with: + ssh-private-key: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY }} - name: "Install Rust toolchain" run: rustup show - uses: Swatinem/rust-cache@v2 + - name: "Install Insiders dependencies" + if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }} + run: pip install -r docs/requirements-insiders.txt - name: "Install dependencies" + if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS != 'true' }} run: pip install -r docs/requirements.txt - name: "Update README File" run: python scripts/transform_readme.py --target mkdocs @@ -272,5 +275,23 @@ jobs: run: python scripts/generate_mkdocs.py - name: "Check docs formatting" run: python scripts/check_docs_formatted.py + - name: "Build Insiders docs" + if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }} + run: mkdocs build --strict -f mkdocs.insiders.yml - name: "Build docs" - run: mkdocs build --strict + if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS != 'true' }} + run: mkdocs build --strict -f mkdocs.generated.yml + + check-formatter-stability: + name: "Check formatter stability" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: "Install Rust toolchain" + run: rustup show + - name: "Cache rust" + uses: Swatinem/rust-cache@v2 + - name: "Clone CPython 3.10" + run: git clone --branch 3.10 --depth 1 https://github.com/python/cpython.git crates/ruff/resources/test/cpython + - name: "Check stability" + run: cargo run --bin ruff_dev -- format-dev --stability-check crates/ruff/resources/test/cpython diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index cb8f646df5..f5ed1a6e31 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -10,20 +10,34 @@ jobs: runs-on: ubuntu-latest env: CF_API_TOKEN_EXISTS: ${{ secrets.CF_API_TOKEN != '' }} + MKDOCS_INSIDERS_SSH_KEY_EXISTS: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY != '' }} steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 + - name: "Add SSH key" + if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }} + uses: webfactory/ssh-agent@v0.8.0 + with: + ssh-private-key: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY }} - name: "Install Rust toolchain" run: rustup show - uses: Swatinem/rust-cache@v2 + - name: "Install Insiders dependencies" + if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }} + run: pip install -r docs/requirements-insiders.txt - name: "Install dependencies" - run: | - pip install -r docs/requirements.txt + if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS != 'true' }} + run: pip install -r docs/requirements.txt - name: "Copy README File" run: | python scripts/transform_readme.py --target mkdocs python scripts/generate_mkdocs.py - mkdocs build --strict + - name: "Build Insiders docs" + if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }} + run: mkdocs build --strict -f mkdocs.insiders.yml + - name: "Build docs" + if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS != 'true' }} + run: mkdocs build --strict -f mkdocs.generated.yml - name: "Deploy to Cloudflare Pages" if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }} uses: cloudflare/wrangler-action@2.0.0 diff --git a/.github/workflows/flake8-to-ruff.yaml b/.github/workflows/flake8-to-ruff.yaml index 68582b1472..44e41b7d29 100644 --- a/.github/workflows/flake8-to-ruff.yaml +++ b/.github/workflows/flake8-to-ruff.yaml @@ -9,7 +9,7 @@ concurrency: env: PACKAGE_NAME: flake8-to-ruff CRATE_NAME: flake8_to_ruff - PYTHON_VERSION: "3.7" # to build abi3 wheels + PYTHON_VERSION: "3.11" CARGO_INCREMENTAL: 0 CARGO_NET_RETRY: 10 CARGO_TERM_COLOR: always diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index d45dcfdffe..83c1679cb2 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -2,8 +2,17 @@ name: "[ruff] Release" on: workflow_dispatch: - release: - types: [ published ] + inputs: + tag: + description: "The version to tag, without the leading 'v'. If omitted, will initiate a dry run (no uploads)." + type: string + sha: + description: "Optionally, the full sha of the commit to be released" + type: string + pull_request: + paths: + # When we change pyproject.toml, we want to ensure that the maturin builds still work + - pyproject.toml concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -11,7 +20,7 @@ concurrency: env: PACKAGE_NAME: ruff - PYTHON_VERSION: "3.7" # to build abi3 wheels + PYTHON_VERSION: "3.11" CARGO_INCREMENTAL: 0 CARGO_NET_RETRY: 10 CARGO_TERM_COLOR: always @@ -383,8 +392,39 @@ jobs: *.tar.gz *.sha256 - release: - name: Release + validate-tag: + name: Validate tag + runs-on: ubuntu-latest + # If you don't set an input tag, it's a dry run (no uploads). + if: ${{ inputs.tag }} + steps: + - uses: actions/checkout@v3 + - name: Check tag consistency + run: | + version=$(grep "version = " pyproject.toml | sed -e 's/version = "\(.*\)"/\1/g') + if [ "${{ inputs.tag }}" != "${version}" ]; then + echo "The input tag does not match the version from pyproject.toml:" >&2 + echo "${{ inputs.tag }}" >&2 + echo "${version}" >&2 + exit 1 + else + echo "Releasing ${version}" + fi + - name: Check SHA consistency + if: ${{ inputs.sha }} + run: | + git_sha=$(git rev-parse HEAD) + if [ "${{ inputs.sha }}" != "${git_sha}" ]; then + echo "The specified sha does not match the git checkout" >&2 + echo "${{ inputs.sha }}" >&2 + echo "${git_sha}" >&2 + exit 1 + else + echo "Releasing ${git_sha}" + fi + + upload-release: + name: Upload to PyPI runs-on: ubuntu-latest needs: - macos-universal @@ -394,25 +434,56 @@ jobs: - linux-cross - musllinux - musllinux-cross - if: "startsWith(github.ref, 'refs/tags/')" + - validate-tag + # If you don't set an input tag, it's a dry run (no uploads). + if: ${{ inputs.tag }} environment: name: release permissions: # For pypi trusted publishing id-token: write - # For GitHub release publishing - contents: write steps: - uses: actions/download-artifact@v3 with: name: wheels path: wheels - - name: "Publish to PyPi" + - name: Publish to PyPi uses: pypa/gh-action-pypi-publish@release/v1 with: skip-existing: true packages-dir: wheels verbose: true + + tag-release: + name: Tag release + runs-on: ubuntu-latest + needs: upload-release + # If you don't set an input tag, it's a dry run (no uploads). + if: ${{ inputs.tag }} + permissions: + # For git tag + contents: write + steps: + - uses: actions/checkout@v3 + - name: git tag + run: | + git config user.email "hey@astral.sh" + git config user.name "Ruff Release CI" + git tag -m "v${{ inputs.tag }}" "v${{ inputs.tag }}" + # If there is duplicate tag, this will fail. The publish to pypi action will have been a noop (due to skip + # existing), so we make a non-destructive exit here + git push --tags + + publish-release: + name: Publish to GitHub + runs-on: ubuntu-latest + needs: tag-release + # If you don't set an input tag, it's a dry run (no uploads). + if: ${{ inputs.tag }} + permissions: + # For GitHub release publishing + contents: write + steps: - uses: actions/download-artifact@v3 with: name: binaries @@ -420,14 +491,16 @@ jobs: - name: "Publish to GitHub" uses: softprops/action-gh-release@v1 with: + draft: true files: binaries/* + tag_name: v${{ inputs.tag }} # After the release has been published, we update downstream repositories # This is separate because if this fails the release is still fine, we just need to do some manual workflow triggers update-dependents: - name: Release + name: Update dependents runs-on: ubuntu-latest - needs: release + needs: publish-release steps: - name: "Update pre-commit mirror" uses: actions/github-script@v6 diff --git a/.gitignore b/.gitignore index 55de3ba557..5bfce3449a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,25 @@ +# Benchmarking cpython (CONTRIBUTING.md) crates/ruff/resources/test/cpython -mkdocs.yml -.overrides +# generate_mkdocs.py +mkdocs.generated.yml +# check_ecosystem.py ruff-old github_search*.jsonl +# update_schemastore.py schemastore +# `maturin develop` and ecosystem_all_check.sh .venv* +# Formatter debugging (crates/ruff_python_formatter/README.md) scratch.py +# Created by `perf` (CONTRIBUTING.md) +perf.data +perf.data.old +# Created by `flamegraph` (CONTRIBUTING.md) +flamegraph.svg +# Additional target directories that don't invalidate the main compile cache when changing linker settings, +# e.g. `CARGO_TARGET_DIR=target-maturin maturin build --release --strip` or +# `CARGO_TARGET_DIR=target-llvm-lines RUSTFLAGS="-Csymbol-mangling-version=v0" cargo llvm-lines -p ruff --lib` +/target* ### # Rust.gitignore diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 572edd2781..5209b8b84b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,8 +3,12 @@ fail_fast: true exclude: | (?x)^( crates/ruff/resources/.*| + crates/ruff/src/rules/.*/snapshots/.*| + crates/ruff_cli/resources/.*| crates/ruff_python_formatter/resources/.*| - crates/ruff_python_formatter/src/snapshots/.* + crates/ruff_python_formatter/tests/snapshots/.*| + crates/ruff_python_resolver/resources/.*| + crates/ruff_python_resolver/tests/snapshots/.* )$ repos: @@ -37,29 +41,19 @@ repos: name: cargo fmt entry: cargo fmt -- language: system - types: [rust] - - id: clippy - name: clippy - entry: cargo clippy --workspace --all-targets --all-features -- -D warnings - language: system - pass_filenames: false + types: [ rust ] + pass_filenames: false # This makes it a lot faster - id: ruff name: ruff - entry: cargo run -p ruff_cli -- check --no-cache --force-exclude --fix --exit-non-zero-on-fix + entry: cargo run --bin ruff -- check --no-cache --force-exclude --fix --exit-non-zero-on-fix language: system - types_or: [python, pyi] + types_or: [ python, pyi ] require_serial: true exclude: | (?x)^( crates/ruff/resources/.*| crates/ruff_python_formatter/resources/.* )$ - - id: dev-generate-all - name: dev-generate-all - entry: cargo dev generate-all - language: system - pass_filenames: false - exclude: target # Black - repo: https://github.com/psf/black @@ -68,4 +62,4 @@ repos: - id: black ci: - skip: [cargo-fmt, clippy, dev-generate-all] + skip: [ cargo-fmt, dev-generate-all ] diff --git a/BREAKING_CHANGES.md b/BREAKING_CHANGES.md index 7ae6cfdf97..cc69ea3483 100644 --- a/BREAKING_CHANGES.md +++ b/BREAKING_CHANGES.md @@ -1,5 +1,67 @@ # Breaking Changes +## 0.0.277 + +### `.ipynb_checkpoints`, `.pyenv`, `.pytest_cache`, and `.vscode` are now excluded by default ([#5513](https://github.com/astral-sh/ruff/pull/5513)) + +Ruff maintains a list of default exclusions, which now consists of the following patterns: + +- `.bzr` +- `.direnv` +- `.eggs` +- `.git` +- `.git-rewrite` +- `.hg` +- `.ipynb_checkpoints` +- `.mypy_cache` +- `.nox` +- `.pants.d` +- `.pyenv` +- `.pytest_cache` +- `.pytype` +- `.ruff_cache` +- `.svn` +- `.tox` +- `.venv` +- `.vscode` +- `__pypackages__` +- `_build` +- `buck-out` +- `build` +- `dist` +- `node_modules` +- `venv` + +Previously, the `.ipynb_checkpoints`, `.pyenv`, `.pytest_cache`, and `.vscode` directories were not +excluded by default. This change brings Ruff's default exclusions in line with other tools like +Black. + +## 0.0.276 + +### The `keep-runtime-typing` setting has been reinstated ([#5470](https://github.com/astral-sh/ruff/pull/5470)) + +The `keep-runtime-typing` setting has been reinstated with revised semantics. This setting was +removed in [#4427](https://github.com/astral-sh/ruff/pull/4427), as it was equivalent to ignoring +the `UP006` and `UP007` rules via Ruff's standard `ignore` mechanism. + +Taking `UP006` (rewrite `List[int]` to `list[int]`) as an example, the setting now behaves as +follows: + +- On Python 3.7 and Python 3.8, setting `keep-runtime-typing = true` will cause Ruff to ignore + `UP006` violations, even if `from __future__ import annotations` is present in the file. + While such annotations are valid in Python 3.7 and Python 3.8 when combined with + `from __future__ import annotations`, they aren't supported by libraries like Pydantic and + FastAPI, which rely on runtime type checking. +- On Python 3.9 and above, the setting has no effect, as `list[int]` is a valid type annotation, + and libraries like Pydantic and FastAPI support it without issue. + +In short: `keep-runtime-typing` can be used to ensure that Ruff doesn't introduce type annotations +that are not supported at runtime by the current Python version, which are unsupported by libraries +like Pydantic and FastAPI. + +Note that this is not a breaking change, but is included here to complement the previous removal +of `keep-runtime-typing`. + ## 0.0.268 ### The `keep-runtime-typing` setting has been removed ([#4427](https://github.com/astral-sh/ruff/pull/4427)) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eaeb300f85..6864a7ecd1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,7 +12,7 @@ Welcome! We're happy to have you here. Thank you in advance for your contributio - [Example: Adding a new configuration option](#example-adding-a-new-configuration-option) - [MkDocs](#mkdocs) - [Release Process](#release-process) -- [Benchmarks](#benchmarks) +- [Benchmarks](#benchmarking-and-profiling) ## The Basics @@ -21,9 +21,11 @@ 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.gg/c9MhzV8aU5) to discuss your idea with -the community. +creating an [**issue**](https://github.com/astral-sh/ruff/issues) outlining your proposed change. +You can also join us on [**Discord**](https://discord.gg/c9MhzV8aU5) to discuss your idea with the +community. We have labeled [beginner-friendly tasks in the issue tracker](https://github.com/astral-sh/ruff/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) +as well as [bugs](https://github.com/astral-sh/ruff/issues?q=is%3Aissue+is%3Aopen+label%3Abug) and +[improvements that are ready for contributions](https://github.com/astral-sh/ruff/issues?q=is%3Aissue+is%3Aopen+label%3Aaccepted). 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 @@ -31,8 +33,10 @@ pattern-match against the examples in the existing codebase. Many lint rules are 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. +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 @@ -45,6 +49,12 @@ You'll also need [Insta](https://insta.rs/docs/) to update snapshot tests: cargo install cargo-insta ``` +and pre-commit to run some validation checks: + +```shell +pipx install pre-commit # or `pip install pre-commit` if you have a virtualenv +``` + ### Development After cloning the repository, run Ruff locally with: @@ -57,9 +67,9 @@ Prior to opening a pull request, ensure that your code has been auto-formatted, and that it passes both the lint and test validation checks: ```shell -cargo fmt # Auto-formatting... -cargo clippy --fix --workspace --all-targets --all-features # Linting... -cargo test # Testing... +cargo clippy --workspace --all-targets --all-features -- -D warnings # Rust linting +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 @@ -72,13 +82,6 @@ after running `cargo test` like so: cargo insta review ``` -If you have `pre-commit` [installed](https://pre-commit.com/#installation) then you can use it to -assist with formatting and linting. The following command will run the `pre-commit` hooks: - -```shell -pre-commit run --all-files -``` - Your Pull Request will be reviewed by a maintainer, which may involve a few rounds of iteration prior to merging. @@ -93,64 +96,89 @@ The vast majority of the code, including all lint rules, lives in the `ruff` cra At time of writing, the repository includes the following crates: - `crates/ruff`: library crate containing all lint rules and the core logic for running them. +- `crates/ruff_benchmark`: binary crate for running micro-benchmarks. +- `crates/ruff_cache`: library crate for caching lint results. - `crates/ruff_cli`: binary crate containing Ruff's command-line interface. - `crates/ruff_dev`: binary crate containing utilities used in the development of Ruff itself (e.g., `cargo dev generate-all`). +- `crates/ruff_diagnostics`: library crate for the lint diagnostics APIs. +- `crates/ruff_formatter`: library crate for generic code formatting logic based on an intermediate + representation. +- `crates/ruff_index`: library crate inspired by `rustc_index`. - `crates/ruff_macros`: library crate containing macros used by Ruff. -- `crates/ruff_python`: library crate implementing Python-specific functionality (e.g., lists of - standard library modules by version). -- `crates/flake8_to_ruff`: binary crate for generating Ruff configuration from Flake8 configuration. +- `crates/ruff_python_ast`: library crate containing Python-specific AST types and utilities. +- `crates/ruff_python_formatter`: library crate containing Python-specific code formatting logic. +- `crates/ruff_python_semantic`: library crate containing Python-specific semantic analysis logic, + including Ruff's semantic model. +- `crates/ruff_python_stdlib`: library crate containing Python-specific standard library data. +- `crates/ruff_python_whitespace`: library crate containing Python-specific whitespace analysis + logic. +- `crates/ruff_rustpython`: library crate containing `RustPython`-specific utilities. +- `crates/ruff_testing_macros`: library crate containing macros used for testing Ruff. +- `crates/ruff_textwrap`: library crate to indent and dedent Python source code. +- `crates/ruff_wasm`: library crate for exposing Ruff as a WebAssembly module. ### Example: Adding a new lint rule At a high level, the steps involved in adding a new lint rule are as follows: -1. Determine a name for the new rule as per our [rule naming convention](#rule-naming-convention). +1. Determine a name for the new rule as per our [rule naming convention](#rule-naming-convention) + (e.g., `AssertFalse`, as in, "allow `assert False`"). -1. Create a file for your rule (e.g., `crates/ruff/src/rules/flake8_bugbear/rules/abstract_base_class.rs`). +1. Create a file for your rule (e.g., `crates/ruff/src/rules/flake8_bugbear/rules/assert_false.rs`). -1. In that file, define a violation struct. You can grep for `#[violation]` to see examples. +1. In that file, define a violation struct (e.g., `pub struct AssertFalse`). You can grep for + `#[violation]` to see examples. -1. Map the violation struct to a rule code in `crates/ruff/src/codes.rs` (e.g., `E402`). +1. In that file, define a function that adds the violation to the diagnostic list as appropriate + (e.g., `pub(crate) fn assert_false`) based on whatever inputs are required for the rule (e.g., + an `ast::StmtAssert` node). 1. Define the logic for triggering the violation in `crates/ruff/src/checkers/ast/mod.rs` (for AST-based checks), `crates/ruff/src/checkers/tokens.rs` (for token-based checks), `crates/ruff/src/checkers/lines.rs` (for text-based checks), or `crates/ruff/src/checkers/filesystem.rs` (for filesystem-based checks). +1. Map the violation struct to a rule code in `crates/ruff/src/codes.rs` (e.g., `B011`). + 1. Add proper [testing](#rule-testing-fixtures-and-snapshots) for your rule. 1. Update the generated files (documentation and generated code). -To define the violation, start by creating a dedicated file for your rule under the appropriate -rule linter (e.g., `crates/ruff/src/rules/flake8_bugbear/rules/abstract_base_class.rs`). That file should -contain a struct defined via `#[violation]`, along with a function that creates the violation -based on any required inputs. - -To trigger the violation, you'll likely want to augment the logic in `crates/ruff/src/checkers/ast.rs`, -which defines the Python AST visitor, responsible for iterating over the abstract syntax tree and -collecting diagnostics as it goes. +To trigger the violation, you'll likely want to augment the logic in `crates/ruff/src/checkers/ast.rs` +to call your new function at the appropriate time and with the appropriate inputs. The `Checker` +defined therein is a Python AST visitor, which iterates over the AST, building up a semantic model, +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 `Check::new` invocations to understand how other, similar rules are implemented. +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) for more details. -Finally, regenerate the documentation and generated code with `cargo dev generate-all`. +Finally, regenerate the documentation and other generated assets (like our JSON Schema) with: +`cargo dev generate-all`. #### Rule naming convention -The rule name should make sense when read as "allow _rule-name_" or "allow _rule-name_ items". +Like Clippy, Ruff's rule names should make grammatical and logical sense when read as "allow +${rule}" or "allow ${rule} items", as in the context of suppression comments. -This implies that rule names: +For example, `AssertFalse` fits this convention: it flags `assert False` statements, and so a +suppression comment would be framed as "allow `assert False`". -- should state the bad thing being checked for +As such, rule names should... -- should not contain instructions on what you should use instead - (these belong in the rule documentation and the `autofix_title` for rules that have autofix) +- Highlight the pattern that is being linted against, rather than the preferred alternative. + For example, `AssertFalse` guards against `assert False` statements. -When re-implementing rules from other linters, this convention is given more importance than +- _Not_ contain instructions on how to fix the violation, which instead belong in the rule + documentation and the `autofix_title`. + +- _Not_ contain a redundant prefix, like `Disallow` or `Banned`, which are already implied by the + convention. + +When re-implementing rules from other linters, we prioritize adhering to this convention over preserving the original rule name. #### Rule testing: fixtures and snapshots @@ -232,7 +260,11 @@ To preview any changes to the documentation locally: 1. Run the development server with: ```shell - mkdocs serve + # For contributors. + mkdocs serve -f mkdocs.generated.yml + + # For members of the Astral org, which has access to MkDocs Insiders via sponsorship. + mkdocs serve -f mkdocs.insiders.yml ``` The documentation should then be available locally at @@ -247,6 +279,28 @@ them to [PyPI](https://pypi.org/project/ruff/). Ruff follows the [semver](https://semver.org/) versioning standard. However, as pre-1.0 software, even patch releases may contain [non-backwards-compatible changes](https://semver.org/#spec-item-4). +### Creating a new release + +1. Update the version with `rg 0.0.269 --files-with-matches | xargs sed -i 's/0.0.269/0.0.270/g'` +1. Update `BREAKING_CHANGES.md` +1. Create a PR with the version and `BREAKING_CHANGES.md` updated +1. Merge the PR +1. Run the release workflow with the version number (without starting `v`) as input. Make sure + main has your merged PR as last commit +1. The release workflow will do the following: + 1. Build all the assets. If this fails (even though we tested in step 4), we haven’t tagged or + uploaded anything, you can restart after pushing a fix + 1. Upload to pypi + 1. Create and push the git tag (from pyproject.toml). We create the git tag only here + because we can't change it ([#4468](https://github.com/charliermarsh/ruff/issues/4468)), so + we want to make sure everything up to and including publishing to pypi worked. + 1. Attach artifacts to draft GitHub release + 1. Trigger downstream repositories. This can fail without causing fallout, it is possible (if + inconvenient) to trigger the downstream jobs manually +1. Create release notes in GitHub UI and promote from draft to proper release() +1. If needed, [update the schemastore](https://github.com/charliermarsh/ruff/blob/main/scripts/update_schemastore.py) +1. If needed, update ruff-lsp and ruff-vscode + ## Ecosystem CI GitHub Actions will run your changes against a number of real-world projects from GitHub and @@ -261,7 +315,15 @@ downloading the [`known-github-tomls.json`](https://github.com/akx/ruff-usage-ag as `github_search.jsonl` and following the instructions in [scripts/Dockerfile.ecosystem](https://github.com/astral-sh/ruff/blob/main/scripts/Dockerfile.ecosystem). Note that this check will take a while to run. -## Benchmarks +## Benchmarking and Profiling + +We have several ways of benchmarking and profiling Ruff: + +- Our main performance benchmark comparing Ruff with other tools on the CPython codebase +- Microbenchmarks which the linter or the formatter on individual files. There run on pull requests. +- Profiling the linter on either the microbenchmarks or entire projects + +### CPython Benchmark First, clone [CPython](https://github.com/python/cpython). It's a large and diverse Python codebase, which makes it a good target for benchmarking. @@ -273,22 +335,18 @@ git clone --branch 3.10 https://github.com/python/cpython.git crates/ruff/resour To benchmark the release build: ```shell -cargo build --release && hyperfine --ignore-failure --warmup 10 \ - "./target/release/ruff ./crates/ruff/resources/test/cpython/ --no-cache" \ - "./target/release/ruff ./crates/ruff/resources/test/cpython/" +cargo build --release && hyperfine --warmup 10 \ + "./target/release/ruff ./crates/ruff/resources/test/cpython/ --no-cache -e" \ + "./target/release/ruff ./crates/ruff/resources/test/cpython/ -e" Benchmark 1: ./target/release/ruff ./crates/ruff/resources/test/cpython/ --no-cache Time (mean ± σ): 293.8 ms ± 3.2 ms [User: 2384.6 ms, System: 90.3 ms] Range (min … max): 289.9 ms … 301.6 ms 10 runs - Warning: Ignoring non-zero exit code. - Benchmark 2: ./target/release/ruff ./crates/ruff/resources/test/cpython/ Time (mean ± σ): 48.0 ms ± 3.1 ms [User: 65.2 ms, System: 124.7 ms] Range (min … max): 45.0 ms … 66.7 ms 62 runs - Warning: Ignoring non-zero exit code. - Summary './target/release/ruff ./crates/ruff/resources/test/cpython/' ran 6.12 ± 0.41 times faster than './target/release/ruff ./crates/ruff/resources/test/cpython/ --no-cache' @@ -340,9 +398,9 @@ Summary 159.43 ± 2.48 times faster than 'pycodestyle crates/ruff/resources/test/cpython' ``` -You can run `poetry install` from `./scripts` to create a working environment for the above. All -reported benchmarks were computed using the versions specified by `./scripts/pyproject.toml` -on Python 3.11. +You can run `poetry install` from `./scripts/benchmarks` to create a working environment for the +above. All reported benchmarks were computed using the versions specified by +`./scripts/benchmarks/pyproject.toml` on Python 3.11. To benchmark Pylint, remove the following files from the CPython repository: @@ -383,3 +441,247 @@ Benchmark 1: find . -type f -name "*.py" | xargs -P 0 pyupgrade --py311-plus Time (mean ± σ): 30.119 s ± 0.195 s [User: 28.638 s, System: 0.390 s] Range (min … max): 29.813 s … 30.356 s 10 runs ``` + +### Microbenchmarks + +The `ruff_benchmark` crate benchmarks the linter and the formatter on individual files. + +You can run the benchmarks with + +```shell +cargo benchmark +``` + +#### Benchmark driven Development + +Ruff uses [Criterion.rs](https://bheisler.github.io/criterion.rs/book/) for benchmarks. You can use +`--save-baseline=` to store an initial baseline benchmark (e.g. on `main`) and then use +`--benchmark=` to compare against that benchmark. Criterion will print a message telling you +if the benchmark improved/regressed compared to that baseline. + +```shell +# Run once on your "baseline" code +cargo benchmark --save-baseline=main + +# Then iterate with +cargo benchmark --baseline=main +``` + +#### PR Summary + +You can use `--save-baseline` and `critcmp` to get a pretty comparison between two recordings. +This is useful to illustrate the improvements of a PR. + +```shell +# On main +cargo benchmark --save-baseline=main + +# After applying your changes +cargo benchmark --save-baseline=pr + +critcmp main pr +``` + +You must install [`critcmp`](https://github.com/BurntSushi/critcmp) for the comparison. + +```bash +cargo install critcmp +``` + +#### Tips + +- Use `cargo benchmark ` to only run specific benchmarks. For example: `cargo benchmark linter/pydantic` + to only run the pydantic tests. +- Use `cargo benchmark --quiet` for a more cleaned up output (without statistical relevance) +- Use `cargo benchmark --quick` to get faster results (more prone to noise) + +### Profiling Projects + +You can either use the microbenchmarks from above or a project directory for benchmarking. There +are a lot of profiling tools out there, +[The Rust Performance Book](https://nnethercote.github.io/perf-book/profiling.html) lists some +examples. + +#### Linux + +Install `perf` and build `ruff_benchmark` with the `release-debug` profile and then run it with perf + +```shell +cargo bench -p ruff_benchmark --no-run --profile=release-debug && perf record --call-graph dwarf -F 9999 cargo bench -p ruff_benchmark --profile=release-debug -- --profile-time=1 +``` + +You can also use the `ruff_dev` launcher to run `ruff check` multiple times on a repository to +gather enough samples for a good flamegraph (change the 999, the sample rate, and the 30, the number +of checks, to your liking) + +```shell +cargo build --bin ruff_dev --profile=release-debug +perf record -g -F 999 target/release-debug/ruff_dev repeat --repeat 30 --exit-zero --no-cache path/to/cpython > /dev/null +``` + +Then convert the recorded profile + +```shell +perf script -F +pid > /tmp/test.perf +``` + +You can now view the converted file with [firefox profiler](https://profiler.firefox.com/), with a +more in-depth guide [here](https://profiler.firefox.com/docs/#/./guide-perf-profiling) + +An alternative is to convert the perf data to `flamegraph.svg` using +[flamegraph](https://github.com/flamegraph-rs/flamegraph) (`cargo install flamegraph`): + +```shell +flamegraph --perfdata perf.data +``` + +#### Mac + +Install [`cargo-instruments`](https://crates.io/crates/cargo-instruments): + +```shell +cargo install cargo-instruments +``` + +Then run the profiler with + +```shell +cargo instruments -t time --bench linter --profile release-debug -p ruff_benchmark -- --profile-time=1 +``` + +- `-t`: Specifies what to profile. Useful options are `time` to profile the wall time and `alloc` + for profiling the allocations. +- You may want to pass an additional filter to run a single test file + +Otherwise, follow the instructions from the linux section. + +## `cargo dev` + +`cargo dev` is a shortcut for `cargo run --package ruff_dev --bin ruff_dev`. You can run some useful +utils with it: + +- `cargo dev print-ast `: Print the AST of a python file using the + [RustPython parser](https://github.com/astral-sh/RustPython-Parser/tree/main/parser) that is + mainly used in Ruff. For `if True: pass # comment`, you can see the syntax tree, the byte offsets + for start and stop of each node and also how the `:` token, the comment and whitespace are not + represented anymore: + +```text +[ + If( + StmtIf { + range: 0..13, + test: Constant( + ExprConstant { + range: 3..7, + value: Bool( + true, + ), + kind: None, + }, + ), + body: [ + Pass( + StmtPass { + range: 9..13, + }, + ), + ], + orelse: [], + }, + ), +] +``` + +- `cargo dev print-tokens `: Print the tokens that the AST is built upon. Again for + `if True: pass # comment`: + +```text +0 If 2 +3 True 7 +7 Colon 8 +9 Pass 13 +14 Comment( + "# comment", +) 23 +23 Newline 24 +``` + +- `cargo dev print-cst `: Print the CST of a python file using + [LibCST](https://github.com/Instagram/LibCST), which is used in addition to the RustPython parser + in Ruff. E.g. for `if True: pass # comment` everything including the whitespace is represented: + +```text +Module { + body: [ + Compound( + If( + If { + test: Name( + Name { + value: "True", + lpar: [], + rpar: [], + }, + ), + body: SimpleStatementSuite( + SimpleStatementSuite { + body: [ + Pass( + Pass { + semicolon: None, + }, + ), + ], + leading_whitespace: SimpleWhitespace( + " ", + ), + trailing_whitespace: TrailingWhitespace { + whitespace: SimpleWhitespace( + " ", + ), + comment: Some( + Comment( + "# comment", + ), + ), + newline: Newline( + None, + Real, + ), + }, + }, + ), + orelse: None, + leading_lines: [], + whitespace_before_test: SimpleWhitespace( + " ", + ), + whitespace_after_test: SimpleWhitespace( + "", + ), + is_elif: false, + }, + ), + ), + ], + header: [], + footer: [], + default_indent: " ", + default_newline: "\n", + has_trailing_newline: true, + encoding: "utf-8", +} +``` + +- `cargo dev generate-all`: Update `ruff.schema.json`, `docs/configuration.md` and `docs/rules`. + You can also set `RUFF_UPDATE_SCHEMA=1` to update `ruff.schema.json` during `cargo test`. +- `cargo dev generate-cli-help`, `cargo dev generate-docs` and `cargo dev generate-json-schema`: + Update just `docs/configuration.md`, `docs/rules` and `ruff.schema.json` respectively. +- `cargo dev generate-options`: Generate a markdown-compatible table of all `pyproject.toml` + options. Used for +- `cargo dev generate-rules-table`: Generate a markdown-compatible table of all rules. Used for +- `cargo dev round-trip `: Read a Python file or Jupyter Notebook, + parse it, serialize the parsed representation and write it back. Used to check how good our + representation is so that fixes don't rewrite irrelevant parts of a file. +- `cargo dev format_dev`: See ruff_python_formatter README.md diff --git a/Cargo.lock b/Cargo.lock index d0c654d156..f3ce5f85ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -25,9 +25,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" dependencies = [ "memchr", ] @@ -86,15 +86,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" +checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" [[package]] name = "anstyle-parse" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" +checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" dependencies = [ "utf8parse", ] @@ -148,17 +148,6 @@ dependencies = [ "wait-timeout", ] -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi 0.1.19", - "libc", - "winapi", -] - [[package]] name = "autocfg" version = "1.1.0" @@ -167,9 +156,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "base64" -version = "0.13.1" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" [[package]] name = "bincode" @@ -188,18 +177,17 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.3.1" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6776fc96284a0bb647b615056fc496d1fe1644a7ab01829818a6d91cae888b84" +checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" [[package]] name = "bstr" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a246e68bb43f6cd9db24bea052a53e40405417c5fb372e3d1a8a7f770a564ef5" +checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05" dependencies = [ "memchr", - "once_cell", "regex-automata", "serde", ] @@ -256,7 +244,8 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", - "time", + "serde", + "time 0.1.45", "wasm-bindgen", "winapi", ] @@ -290,9 +279,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.3.1" +version = "4.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ed2379f8603fa2b7509891660e802b88c70a79a6427a70abb5968054de2c28" +checksum = "1640e5cc7fb47dbb8338fd471b105e7ed6c3cb2aeb00c2e067127ffd3764a05d" dependencies = [ "clap_builder", "clap_derive", @@ -301,22 +290,21 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.3.1" +version = "4.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72394f3339a76daf211e57d4bcb374410f3965dcc606dd0e03738c7888766980" +checksum = "98c59138d527eeaf9b53f35a77fcc1fad9d883116070c63d5de1c7dc7b00c72b" dependencies = [ "anstream", "anstyle", - "bitflags 1.3.2", "clap_lex", "strsim", ] [[package]] name = "clap_complete" -version = "4.3.1" +version = "4.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6b5c519bab3ea61843a7923d074b04245624bb84a64a8c150f5deb014e388b" +checksum = "5fc443334c81a804575546c5a8a79b4913b50e28d69232903604cada1de817ce" dependencies = [ "clap", ] @@ -355,14 +343,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.3.1" +version = "4.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59e9ef9a08ee1c0e1f2e162121665ac45ac3783b0f897db7244ae75ad9a8f65b" +checksum = "b8cd2b2a819ad6eec39e8f1d6b53001af1e5469f8c177579cdaeb313115b825f" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.23", ] [[package]] @@ -392,13 +380,13 @@ checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "colored" -version = "2.0.0" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" +checksum = "2674ec482fbc38012cf31e6c42ba0177b431a0cb6f15fe40efa5aab1bda516f6" dependencies = [ - "atty", + "is-terminal", "lazy_static", - "winapi", + "windows-sys 0.48.0", ] [[package]] @@ -416,6 +404,7 @@ dependencies = [ "encode_unicode", "lazy_static", "libc", + "unicode-width", "windows-sys 0.45.0", ] @@ -519,9 +508,9 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.14" +version = "0.9.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" dependencies = [ "autocfg", "cfg-if", @@ -532,9 +521,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.15" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" dependencies = [ "cfg-if", ] @@ -555,6 +544,41 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "darling" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0558d22a7b463ed0241e993f76f09f30b126687447751a8638587b864e4b3944" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8bfa2e259f8ee1ce5e97824a3c55ec4404a0d772ca7fa96bf19f0752a046eb" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.23", +] + +[[package]] +name = "darling_macro" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29a358ff9f12ec09c3e61fef9b5a9902623a695a46a917b07f269bff1445611a" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.23", +] + [[package]] name = "diff" version = "0.1.13" @@ -638,6 +662,25 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +[[package]] +name = "env_logger" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1" + [[package]] name = "errno" version = "0.3.1" @@ -691,7 +734,7 @@ dependencies = [ [[package]] name = "flake8-to-ruff" -version = "0.0.272" +version = "0.0.278" dependencies = [ "anyhow", "clap", @@ -726,9 +769,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" dependencies = [ "percent-encoding", ] @@ -744,9 +787,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if", "libc", @@ -784,6 +827,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" + [[package]] name = "heck" version = "0.4.1" @@ -792,27 +841,15 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.1.19" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" [[package]] -name = "hermit-abi" -version = "0.2.6" +name = "hex" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" -dependencies = [ - "libc", -] - -[[package]] -name = "hermit-abi" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hexf-parse" @@ -821,10 +858,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" [[package]] -name = "iana-time-zone" -version = "0.1.56" +name = "humantime" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "iana-time-zone" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -844,10 +887,16 @@ dependencies = [ ] [[package]] -name = "idna" -version = "0.3.0" +name = "ident_case" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -887,10 +936,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", "serde", ] +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown 0.14.0", +] + +[[package]] +name = "indicatif" +version = "0.17.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ff8cc23a7393a397ed1d7f56e6365cba772aba9f9912ab968b03043c395d057" +dependencies = [ + "console", + "instant", + "number_prefix", + "portable-atomic", + "unicode-width", +] + [[package]] name = "inotify" version = "0.9.6" @@ -913,14 +985,16 @@ dependencies = [ [[package]] name = "insta" -version = "1.29.0" +version = "1.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a28d25139df397cbca21408bb742cf6837e04cdbebf1b07b760caf971d6a972" +checksum = "28491f7753051e5704d4d0ae7860d45fae3238d7d235bc4289dcd45c48d3cec3" dependencies = [ "console", + "globset", "lazy_static", "linked-hash-map", "similar", + "walkdir", "yaml-rust", ] @@ -939,7 +1013,7 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ - "hermit-abi 0.3.1", + "hermit-abi", "libc", "windows-sys 0.48.0", ] @@ -959,13 +1033,12 @@ dependencies = [ [[package]] name = "is-terminal" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" +checksum = "24fddda5af7e54bf7da53067d6e802dbcc381d0a8eef629df528e3ebf68755cb" dependencies = [ - "hermit-abi 0.3.1", - "io-lifetimes", - "rustix", + "hermit-abi", + "rustix 0.38.3", "windows-sys 0.48.0", ] @@ -980,15 +1053,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.6" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" +checksum = "62b02a5381cc465bd3041d84623d0fa3b66738b52b8e2fc3bab8ad63ab032f4a" [[package]] name = "js-sys" -version = "0.3.63" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f37a4a5928311ac501dee68b3c7613a1037d0edb30c8e5427bd832d55d1b790" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" dependencies = [ "wasm-bindgen", ] @@ -1057,14 +1130,14 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.144" +version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" [[package]] name = "libcst" version = "0.1.0" -source = "git+https://github.com/charliermarsh/LibCST?rev=80e4c1399f95e5beb532fdd1e209ad2dbb470438#80e4c1399f95e5beb532fdd1e209ad2dbb470438" +source = "git+https://github.com/Instagram/LibCST.git?rev=3cacca1a1029f05707e50703b49fe3dd860aa839#3cacca1a1029f05707e50703b49fe3dd860aa839" dependencies = [ "chic", "itertools", @@ -1079,7 +1152,7 @@ dependencies = [ [[package]] name = "libcst_derive" version = "0.1.0" -source = "git+https://github.com/charliermarsh/LibCST?rev=80e4c1399f95e5beb532fdd1e209ad2dbb470438#80e4c1399f95e5beb532fdd1e209ad2dbb470438" +source = "git+https://github.com/Instagram/LibCST.git?rev=3cacca1a1029f05707e50703b49fe3dd860aa839#3cacca1a1029f05707e50703b49fe3dd860aa839" dependencies = [ "quote", "syn 1.0.109", @@ -1108,10 +1181,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] -name = "log" -version = "0.4.18" +name = "linux-raw-sys" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de" +checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" + +[[package]] +name = "log" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" [[package]] name = "matches" @@ -1127,9 +1206,9 @@ checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "memoffset" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" dependencies = [ "autocfg", ] @@ -1260,19 +1339,25 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.2.6", + "hermit-abi", "libc", ] [[package]] -name = "once_cell" -version = "1.17.2" +name = "number_prefix" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9670a07f94779e00908f3e686eab508878ebb390ba6e604d3a284c00e8d0487b" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "oorandom" @@ -1288,9 +1373,9 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "os_str_bytes" -version = "6.5.0" +version = "6.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267" +checksum = "4d5d9eb14b174ee9aa2ef96dc2b94637a2d4b6e7cb873c7e171f0c20c6cf3eac" dependencies = [ "memchr", ] @@ -1306,9 +1391,9 @@ dependencies = [ [[package]] name = "paste" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" +checksum = "b4b27ab7be369122c218afc2079489cdcb4b517c0a3fc386ff11e1fedfcc2b35" [[package]] name = "path-absolutize" @@ -1363,22 +1448,21 @@ checksum = "9fa00462b37ead6d11a82c9d568b26682d78e0477dc02d1966c013af80969739" [[package]] name = "pep440_rs" -version = "0.3.9" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe1d15693a11422cfa7d401b00dc9ae9fb8edbfbcb711a77130663f4ddf67650" +checksum = "b05bf2c44c4cd12f03b2c3ca095f3aa21f44e43c16021c332e511884719705be" dependencies = [ "lazy_static", "regex", "serde", - "tracing", "unicode-width", ] [[package]] name = "pep508_rs" -version = "0.1.5" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "969679a29dfdc8278a449f75b3dd45edf57e649bd59f7502429c2840751c46d8" +checksum = "c0713d7bb861ca2b7d4c50a38e1f31a4b63a2e2df35ef1e5855cc29e108453e2" dependencies = [ "once_cell", "pep440_rs", @@ -1392,15 +1476,15 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "phf" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928c6535de93548188ef63bb7c4036bd415cd8f36ad25af44b9789b2ee72a48c" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ "phf_macros", "phf_shared", @@ -1408,9 +1492,9 @@ dependencies = [ [[package]] name = "phf_codegen" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56ac890c5e3ca598bbdeaa99964edb5b0258a583a9eb6ef4e89fc85d9224770" +checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" dependencies = [ "phf_generator", "phf_shared", @@ -1418,9 +1502,9 @@ dependencies = [ [[package]] name = "phf_generator" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1181c94580fa345f50f19d738aaa39c0ed30a600d95cb2d3e23f94266f14fbf" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" dependencies = [ "phf_shared", "rand", @@ -1428,37 +1512,37 @@ dependencies = [ [[package]] name = "phf_macros" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92aacdc5f16768709a569e913f7451034034178b05bdc8acda226659a3dccc66" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" dependencies = [ "phf_generator", "phf_shared", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.23", ] [[package]] name = "phf_shared" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1fb5f6f826b772a8d4c0394209441e7d37cbbb967ae9c7e0e8134365c9ee676" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" dependencies = [ "siphasher", ] [[package]] name = "pin-project-lite" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" [[package]] name = "plotters" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2538b639e642295546c50fcd545198c9d64ee2a38620a628724a3b266d5fbf97" +checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" dependencies = [ "num-traits", "plotters-backend", @@ -1469,15 +1553,15 @@ dependencies = [ [[package]] name = "plotters-backend" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "193228616381fecdc1224c62e96946dfbc73ff4384fba576e052ff8c1bea8142" +checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" [[package]] name = "plotters-svg" -version = "0.3.3" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a81d2759aae1dae668f783c308bc5c8ebd191ff4184aaa1b37f65a6ae5a56f" +checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" dependencies = [ "plotters-backend", ] @@ -1493,6 +1577,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "portable-atomic" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "767eb9f07d4a5ebcb39bbf2d452058a93c011373abf6832e24194a1c3f004794" + [[package]] name = "predicates" version = "3.0.3" @@ -1559,20 +1649,20 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.59" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b" +checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb" dependencies = [ "unicode-ident", ] [[package]] name = "pyproject-toml" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04dbbb336bd88583943c7cd973a32fed323578243a7569f40cb0c7da673321b" +checksum = "ee79feaa9d31e1c417e34219e610b67db4e786ce9b49d77dda549640abb9dc5f" dependencies = [ - "indexmap", + "indexmap 1.9.3", "pep440_rs", "pep508_rs", "serde", @@ -1586,7 +1676,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05b909fe9bf2abb1e3d6a97c9189a37c8105c61d03dca9ce6aace023e7d682bd" dependencies = [ "chrono", - "indexmap", + "indexmap 1.9.3", "nextest-workspace-hack", "quick-xml", "thiserror", @@ -1604,9 +1694,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.28" +version = "1.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" +checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" dependencies = [ "proc-macro2", ] @@ -1679,26 +1769,32 @@ dependencies = [ [[package]] name = "regex" -version = "1.8.3" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81ca098a9821bd52d6b24fd8b10bd081f47d39c22778cafaa75a2857a62c6390" +checksum = "89089e897c013b3deb627116ae56a6955a72b8bed395c9526af31c9fe528b484" dependencies = [ - "aho-corasick 1.0.1", + "aho-corasick 1.0.2", "memchr", + "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" -version = "0.1.10" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +checksum = "fa250384981ea14565685dea16a9ccc4d1c541a13f82b9c168572264d1df8c56" +dependencies = [ + "aho-corasick 1.0.2", + "memchr", + "regex-syntax", +] [[package]] name = "regex-syntax" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" +checksum = "2ab07dc67230e4a4718e70fd5c20055a4334b121f1f9db8fe63ef39ce9b8c846" [[package]] name = "result-like" @@ -1739,11 +1835,11 @@ dependencies = [ [[package]] name = "ruff" -version = "0.0.272" +version = "0.0.278" dependencies = [ "annotate-snippets 0.9.1", "anyhow", - "bitflags 2.3.1", + "bitflags 2.3.3", "chrono", "clap", "colored", @@ -1758,6 +1854,7 @@ dependencies = [ "itertools", "libcst", "log", + "memchr", "natord", "nohash-hasher", "num-bigint", @@ -1774,6 +1871,7 @@ dependencies = [ "result-like", "ruff_cache", "ruff_diagnostics", + "ruff_index", "ruff_macros", "ruff_python_ast", "ruff_python_semantic", @@ -1789,6 +1887,7 @@ dependencies = [ "semver", "serde", "serde_json", + "serde_with", "shellexpand", "similar", "smallvec", @@ -1800,6 +1899,7 @@ dependencies = [ "typed-arena", "unicode-width", "unicode_names2", + "wsl", ] [[package]] @@ -1825,6 +1925,7 @@ name = "ruff_cache" version = "0.0.0" dependencies = [ "filetime", + "glob", "globset", "itertools", "regex", @@ -1833,15 +1934,14 @@ dependencies = [ [[package]] name = "ruff_cli" -version = "0.0.272" +version = "0.0.278" dependencies = [ "annotate-snippets 0.9.1", "anyhow", "argfile", "assert_cmd", - "atty", "bincode", - "bitflags 2.3.1", + "bitflags 2.3.3", "cachedir", "chrono", "clap", @@ -1852,6 +1952,7 @@ dependencies = [ "glob", "ignore", "itertools", + "itoa", "log", "mimalloc", "notify", @@ -1884,21 +1985,30 @@ version = "0.0.0" dependencies = [ "anyhow", "clap", + "ignore", + "indicatif", "itertools", "libcst", + "log", "once_cell", "pretty_assertions", + "rayon", "regex", "ruff", "ruff_cli", "ruff_diagnostics", + "ruff_formatter", + "ruff_python_formatter", + "ruff_python_stdlib", "ruff_textwrap", "rustpython-format", "rustpython-parser", "schemars", "serde_json", + "similar", "strum", "strum_macros", + "tempfile", ] [[package]] @@ -1942,7 +2052,7 @@ dependencies = [ "proc-macro2", "quote", "ruff_textwrap", - "syn 2.0.18", + "syn 2.0.23", ] [[package]] @@ -1950,7 +2060,7 @@ name = "ruff_python_ast" version = "0.0.0" dependencies = [ "anyhow", - "bitflags 2.3.1", + "bitflags 2.3.3", "insta", "is-macro", "itertools", @@ -1974,6 +2084,7 @@ name = "ruff_python_formatter" version = "0.0.0" dependencies = [ "anyhow", + "bitflags 2.3.3", "clap", "countme", "insta", @@ -1983,19 +2094,32 @@ dependencies = [ "ruff_formatter", "ruff_python_ast", "ruff_python_whitespace", - "ruff_testing_macros", "ruff_text_size", "rustc-hash", "rustpython-parser", + "serde", + "serde_json", "similar", - "test-case", + "smallvec", + "thiserror", + "unic-ucd-ident", +] + +[[package]] +name = "ruff_python_resolver" +version = "0.0.0" +dependencies = [ + "env_logger", + "insta", + "log", + "tempfile", ] [[package]] name = "ruff_python_semantic" version = "0.0.0" dependencies = [ - "bitflags 2.3.1", + "bitflags 2.3.3", "is-macro", "nohash-hasher", "num-traits", @@ -2011,10 +2135,6 @@ dependencies = [ [[package]] name = "ruff_python_stdlib" version = "0.0.0" -dependencies = [ - "once_cell", - "rustc-hash", -] [[package]] name = "ruff_python_whitespace" @@ -2032,20 +2152,10 @@ dependencies = [ "rustpython-parser", ] -[[package]] -name = "ruff_testing_macros" -version = "0.0.0" -dependencies = [ - "glob", - "proc-macro2", - "quote", - "syn 2.0.18", -] - [[package]] name = "ruff_text_size" version = "0.0.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=0dc8fdf52d146698c5bcf0b842fddc9e398ad8db#0dc8fdf52d146698c5bcf0b842fddc9e398ad8db" +source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=c174bbf1f29527edd43d432326327f16f47ab9e0#c174bbf1f29527edd43d432326327f16f47ab9e0" dependencies = [ "schemars", "serde", @@ -2096,34 +2206,57 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.37.19" +version = "0.37.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" +checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" dependencies = [ "bitflags 1.3.2", "errno", "io-lifetimes", "libc", - "linux-raw-sys", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustix" +version = "0.38.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac5ffa1efe7548069688cd7028f32591853cd7b5b756d41bcffd2353e4fc75b4" +dependencies = [ + "bitflags 2.3.3", + "errno", + "libc", + "linux-raw-sys 0.4.3", "windows-sys 0.48.0", ] [[package]] name = "rustls" -version = "0.20.8" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" +checksum = "e32ca28af694bc1bbf399c33a516dbdf1c90090b8ab23c2bc24f834aa2247f5f" dependencies = [ "log", "ring", + "rustls-webpki", "sct", - "webpki", +] + +[[package]] +name = "rustls-webpki" +version = "0.100.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6207cd5ed3d8dca7816f8f3725513a34609c0c765bf652b8c3cb4cfd87db46b" +dependencies = [ + "ring", + "untrusted", ] [[package]] name = "rustpython-ast" version = "0.2.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=0dc8fdf52d146698c5bcf0b842fddc9e398ad8db#0dc8fdf52d146698c5bcf0b842fddc9e398ad8db" +source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=c174bbf1f29527edd43d432326327f16f47ab9e0#c174bbf1f29527edd43d432326327f16f47ab9e0" dependencies = [ "is-macro", "num-bigint", @@ -2134,9 +2267,9 @@ dependencies = [ [[package]] name = "rustpython-format" version = "0.2.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=0dc8fdf52d146698c5bcf0b842fddc9e398ad8db#0dc8fdf52d146698c5bcf0b842fddc9e398ad8db" +source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=c174bbf1f29527edd43d432326327f16f47ab9e0#c174bbf1f29527edd43d432326327f16f47ab9e0" dependencies = [ - "bitflags 2.3.1", + "bitflags 2.3.3", "itertools", "num-bigint", "num-traits", @@ -2146,7 +2279,7 @@ dependencies = [ [[package]] name = "rustpython-literal" version = "0.2.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=0dc8fdf52d146698c5bcf0b842fddc9e398ad8db#0dc8fdf52d146698c5bcf0b842fddc9e398ad8db" +source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=c174bbf1f29527edd43d432326327f16f47ab9e0#c174bbf1f29527edd43d432326327f16f47ab9e0" dependencies = [ "hexf-parse", "is-macro", @@ -2158,7 +2291,7 @@ dependencies = [ [[package]] name = "rustpython-parser" version = "0.2.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=0dc8fdf52d146698c5bcf0b842fddc9e398ad8db#0dc8fdf52d146698c5bcf0b842fddc9e398ad8db" +source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=c174bbf1f29527edd43d432326327f16f47ab9e0#c174bbf1f29527edd43d432326327f16f47ab9e0" dependencies = [ "anyhow", "is-macro", @@ -2181,23 +2314,24 @@ dependencies = [ [[package]] name = "rustpython-parser-core" version = "0.2.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=0dc8fdf52d146698c5bcf0b842fddc9e398ad8db#0dc8fdf52d146698c5bcf0b842fddc9e398ad8db" +source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=c174bbf1f29527edd43d432326327f16f47ab9e0#c174bbf1f29527edd43d432326327f16f47ab9e0" dependencies = [ "is-macro", + "memchr", "ruff_text_size", ] [[package]] name = "rustversion" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06" +checksum = "dc31bd9b61a32c31f9650d18add92aa83a49ba979c143eefd27fe7177b05bd5f" [[package]] name = "ryu" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +checksum = "fe232bdf6be8c8de797b22184ee71118d63780ea42ac85b61d1baa6d3b782ae9" [[package]] name = "same-file" @@ -2262,9 +2396,9 @@ checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" [[package]] name = "serde" -version = "1.0.163" +version = "1.0.166" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" +checksum = "d01b7404f9d441d3ad40e6a636a7782c377d2abdbe4fa2440e2edcc2f4f10db8" dependencies = [ "serde_derive", ] @@ -2282,13 +2416,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.163" +version = "1.0.166" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" +checksum = "5dd83d6dde2b6b2d466e14d9d1acce8816dedee94f735eac6395808b3483c6d6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.23", ] [[package]] @@ -2304,11 +2438,10 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.96" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +checksum = "0f1e14e89be7aa4c4b78bdbdc9eb5bf8517829a600ae8eaa39a6e1d960b5185c" dependencies = [ - "indexmap", "itoa", "ryu", "serde", @@ -2316,13 +2449,41 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93107647184f6027e3b7dcb2e11034cf95ffa1e3a682c67951963ac69c1c007d" +checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f02d8aa6e3c385bf084924f660ce2a3a6bd333ba55b35e8590b321f35d88513" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "serde", + "serde_json", + "serde_with_macros", + "time 0.3.22", +] + +[[package]] +name = "serde_with_macros" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edc7d5d3932fb12ce722ee5e64dd38c504efba37567f0c402f6ca728c3b8b070" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.23", +] + [[package]] name = "shellexpand" version = "3.1.0" @@ -2403,9 +2564,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.18" +version = "2.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" +checksum = "59fb7d6d8281a51045d62b8eb3a7d1ce347b76f312af50cd3dc0af39c87c1737" dependencies = [ "proc-macro2", "quote", @@ -2423,15 +2584,25 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.5.0" +version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" +checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" dependencies = [ + "autocfg", "cfg-if", "fastrand", "redox_syscall 0.3.5", - "rustix", - "windows-sys 0.45.0", + "rustix 0.37.23", + "windows-sys 0.48.0", +] + +[[package]] +name = "termcolor" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ + "winapi-util", ] [[package]] @@ -2490,22 +2661,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.40" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +checksum = "a35fc5b8971143ca348fa6df4f024d4d55264f3468c71ad1c2f365b0a4d58c42" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.40" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.23", ] [[package]] @@ -2549,6 +2720,33 @@ dependencies = [ "winapi", ] +[[package]] +name = "time" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd" +dependencies = [ + "itoa", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" + +[[package]] +name = "time-macros" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" +dependencies = [ + "time-core", +] + [[package]] name = "tiny-keccak" version = "2.0.2" @@ -2585,9 +2783,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "toml" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6135d499e69981f9ff0ef2167955a5333c35e36f6937d382974566b3d5b94ec" +checksum = "1ebafdf5ad1220cb59e7d17cf4d2c72015297b75b19a10472f99b89225089240" dependencies = [ "serde", "serde_spanned", @@ -2597,20 +2795,20 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a76a9312f5ba4c2dec6b9161fdf25d87ad8a09256ccea5a556fef03c706a10f" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.19.10" +version = "0.19.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380d56e8670370eee6566b0bfd4265f65b3f432e8c6d85623f728d4fa31f739" +checksum = "266f016b7f039eec8a1a80dfe6156b633d208b9fccca5e4db1d6775b0c4e34a7" dependencies = [ - "indexmap", + "indexmap 2.0.0", "serde", "serde_spanned", "toml_datetime", @@ -2632,13 +2830,13 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.24" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" +checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.23", ] [[package]] @@ -2728,9 +2926,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" +checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73" [[package]] name = "unicode-normalization" @@ -2763,25 +2961,25 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "ureq" -version = "2.6.2" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "338b31dd1314f68f3aabf3ed57ab922df95ffcd902476ca7ba3c4ce7b908c46d" +checksum = "0b11c96ac7ee530603dcdf68ed1557050f374ce55a5a07193ebf8cbc9f8927e9" dependencies = [ "base64", "flate2", "log", "once_cell", "rustls", + "rustls-webpki", "url", - "webpki", "webpki-roots", ] [[package]] name = "url" -version = "2.3.1" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" dependencies = [ "form_urlencoded", "idna", @@ -2797,9 +2995,9 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uuid" -version = "1.3.3" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2" +checksum = "d023da39d1fde5a8a3fe1f3e01ca9632ada0a63e9797de55a879d6e2236277be" [[package]] name = "version_check" @@ -2840,9 +3038,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.86" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bba0e8cb82ba49ff4e229459ff22a191bbe9a1cb3a341610c9c33efc27ddf73" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -2850,24 +3048,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.86" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b04bc93f9d6bdee709f6bd2118f57dd6679cf1176a1af464fca3ab0d66d8fb" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.23", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.36" +version = "0.4.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d1985d03709c53167ce907ff394f5316aa22cb4e12761295c5dc57dacb6297e" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" dependencies = [ "cfg-if", "js-sys", @@ -2877,9 +3075,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.86" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14d6b024f1a526bb0234f52840389927257beb670610081360e5a03c5df9c258" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2887,28 +3085,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.86" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.23", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.86" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" [[package]] name = "wasm-bindgen-test" -version = "0.3.36" +version = "0.3.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e636f3a428ff62b3742ebc3c70e254dfe12b8c2b469d688ea59cdd4abcf502" +checksum = "6e6e302a7ea94f83a6d09e78e7dc7d9ca7b186bc2829c24a22d0753efd680671" dependencies = [ "console_error_panic_hook", "js-sys", @@ -2920,9 +3118,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.36" +version = "0.3.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f18c1fad2f7c4958e7bcce014fa212f59a65d5e3721d0f77e6c0b27ede936ba3" +checksum = "ecb993dd8c836930ed130e020e77d9b2e65dd0fbab1b67c790b0f5d80b11a575" dependencies = [ "proc-macro2", "quote", @@ -2930,31 +3128,21 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.63" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bdd9ef4e984da1187bf8110c5cf5b845fbc87a23602cdf912386a76fcd3a7c2" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" dependencies = [ "js-sys", "wasm-bindgen", ] -[[package]] -name = "webpki" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "webpki-roots" -version = "0.22.6" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" +checksum = "b03058f88386e5ff5310d9111d53f48b17d732b401aeb83a8d5190f2ac459338" dependencies = [ - "webpki", + "rustls-webpki", ] [[package]] @@ -3014,7 +3202,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows-targets 0.48.0", + "windows-targets 0.48.1", ] [[package]] @@ -3032,7 +3220,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.48.0", + "windows-targets 0.48.1", ] [[package]] @@ -3052,9 +3240,9 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.48.0" +version = "0.48.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" dependencies = [ "windows_aarch64_gnullvm 0.48.0", "windows_aarch64_msvc 0.48.0", @@ -3151,13 +3339,19 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" [[package]] name = "winnow" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61de7bac303dc551fe038e2b3cef0f571087a47571ea6e79a87692ac99b99699" +checksum = "ca0ace3845f0d96209f0375e6d367e3eb87eb65d27d445bdc9f1843a26f39448" dependencies = [ "memchr", ] +[[package]] +name = "wsl" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dab7ac864710bdea6594becbea5b5050333cf34fefb0dc319567eb347950d4" + [[package]] name = "yaml-rust" version = "0.4.5" diff --git a/Cargo.toml b/Cargo.toml index fb9e209e36..b9f44b1438 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,10 +5,11 @@ resolver = "2" [workspace.package] edition = "2021" rust-version = "1.70" -homepage = "https://beta.ruff.rs/docs/" -documentation = "https://beta.ruff.rs/docs/" +homepage = "https://beta.ruff.rs/docs" +documentation = "https://beta.ruff.rs/docs" repository = "https://github.com/astral-sh/ruff" authors = ["Charlie Marsh "] +license = "MIT" [workspace.dependencies] anyhow = { version = "1.0.69" } @@ -20,10 +21,9 @@ filetime = { version = "0.2.20" } glob = { version = "0.3.1" } globset = { version = "0.4.10" } ignore = { version = "0.4.20" } -insta = { version = "1.28.0" } +insta = { version = "1.30.0" } is-macro = { version = "0.2.2" } itertools = { version = "0.10.5" } -libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "80e4c1399f95e5beb532fdd1e209ad2dbb470438" } log = { version = "0.4.17" } memchr = "2.5.0" nohash-hasher = { version = "0.2.0" } @@ -35,25 +35,35 @@ proc-macro2 = { version = "1.0.51" } quote = { version = "1.0.23" } regex = { version = "1.7.1" } rustc-hash = { version = "1.1.0" } -ruff_text_size = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "0dc8fdf52d146698c5bcf0b842fddc9e398ad8db" } -rustpython-ast = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "0dc8fdf52d146698c5bcf0b842fddc9e398ad8db", default-features = false, features = ["all-nodes-with-ranges"]} -rustpython-format = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "0dc8fdf52d146698c5bcf0b842fddc9e398ad8db" } -rustpython-literal = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "0dc8fdf52d146698c5bcf0b842fddc9e398ad8db" } -rustpython-parser = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "0dc8fdf52d146698c5bcf0b842fddc9e398ad8db", default-features = false, features = ["full-lexer", "all-nodes-with-ranges"] } schemars = { version = "0.8.12" } serde = { version = "1.0.152", features = ["derive"] } -serde_json = { version = "1.0.93", features = ["preserve_order"] } +serde_json = { version = "1.0.93" } shellexpand = { version = "3.0.0" } -similar = { version = "2.2.1" } +similar = { version = "2.2.1", features = ["inline"] } smallvec = { version = "1.10.0" } strum = { version = "0.24.1", features = ["strum_macros"] } strum_macros = { version = "0.24.3" } syn = { version = "2.0.15" } test-case = { version = "3.0.0" } +thiserror = { version = "1.0.43" } toml = { version = "0.7.2" } +wsl = { version = "0.1.0" } + +# v1.0.1 +libcst = { git = "https://github.com/Instagram/LibCST.git", rev = "3cacca1a1029f05707e50703b49fe3dd860aa839", default-features = false } + +# Please tag the RustPython version every time you update its revision here and in fuzz/Cargo.toml +# Tagging the version ensures that older ruff versions continue to build from source even when we rebase our RustPython fork. +# Current tag: v0.0.7 +ruff_text_size = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "c174bbf1f29527edd43d432326327f16f47ab9e0" } +rustpython-ast = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "c174bbf1f29527edd43d432326327f16f47ab9e0" , default-features = false, features = ["num-bigint"]} +rustpython-format = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "c174bbf1f29527edd43d432326327f16f47ab9e0", default-features = false, features = ["num-bigint"] } +rustpython-literal = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "c174bbf1f29527edd43d432326327f16f47ab9e0", default-features = false } +rustpython-parser = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "c174bbf1f29527edd43d432326327f16f47ab9e0" , default-features = false, features = ["full-lexer", "num-bigint"] } [profile.release] lto = "fat" +codegen-units = 1 [profile.dev.package.insta] opt-level = 3 diff --git a/LICENSE b/LICENSE index 932ce42b6b..8ffd09c5f2 100644 --- a/LICENSE +++ b/LICENSE @@ -1199,6 +1199,57 @@ are: - flake8-django, licensed under the GPL license. +- perflint, licensed as follows: + """ + MIT License + + Copyright (c) 2022 Anthony Shaw + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ + +- Pyright, licensed as follows: + """ + MIT License + + Pyright - A static type checker for the Python language + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + """ + - rust-analyzer/text-size, licensed under the MIT license: """ Permission is hereby granted, free of charge, to any diff --git a/README.md b/README.md index b23d42d610..7b1087920d 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,9 @@ An extremely fast Python linter, written in Rust.

- - - Shows a bar chart with benchmark results. + + + Shows a bar chart with benchmark results.

@@ -34,7 +34,8 @@ An extremely fast Python linter, written in Rust. - ⚖️ [Near-parity](https://beta.ruff.rs/docs/faq/#how-does-ruff-compare-to-flake8) with the built-in Flake8 rule set - 🔌 Native re-implementations of dozens of Flake8 plugins, like flake8-bugbear -- ⌨️ First-party editor integrations for [VS Code](https://github.com/astral-sh/ruff-vscode) and [more](https://github.com/astral-sh/ruff-lsp) +- ⌨️ First-party [editor integrations](https://beta.ruff.rs/docs/editor-integrations/) for + [VS Code](https://github.com/astral-sh/ruff-vscode) and [more](https://github.com/astral-sh/ruff-lsp) - 🌎 Monorepo-friendly, with [hierarchical and cascading configuration](https://beta.ruff.rs/docs/configuration/#pyprojecttoml-discovery) Ruff aims to be orders of magnitude faster than alternative tools while integrating more @@ -139,7 +140,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com) hook: ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.0.272 + rev: v0.0.278 hooks: - id: ruff ``` @@ -261,6 +262,7 @@ quality tools, including: - [flake8-builtins](https://pypi.org/project/flake8-builtins/) - [flake8-commas](https://pypi.org/project/flake8-commas/) - [flake8-comprehensions](https://pypi.org/project/flake8-comprehensions/) +- [flake8-copyright](https://pypi.org/project/flake8-copyright/) - [flake8-datetimez](https://pypi.org/project/flake8-datetimez/) - [flake8-debugger](https://pypi.org/project/flake8-debugger/) - [flake8-django](https://pypi.org/project/flake8-django/) @@ -329,9 +331,11 @@ We're grateful to the maintainers of these tools for their work, and for all the value they've provided to the Python community. Ruff's autoformatter is built on a fork of Rome's [`rome_formatter`](https://github.com/rome/tools/tree/main/crates/rome_formatter), -and again draws on both the APIs and implementation details of [Rome](https://github.com/rome/tools), +and again draws on both API and implementation details from [Rome](https://github.com/rome/tools), [Prettier](https://github.com/prettier/prettier), and [Black](https://github.com/psf/black). +Ruff's import resolver is based on the import resolution algorithm from [Pyright](https://github.com/microsoft/pyright). + Ruff is also influenced by a number of tools outside the Python ecosystem, like [Clippy](https://github.com/rust-lang/rust-clippy) and [ESLint](https://github.com/eslint/eslint). @@ -344,6 +348,7 @@ Ruff is released under the MIT license. Ruff is used by a number of major open-source projects and companies, including: - 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) - AstraZeneca ([Magnus](https://github.com/AstraZeneca/magnus-core)) - Benchling ([Refac](https://github.com/benchling/refac)) @@ -353,26 +358,30 @@ Ruff is used by a number of major open-source projects and companies, including: - [DVC](https://github.com/iterative/dvc) - [Dagger](https://github.com/dagger/dagger) - [Dagster](https://github.com/dagster-io/dagster) +- Databricks ([MLflow](https://github.com/mlflow/mlflow)) - [FastAPI](https://github.com/tiangolo/fastapi) - [Gradio](https://github.com/gradio-app/gradio) - [Great Expectations](https://github.com/great-expectations/great_expectations) +- [HTTPX](https://github.com/encode/httpx) - Hugging Face ([Transformers](https://github.com/huggingface/transformers), [Datasets](https://github.com/huggingface/datasets), [Diffusers](https://github.com/huggingface/diffusers)) - [Hatch](https://github.com/pypa/hatch) - [Home Assistant](https://github.com/home-assistant/core) +- ING Bank ([popmon](https://github.com/ing-bank/popmon), [probatus](https://github.com/ing-bank/probatus)) - [Ibis](https://github.com/ibis-project/ibis) - [Jupyter](https://github.com/jupyter-server/jupyter_server) - [LangChain](https://github.com/hwchase17/langchain) - [LlamaIndex](https://github.com/jerryjliu/llama_index) - Matrix ([Synapse](https://github.com/matrix-org/synapse)) -- Meltano ([Meltano CLI](https://github.com/meltano/meltano), [Singer SDK](https://github.com/meltano/sdk)) -- Modern Treasury ([Python SDK](https://github.com/Modern-Treasury/modern-treasury-python-sdk)) -- Mozilla ([Firefox](https://github.com/mozilla/gecko-dev)) - [MegaLinter](https://github.com/oxsecurity/megalinter) +- Meltano ([Meltano CLI](https://github.com/meltano/meltano), [Singer SDK](https://github.com/meltano/sdk)) - Microsoft ([Semantic Kernel](https://github.com/microsoft/semantic-kernel), [ONNX Runtime](https://github.com/microsoft/onnxruntime), [LightGBM](https://github.com/microsoft/LightGBM)) +- Modern Treasury ([Python SDK](https://github.com/Modern-Treasury/modern-treasury-python-sdk)) +- Mozilla ([Firefox](https://github.com/mozilla/gecko-dev)) +- [Mypy](https://github.com/python/mypy) - Netflix ([Dispatch](https://github.com/Netflix/dispatch)) - [Neon](https://github.com/neondatabase/neon) - [ONNX](https://github.com/onnx/onnx) @@ -408,6 +417,7 @@ Ruff is used by a number of major open-source projects and companies, including: - [featuretools](https://github.com/alteryx/featuretools) - [meson-python](https://github.com/mesonbuild/meson-python) - [nox](https://github.com/wntrblm/nox) +- [pip](https://github.com/pypa/pip) ### Show Your Support diff --git a/_typos.toml b/_typos.toml index 778ba59eaf..6d9da48c39 100644 --- a/_typos.toml +++ b/_typos.toml @@ -2,9 +2,9 @@ extend-exclude = ["resources", "snapshots"] [default.extend-words] -trivias = "trivias" hel = "hel" whos = "whos" spawnve = "spawnve" ned = "ned" poit = "poit" +BA = "BA" # acronym for "Bad Allowed", used in testing. diff --git a/assets/png/Astral.png b/assets/png/Astral.png new file mode 100644 index 0000000000..c55bead1ce Binary files /dev/null and b/assets/png/Astral.png differ diff --git a/crates/flake8_to_ruff/Cargo.toml b/crates/flake8_to_ruff/Cargo.toml index 05109e7495..012975a2b6 100644 --- a/crates/flake8_to_ruff/Cargo.toml +++ b/crates/flake8_to_ruff/Cargo.toml @@ -1,8 +1,16 @@ [package] name = "flake8-to-ruff" -version = "0.0.272" +version = "0.0.278" +description = """ +Convert Flake8 configuration files to Ruff configuration files. +""" +authors = { workspace = true } edition = { workspace = true } rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } [dependencies] ruff = { path = "../ruff", default-features = false } diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml index f32364904b..b659730d8a 100644 --- a/crates/ruff/Cargo.toml +++ b/crates/ruff/Cargo.toml @@ -1,14 +1,15 @@ [package] name = "ruff" -version = "0.0.272" -authors.workspace = true -edition.workspace = true -rust-version.workspace = true -documentation.workspace = true -homepage.workspace = true -repository.workspace = true +version = "0.0.278" +publish = false +authors = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } readme = "README.md" -license = "MIT" [lib] name = "ruff" @@ -16,6 +17,7 @@ name = "ruff" [dependencies] ruff_cache = { path = "../ruff_cache" } ruff_diagnostics = { path = "../ruff_diagnostics", features = ["serde"] } +ruff_index = { path = "../ruff_index" } ruff_macros = { path = "../ruff_macros" } ruff_python_whitespace = { path = "../ruff_python_whitespace" } ruff_python_ast = { path = "../ruff_python_ast", features = ["serde"] } @@ -41,6 +43,7 @@ is-macro = { workspace = true } itertools = { workspace = true } libcst = { workspace = true } log = { workspace = true } +memchr = { workspace = true } natord = { version = "1.0.9" } nohash-hasher = { workspace = true } num-bigint = { workspace = true } @@ -64,16 +67,18 @@ schemars = { workspace = true, optional = true } semver = { version = "1.0.16" } serde = { workspace = true } serde_json = { workspace = true } -similar = { workspace = true, features = ["inline"] } +serde_with = { version = "3.0.0" } +similar = { workspace = true } shellexpand = { workspace = true } smallvec = { workspace = true } strum = { workspace = true } strum_macros = { workspace = true } -thiserror = { version = "1.0.38" } +thiserror = { version = "1.0.43" } toml = { workspace = true } typed-arena = { version = "2.0.2" } unicode-width = { version = "0.1.10" } unicode_names2 = { version = "0.6.0", git = "https://github.com/youknowone/unicode_names2.git", rev = "4ce16aa85cbcdd9cc830410f1a72ef9a235f2fde" } +wsl = { version = "0.1.0" } [dev-dependencies] insta = { workspace = true } @@ -85,4 +90,5 @@ colored = { workspace = true, features = ["no-color"] } [features] default = [] schemars = ["dep:schemars"] -jupyter_notebook = [] +# Enables the UnreachableCode rule +unreachable-code = [] diff --git a/crates/ruff/resources/test/fixtures/control-flow-graph/assert.py b/crates/ruff/resources/test/fixtures/control-flow-graph/assert.py new file mode 100644 index 0000000000..bfb3ab9030 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/control-flow-graph/assert.py @@ -0,0 +1,11 @@ +def func(): + assert True + +def func(): + assert False + +def func(): + assert True, "oops" + +def func(): + assert False, "oops" diff --git a/crates/ruff/resources/test/fixtures/control-flow-graph/async-for.py b/crates/ruff/resources/test/fixtures/control-flow-graph/async-for.py new file mode 100644 index 0000000000..a1dc86a6e9 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/control-flow-graph/async-for.py @@ -0,0 +1,41 @@ +def func(): + async for i in range(5): + print(i) + +def func(): + async for i in range(20): + print(i) + else: + return 0 + +def func(): + async for i in range(10): + if i == 5: + return 1 + return 0 + +def func(): + async for i in range(111): + if i == 5: + return 1 + else: + return 0 + return 2 + +def func(): + async for i in range(12): + continue + +def func(): + async for i in range(1110): + if True: + continue + +def func(): + async for i in range(13): + break + +def func(): + async for i in range(1110): + if True: + break diff --git a/crates/ruff/resources/test/fixtures/control-flow-graph/for.py b/crates/ruff/resources/test/fixtures/control-flow-graph/for.py new file mode 100644 index 0000000000..a5807a635a --- /dev/null +++ b/crates/ruff/resources/test/fixtures/control-flow-graph/for.py @@ -0,0 +1,41 @@ +def func(): + for i in range(5): + print(i) + +def func(): + for i in range(20): + print(i) + else: + return 0 + +def func(): + for i in range(10): + if i == 5: + return 1 + return 0 + +def func(): + for i in range(111): + if i == 5: + return 1 + else: + return 0 + return 2 + +def func(): + for i in range(12): + continue + +def func(): + for i in range(1110): + if True: + continue + +def func(): + for i in range(13): + break + +def func(): + for i in range(1110): + if True: + break diff --git a/crates/ruff/resources/test/fixtures/control-flow-graph/if.py b/crates/ruff/resources/test/fixtures/control-flow-graph/if.py new file mode 100644 index 0000000000..2b5fa42099 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/control-flow-graph/if.py @@ -0,0 +1,108 @@ +def func(): + if False: + return 0 + return 1 + +def func(): + if True: + return 1 + return 0 + +def func(): + if False: + return 0 + else: + return 1 + +def func(): + if True: + return 1 + else: + return 0 + +def func(): + if False: + return 0 + else: + return 1 + return "unreachable" + +def func(): + if True: + return 1 + else: + return 0 + return "unreachable" + +def func(): + if True: + if True: + return 1 + return 2 + else: + return 3 + return "unreachable2" + +def func(): + if False: + return 0 + +def func(): + if True: + return 1 + +def func(): + if True: + return 1 + elif False: + return 2 + else: + return 0 + +def func(): + if False: + return 1 + elif True: + return 2 + else: + return 0 + +def func(): + if True: + if False: + return 0 + elif True: + return 1 + else: + return 2 + return 3 + elif True: + return 4 + else: + return 5 + return 6 + +def func(): + if False: + return "unreached" + elif False: + return "also unreached" + return "reached" + +# Test case found in the Bokeh repository that trigger a false positive. +def func(self, obj: BytesRep) -> bytes: + data = obj["data"] + + if isinstance(data, str): + return base64.b64decode(data) + elif isinstance(data, Buffer): + buffer = data + else: + id = data["id"] + + if id in self._buffers: + buffer = self._buffers[id] + else: + self.error(f"can't resolve buffer '{id}'") + + return buffer.data diff --git a/crates/ruff/resources/test/fixtures/control-flow-graph/match.py b/crates/ruff/resources/test/fixtures/control-flow-graph/match.py new file mode 100644 index 0000000000..cce019e308 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/control-flow-graph/match.py @@ -0,0 +1,131 @@ +def func(status): + match status: + case _: + return 0 + return "unreachable" + +def func(status): + match status: + case 1: + return 1 + return 0 + +def func(status): + match status: + case 1: + return 1 + case _: + return 0 + +def func(status): + match status: + case 1 | 2 | 3: + return 5 + return 6 + +def func(status): + match status: + case 1 | 2 | 3: + return 5 + case _: + return 10 + return 0 + +def func(status): + match status: + case 0: + return 0 + case 1: + return 1 + case 1: + return "1 again" + case _: + return 3 + +def func(status): + i = 0 + match status, i: + case _, _: + return 0 + +def func(status): + i = 0 + match status, i: + case _, 0: + return 0 + case _, 2: + return 0 + +def func(point): + match point: + case (0, 0): + print("Origin") + case _: + raise ValueError("oops") + +def func(point): + match point: + case (0, 0): + print("Origin") + case (0, y): + print(f"Y={y}") + case (x, 0): + print(f"X={x}") + case (x, y): + print(f"X={x}, Y={y}") + case _: + raise ValueError("Not a point") + +def where_is(point): + class Point: + x: int + y: int + + match point: + case Point(x=0, y=0): + print("Origin") + case Point(x=0, y=y): + print(f"Y={y}") + case Point(x=x, y=0): + print(f"X={x}") + case Point(): + print("Somewhere else") + case _: + print("Not a point") + +def func(points): + match points: + case []: + print("No points") + case [Point(0, 0)]: + print("The origin") + case [Point(x, y)]: + print(f"Single point {x}, {y}") + case [Point(0, y1), Point(0, y2)]: + print(f"Two on the Y axis at {y1}, {y2}") + case _: + print("Something else") + +def func(point): + match point: + case Point(x, y) if x == y: + print(f"Y=X at {x}") + case Point(x, y): + print(f"Not on the diagonal") + +def func(): + from enum import Enum + class Color(Enum): + RED = 'red' + GREEN = 'green' + BLUE = 'blue' + + color = Color(input("Enter your choice of 'red', 'blue' or 'green': ")) + + match color: + case Color.RED: + print("I see red!") + case Color.GREEN: + print("Grass is green") + case Color.BLUE: + print("I'm feeling the blues :(") diff --git a/crates/ruff/resources/test/fixtures/control-flow-graph/raise.py b/crates/ruff/resources/test/fixtures/control-flow-graph/raise.py new file mode 100644 index 0000000000..37aadc61a0 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/control-flow-graph/raise.py @@ -0,0 +1,5 @@ +def func(): + raise Exception + +def func(): + raise "a glass!" diff --git a/crates/ruff/resources/test/fixtures/control-flow-graph/simple.py b/crates/ruff/resources/test/fixtures/control-flow-graph/simple.py new file mode 100644 index 0000000000..d1f710149b --- /dev/null +++ b/crates/ruff/resources/test/fixtures/control-flow-graph/simple.py @@ -0,0 +1,23 @@ +def func(): + pass + +def func(): + pass + +def func(): + return + +def func(): + return 1 + +def func(): + return 1 + return "unreachable" + +def func(): + i = 0 + +def func(): + i = 0 + i += 2 + return i diff --git a/crates/ruff/resources/test/fixtures/control-flow-graph/try.py b/crates/ruff/resources/test/fixtures/control-flow-graph/try.py new file mode 100644 index 0000000000..e9f109dfd7 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/control-flow-graph/try.py @@ -0,0 +1,41 @@ +def func(): + try: + ... + except Exception: + ... + except OtherException as e: + ... + else: + ... + finally: + ... + +def func(): + try: + ... + except Exception: + ... + +def func(): + try: + ... + except Exception: + ... + except OtherException as e: + ... + +def func(): + try: + ... + except Exception: + ... + except OtherException as e: + ... + else: + ... + +def func(): + try: + ... + finally: + ... diff --git a/crates/ruff/resources/test/fixtures/control-flow-graph/while.py b/crates/ruff/resources/test/fixtures/control-flow-graph/while.py new file mode 100644 index 0000000000..6a4174358b --- /dev/null +++ b/crates/ruff/resources/test/fixtures/control-flow-graph/while.py @@ -0,0 +1,121 @@ +def func(): + while False: + return "unreachable" + return 1 + +def func(): + while False: + return "unreachable" + else: + return 1 + +def func(): + while False: + return "unreachable" + else: + return 1 + return "also unreachable" + +def func(): + while True: + return 1 + return "unreachable" + +def func(): + while True: + return 1 + else: + return "unreachable" + +def func(): + while True: + return 1 + else: + return "unreachable" + return "also unreachable" + +def func(): + i = 0 + while False: + i += 1 + return i + +def func(): + i = 0 + while True: + i += 1 + return i + +def func(): + while True: + pass + return 1 + +def func(): + i = 0 + while True: + if True: + print("ok") + i += 1 + return i + +def func(): + i = 0 + while True: + if False: + print("ok") + i += 1 + return i + +def func(): + while True: + if True: + return 1 + return 0 + +def func(): + while True: + continue + +def func(): + while False: + continue + +def func(): + while True: + break + +def func(): + while False: + break + +def func(): + while True: + if True: + continue + +def func(): + while True: + if True: + break + +''' +TODO: because `try` statements aren't handled this triggers a false positive as +the last statement is reached, but the rules thinks it isn't (it doesn't +see/process the break statement). + +# Test case found in the Bokeh repository that trigger a false positive. +def bokeh2(self, host: str = DEFAULT_HOST, port: int = DEFAULT_PORT) -> None: + self.stop_serving = False + while True: + try: + self.server = HTTPServer((host, port), HtmlOnlyHandler) + self.host = host + self.port = port + break + except OSError: + log.debug(f"port {port} is in use, trying to next one") + port += 1 + + self.thread = threading.Thread(target=self._run_web_server) +''' diff --git a/crates/ruff/resources/test/fixtures/flake8_annotations/annotation_presence.py b/crates/ruff/resources/test/fixtures/flake8_annotations/annotation_presence.py index d37f178bbb..7778c85072 100644 --- a/crates/ruff/resources/test/fixtures/flake8_annotations/annotation_presence.py +++ b/crates/ruff/resources/test/fixtures/flake8_annotations/annotation_presence.py @@ -1,4 +1,4 @@ -from typing import Any, Type +from typing import Annotated, Any, Optional, Type, Union from typing_extensions import override # Error @@ -95,27 +95,27 @@ class Foo: def foo(self: "Foo", a: int, *params: str, **options: Any) -> int: pass - # ANN401 + # OK @override def foo(self: "Foo", a: Any, *params: str, **options: str) -> int: pass - # ANN401 + # OK @override def foo(self: "Foo", a: int, *params: str, **options: str) -> Any: pass - # ANN401 + # OK @override def foo(self: "Foo", a: int, *params: Any, **options: Any) -> int: pass - # ANN401 + # OK @override def foo(self: "Foo", a: int, *params: Any, **options: str) -> int: pass - # ANN401 + # OK @override def foo(self: "Foo", a: int, *params: str, **options: Any) -> int: pass @@ -137,3 +137,17 @@ class Foo: # OK def f(*args: *tuple[int]) -> None: ... +def f(a: object) -> None: ... +def f(a: str | bytes) -> None: ... +def f(a: Union[str, bytes]) -> None: ... +def f(a: Optional[str]) -> None: ... +def f(a: Annotated[str, ...]) -> None: ... +def f(a: "Union[str, bytes]") -> None: ... + +# ANN401 +def f(a: Any | int) -> None: ... +def f(a: int | Any) -> None: ... +def f(a: Union[str, bytes, Any]) -> None: ... +def f(a: Optional[Any]) -> None: ... +def f(a: Annotated[Any, ...]) -> None: ... +def f(a: "Union[str, bytes, Any]") -> None: ... diff --git a/crates/ruff/resources/test/fixtures/flake8_bandit/S307.py b/crates/ruff/resources/test/fixtures/flake8_bandit/S307.py new file mode 100644 index 0000000000..06bccc084a --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_bandit/S307.py @@ -0,0 +1,12 @@ +import os + +print(eval("1+1")) # S307 +print(eval("os.getcwd()")) # S307 + + +class Class(object): + def eval(self): + print("hi") + + def foo(self): + self.eval() # OK diff --git a/crates/ruff/resources/test/fixtures/flake8_bugbear/B017.py b/crates/ruff/resources/test/fixtures/flake8_bugbear/B017.py index 43f1b986e9..917a848ba1 100644 --- a/crates/ruff/resources/test/fixtures/flake8_bugbear/B017.py +++ b/crates/ruff/resources/test/fixtures/flake8_bugbear/B017.py @@ -23,6 +23,10 @@ class Foobar(unittest.TestCase): with self.assertRaises(Exception): raise Exception("Evil I say!") + def also_evil_raises(self) -> None: + with self.assertRaises(BaseException): + raise Exception("Evil I say!") + def context_manager_raises(self) -> None: with self.assertRaises(Exception) as ex: raise Exception("Context manager is good") @@ -41,6 +45,9 @@ def test_pytest_raises(): with pytest.raises(Exception): raise ValueError("Hello") + with pytest.raises(Exception), pytest.raises(ValueError): + raise ValueError("Hello") + with pytest.raises(Exception, "hello"): raise ValueError("This is fine") diff --git a/crates/ruff/resources/test/fixtures/flake8_bugbear/B034.py b/crates/ruff/resources/test/fixtures/flake8_bugbear/B034.py new file mode 100644 index 0000000000..1236259c6b --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_bugbear/B034.py @@ -0,0 +1,27 @@ +import re +from re import sub + +# B034 +re.sub("a", "b", "aaa", re.IGNORECASE) +re.sub("a", "b", "aaa", 5) +re.sub("a", "b", "aaa", 5, re.IGNORECASE) +re.subn("a", "b", "aaa", re.IGNORECASE) +re.subn("a", "b", "aaa", 5) +re.subn("a", "b", "aaa", 5, re.IGNORECASE) +re.split(" ", "a a a a", re.I) +re.split(" ", "a a a a", 2) +re.split(" ", "a a a a", 2, re.I) +sub("a", "b", "aaa", re.IGNORECASE) + +# OK +re.sub("a", "b", "aaa") +re.sub("a", "b", "aaa", flags=re.IGNORECASE) +re.sub("a", "b", "aaa", count=5) +re.sub("a", "b", "aaa", count=5, flags=re.IGNORECASE) +re.subn("a", "b", "aaa") +re.subn("a", "b", "aaa", flags=re.IGNORECASE) +re.subn("a", "b", "aaa", count=5) +re.subn("a", "b", "aaa", count=5, flags=re.IGNORECASE) +re.split(" ", "a a a a", flags=re.I) +re.split(" ", "a a a a", maxsplit=2) +re.split(" ", "a a a a", maxsplit=2, flags=re.I) diff --git a/crates/ruff/resources/test/fixtures/flake8_builtins/A003.py b/crates/ruff/resources/test/fixtures/flake8_builtins/A003.py index 0337a34e95..b971974ef2 100644 --- a/crates/ruff/resources/test/fixtures/flake8_builtins/A003.py +++ b/crates/ruff/resources/test/fixtures/flake8_builtins/A003.py @@ -1,6 +1,6 @@ class MyClass: ImportError = 4 - id = 5 + id: int dir = "/" def __init__(self): @@ -10,3 +10,10 @@ class MyClass: def str(self): pass + + +from typing import TypedDict + + +class MyClass(TypedDict): + id: int diff --git a/crates/ruff/resources/test/fixtures/flake8_comprehensions/C417.py b/crates/ruff/resources/test/fixtures/flake8_comprehensions/C417.py index e0353b49c3..7077acfad6 100644 --- a/crates/ruff/resources/test/fixtures/flake8_comprehensions/C417.py +++ b/crates/ruff/resources/test/fixtures/flake8_comprehensions/C417.py @@ -25,10 +25,15 @@ map(lambda x=2, y=1: x + y, nums, nums) set(map(lambda x, y: x, nums, nums)) -def myfunc(arg1: int, arg2: int = 4): +def func(arg1: int, arg2: int = 4): return 2 * arg1 + arg2 -list(map(myfunc, nums)) +# Non-error: `func` is not a lambda. +list(map(func, nums)) -[x for x in nums] +# False positive: need to preserve the late-binding of `x` in the inner lambda. +map(lambda x: lambda: x, range(4)) + +# Error: the `x` is overridden by the inner lambda. +map(lambda x: lambda x: x, range(4)) diff --git a/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ001.py b/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ001.py index 29533cba75..0974f807ef 100644 --- a/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ001.py +++ b/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ001.py @@ -19,3 +19,6 @@ from datetime import datetime # no args unqualified datetime(2000, 1, 1, 0, 0, 0) + +# uses `astimezone` method +datetime(2000, 1, 1, 0, 0, 0).astimezone() diff --git a/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ002.py b/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ002.py index 7f6231deff..9d124091bb 100644 --- a/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ002.py +++ b/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ002.py @@ -7,3 +7,6 @@ from datetime import datetime # unqualified datetime.today() + +# uses `astimezone` method +datetime.today().astimezone() diff --git a/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ003.py b/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ003.py index 4c463802e7..646bfa32b3 100644 --- a/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ003.py +++ b/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ003.py @@ -7,3 +7,6 @@ from datetime import datetime # unqualified datetime.utcnow() + +# uses `astimezone` method +datetime.utcnow().astimezone() diff --git a/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ004.py b/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ004.py index b4b535f57f..ae1658a688 100644 --- a/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ004.py +++ b/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ004.py @@ -7,3 +7,6 @@ from datetime import datetime # unqualified datetime.utcfromtimestamp(1234) + +# uses `astimezone` method +datetime.utcfromtimestamp(1234).astimezone() diff --git a/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ005.py b/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ005.py index 41f1a72a38..5d6f8596b6 100644 --- a/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ005.py +++ b/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ005.py @@ -16,3 +16,6 @@ from datetime import datetime # no args unqualified datetime.now() + +# uses `astimezone` method +datetime.now().astimezone() diff --git a/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ006.py b/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ006.py index b6f80613e2..5e9b217611 100644 --- a/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ006.py +++ b/crates/ruff/resources/test/fixtures/flake8_datetimez/DTZ006.py @@ -16,3 +16,6 @@ from datetime import datetime # no args unqualified datetime.fromtimestamp(1234) + +# uses `astimezone` method +datetime.fromtimestamp(1234).astimezone() diff --git a/crates/ruff/resources/test/fixtures/flake8_django/DJ012.py b/crates/ruff/resources/test/fixtures/flake8_django/DJ012.py index fc0ffd0cbb..a0f8d9da22 100644 --- a/crates/ruff/resources/test/fixtures/flake8_django/DJ012.py +++ b/crates/ruff/resources/test/fixtures/flake8_django/DJ012.py @@ -111,3 +111,19 @@ class PerfectlyFine(models.Model): @property def random_property(self): return "%s" % self + + +class MultipleConsecutiveFields(models.Model): + """Model that contains multiple out-of-order field definitions in a row.""" + + + class Meta: + verbose_name = "test" + + first_name = models.CharField(max_length=32) + last_name = models.CharField(max_length=32) + + def get_absolute_url(self): + pass + + middle_name = models.CharField(max_length=32) diff --git a/crates/ruff/resources/test/fixtures/flake8_implicit_str_concat/ISC.py b/crates/ruff/resources/test/fixtures/flake8_implicit_str_concat/ISC.py index a88e8e5573..633e075731 100644 --- a/crates/ruff/resources/test/fixtures/flake8_implicit_str_concat/ISC.py +++ b/crates/ruff/resources/test/fixtures/flake8_implicit_str_concat/ISC.py @@ -34,3 +34,19 @@ _ = ( b"abc" b"def" ) + +_ = """a""" """b""" + +_ = """a +b""" """c +d""" + +_ = f"""a""" f"""b""" + +_ = f"a" "b" + +_ = """a""" "b" + +_ = 'a' "b" + +_ = rf"a" rf"b" diff --git a/crates/ruff/resources/test/fixtures/flake8_import_conventions/defaults.py b/crates/ruff/resources/test/fixtures/flake8_import_conventions/defaults.py index 277b6ca10b..bbaf23ea44 100644 --- a/crates/ruff/resources/test/fixtures/flake8_import_conventions/defaults.py +++ b/crates/ruff/resources/test/fixtures/flake8_import_conventions/defaults.py @@ -5,15 +5,18 @@ import matplotlib.pyplot # unconventional import numpy # unconventional import pandas # unconventional import seaborn # unconventional +import tkinter # unconventional import altair as altr # unconventional import matplotlib.pyplot as plot # unconventional import numpy as nmp # unconventional import pandas as pdas # unconventional import seaborn as sbrn # unconventional +import tkinter as tkr # unconventional import altair as alt # conventional import matplotlib.pyplot as plt # conventional import numpy as np # conventional import pandas as pd # conventional import seaborn as sns # conventional +import tkinter as tk # conventional diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI002.py b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI002.py new file mode 100644 index 0000000000..50cf7c884f --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI002.py @@ -0,0 +1,6 @@ +import sys + +if sys.version == 'Python 2.7.10': ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info +if 'linux' == sys.platform: ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info +if hasattr(sys, 'maxint'): ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info +if sys.maxsize == 42: ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI002.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI002.pyi new file mode 100644 index 0000000000..50cf7c884f --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI002.pyi @@ -0,0 +1,6 @@ +import sys + +if sys.version == 'Python 2.7.10': ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info +if 'linux' == sys.platform: ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info +if hasattr(sys, 'maxint'): ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info +if sys.maxsize == 42: ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI003.py b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI003.py new file mode 100644 index 0000000000..9c4481179f --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI003.py @@ -0,0 +1,31 @@ +import sys + +if sys.version_info[0] == 2: ... +if sys.version_info[0] == True: ... # Y003 Unrecognized sys.version_info check # E712 comparison to True should be 'if cond is True:' or 'if cond:' +if sys.version_info[0.0] == 2: ... # Y003 Unrecognized sys.version_info check +if sys.version_info[False] == 2: ... # Y003 Unrecognized sys.version_info check +if sys.version_info[0j] == 2: ... # Y003 Unrecognized sys.version_info check +if sys.version_info[0] == (2, 7): ... # Y003 Unrecognized sys.version_info check +if sys.version_info[0] == '2': ... # Y003 Unrecognized sys.version_info check +if sys.version_info[1:] >= (7, 11): ... # Y003 Unrecognized sys.version_info check +if sys.version_info[::-1] < (11, 7): ... # Y003 Unrecognized sys.version_info check +if sys.version_info[:3] >= (2, 7): ... # Y003 Unrecognized sys.version_info check +if sys.version_info[:True] >= (2, 7): ... # Y003 Unrecognized sys.version_info check +if sys.version_info[:1] == (2,): ... +if sys.version_info[:1] == (True,): ... # Y003 Unrecognized sys.version_info check +if sys.version_info[:1] == (2, 7): ... # Y005 Version comparison must be against a length-1 tuple +if sys.version_info[:2] == (2, 7): ... +if sys.version_info[:2] == (2,): ... # Y005 Version comparison must be against a length-2 tuple +if sys.version_info[:2] == "lol": ... # Y003 Unrecognized sys.version_info check +if sys.version_info[:2.0] >= (3, 9): ... # Y003 Unrecognized sys.version_info check +if sys.version_info[:2j] >= (3, 9): ... # Y003 Unrecognized sys.version_info check +if sys.version_info[:, :] >= (2, 7): ... # Y003 Unrecognized sys.version_info check +if sys.version_info < [3, 0]: ... # Y003 Unrecognized sys.version_info check +if sys.version_info < ('3', '0'): ... # Y003 Unrecognized sys.version_info check +if sys.version_info >= (3, 4, 3): ... # Y004 Version comparison must use only major and minor version +if sys.version_info == (3, 4): ... # Y006 Use only < and >= for version comparisons +if sys.version_info > (3, 0): ... # Y006 Use only < and >= for version comparisons +if sys.version_info <= (3, 0): ... # Y006 Use only < and >= for version comparisons +if sys.version_info < (3, 5): ... +if sys.version_info >= (3, 5): ... +if (2, 7) <= sys.version_info < (3, 5): ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI003.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI003.pyi new file mode 100644 index 0000000000..9c4481179f --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI003.pyi @@ -0,0 +1,31 @@ +import sys + +if sys.version_info[0] == 2: ... +if sys.version_info[0] == True: ... # Y003 Unrecognized sys.version_info check # E712 comparison to True should be 'if cond is True:' or 'if cond:' +if sys.version_info[0.0] == 2: ... # Y003 Unrecognized sys.version_info check +if sys.version_info[False] == 2: ... # Y003 Unrecognized sys.version_info check +if sys.version_info[0j] == 2: ... # Y003 Unrecognized sys.version_info check +if sys.version_info[0] == (2, 7): ... # Y003 Unrecognized sys.version_info check +if sys.version_info[0] == '2': ... # Y003 Unrecognized sys.version_info check +if sys.version_info[1:] >= (7, 11): ... # Y003 Unrecognized sys.version_info check +if sys.version_info[::-1] < (11, 7): ... # Y003 Unrecognized sys.version_info check +if sys.version_info[:3] >= (2, 7): ... # Y003 Unrecognized sys.version_info check +if sys.version_info[:True] >= (2, 7): ... # Y003 Unrecognized sys.version_info check +if sys.version_info[:1] == (2,): ... +if sys.version_info[:1] == (True,): ... # Y003 Unrecognized sys.version_info check +if sys.version_info[:1] == (2, 7): ... # Y005 Version comparison must be against a length-1 tuple +if sys.version_info[:2] == (2, 7): ... +if sys.version_info[:2] == (2,): ... # Y005 Version comparison must be against a length-2 tuple +if sys.version_info[:2] == "lol": ... # Y003 Unrecognized sys.version_info check +if sys.version_info[:2.0] >= (3, 9): ... # Y003 Unrecognized sys.version_info check +if sys.version_info[:2j] >= (3, 9): ... # Y003 Unrecognized sys.version_info check +if sys.version_info[:, :] >= (2, 7): ... # Y003 Unrecognized sys.version_info check +if sys.version_info < [3, 0]: ... # Y003 Unrecognized sys.version_info check +if sys.version_info < ('3', '0'): ... # Y003 Unrecognized sys.version_info check +if sys.version_info >= (3, 4, 3): ... # Y004 Version comparison must use only major and minor version +if sys.version_info == (3, 4): ... # Y006 Use only < and >= for version comparisons +if sys.version_info > (3, 0): ... # Y006 Use only < and >= for version comparisons +if sys.version_info <= (3, 0): ... # Y006 Use only < and >= for version comparisons +if sys.version_info < (3, 5): ... +if sys.version_info >= (3, 5): ... +if (2, 7) <= sys.version_info < (3, 5): ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI004.py b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI004.py new file mode 100644 index 0000000000..bca3f9f7e3 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI004.py @@ -0,0 +1,15 @@ +import sys +from sys import version_info + +if sys.version_info >= (3, 4, 3): ... # PYI004 +if sys.version_info < (3, 4, 3): ... # PYI004 +if sys.version_info == (3, 4, 3): ... # PYI004 +if sys.version_info != (3, 4, 3): ... # PYI004 + +if sys.version_info[0] == 2: ... +if version_info[0] == 2: ... +if sys.version_info < (3, 5): ... +if version_info >= (3, 5): ... +if sys.version_info[:2] == (2, 7): ... +if sys.version_info[:1] == (2,): ... +if sys.platform == 'linux': ... diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI004.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI004.pyi new file mode 100644 index 0000000000..bca3f9f7e3 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI004.pyi @@ -0,0 +1,15 @@ +import sys +from sys import version_info + +if sys.version_info >= (3, 4, 3): ... # PYI004 +if sys.version_info < (3, 4, 3): ... # PYI004 +if sys.version_info == (3, 4, 3): ... # PYI004 +if sys.version_info != (3, 4, 3): ... # PYI004 + +if sys.version_info[0] == 2: ... +if version_info[0] == 2: ... +if sys.version_info < (3, 5): ... +if version_info >= (3, 5): ... +if sys.version_info[:2] == (2, 7): ... +if sys.version_info[:1] == (2,): ... +if sys.platform == 'linux': ... diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI005.py b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI005.py new file mode 100644 index 0000000000..0053e9e9df --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI005.py @@ -0,0 +1,14 @@ +import sys +from sys import platform, version_info + +if sys.version_info[:1] == (2, 7): ... # Y005 +if sys.version_info[:2] == (2,): ... # Y005 + + +if sys.version_info[0] == 2: ... +if version_info[0] == 2: ... +if sys.version_info < (3, 5): ... +if version_info >= (3, 5): ... +if sys.version_info[:2] == (2, 7): ... +if sys.version_info[:1] == (2,): ... +if platform == 'linux': ... diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI005.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI005.pyi new file mode 100644 index 0000000000..0053e9e9df --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI005.pyi @@ -0,0 +1,14 @@ +import sys +from sys import platform, version_info + +if sys.version_info[:1] == (2, 7): ... # Y005 +if sys.version_info[:2] == (2,): ... # Y005 + + +if sys.version_info[0] == 2: ... +if version_info[0] == 2: ... +if sys.version_info < (3, 5): ... +if version_info >= (3, 5): ... +if sys.version_info[:2] == (2, 7): ... +if sys.version_info[:1] == (2,): ... +if platform == 'linux': ... diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI015.py b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI015.py index 37a4f4d867..16ca34e318 100644 --- a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI015.py +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI015.py @@ -91,3 +91,4 @@ field27 = list[str] field28 = builtins.str field29 = str field30 = str | bytes | None +field31: typing.Final = field30 diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI015.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI015.pyi index 860ee255fb..10f7a70770 100644 --- a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI015.pyi +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI015.pyi @@ -98,3 +98,4 @@ field27 = list[str] field28 = builtins.str field29 = str field30 = str | bytes | None +field31: typing.Final = field30 diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI016.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI016.pyi index c1b2d7711c..fd19cf669e 100644 --- a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI016.pyi +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI016.pyi @@ -1,3 +1,5 @@ +import typing + # Shouldn't affect non-union field types. field1: str @@ -30,3 +32,42 @@ field10: (str | int) | str # PYI016: Duplicate union member `str` # Should emit for nested unions. field11: dict[int | int, str] + +# Should emit for unions with more than two cases +field12: int | int | int # Error +field13: int | int | int | int # Error + +# Should emit for unions with more than two cases, even if not directly adjacent +field14: int | int | str | int # Error + +# Should emit for duplicate literal types; also covered by PYI030 +field15: typing.Literal[1] | typing.Literal[1] # Error + +# Shouldn't emit if in new parent type +field16: int | dict[int, str] # OK + +# Shouldn't emit if not in a union parent +field17: dict[int, int] # OK + +# Should emit in cases with newlines +field18: typing.Union[ + set[ + int # foo + ], + set[ + int # bar + ], +] # Error, newline and comment will not be emitted in message + + +# Should emit in cases with `typing.Union` instead of `|` +field19: typing.Union[int, int] # Error + +# Should emit in cases with nested `typing.Union` +field20: typing.Union[int, typing.Union[int, str]] # Error + +# Should emit in cases with mixed `typing.Union` and `|` +field21: typing.Union[int, int | str] # Error + +# Should emit only once in cases with multiple nested `typing.Union` +field22: typing.Union[int, typing.Union[int, typing.Union[int, int]]] # Error diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI025.py b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI025.py index 5c8b8fa26b..e500a3f4a9 100644 --- a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI025.py +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI025.py @@ -1,19 +1,19 @@ -from collections.abc import Set as AbstractSet # Ok +def f(): + from collections.abc import Set as AbstractSet # Ok -from collections.abc import Set # Ok +def f(): + from collections.abc import Container, Sized, Set as AbstractSet, ValuesView # Ok -from collections.abc import ( - Container, - Sized, - Set, # Ok - ValuesView -) +def f(): + from collections.abc import Set # PYI025 -from collections.abc import ( - Container, - Sized, - Set as AbstractSet, # Ok - ValuesView -) + +def f(): + from collections.abc import Container, Sized, Set, ValuesView # PYI025 + + GLOBAL: Set[int] = set() + + class Class: + member: Set[int] diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI025.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI025.pyi index c12ccdffb5..26a2a69ce9 100644 --- a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI025.pyi +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI025.pyi @@ -1,19 +1,50 @@ -from collections.abc import Set as AbstractSet # Ok +def f(): + from collections.abc import Set as AbstractSet # Ok +def f(): + from collections.abc import Container, Sized, Set as AbstractSet, ValuesView # Ok -from collections.abc import Set # PYI025 +def f(): + from collections.abc import Set # PYI025 +def f(): + from collections.abc import Container, Sized, Set, ValuesView # PYI025 -from collections.abc import ( - Container, - Sized, - Set, # PYI025 - ValuesView -) +def f(): + """Test: local symbol renaming.""" + if True: + from collections.abc import Set + else: + Set = 1 -from collections.abc import ( - Container, - Sized, - Set as AbstractSet, - ValuesView # Ok -) + x: Set = set() + + x: Set + + del Set + + def f(): + print(Set) + + def Set(): + pass + print(Set) + +from collections.abc import Set + +def f(): + """Test: global symbol renaming.""" + global Set + + Set = 1 + print(Set) + +def f(): + """Test: nonlocal symbol renaming.""" + from collections.abc import Set + + def g(): + nonlocal Set + + Set = 1 + print(Set) diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI030.py b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI030.py new file mode 100644 index 0000000000..cc199f1480 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI030.py @@ -0,0 +1,24 @@ +from typing import Literal +# Shouldn't emit for any cases in the non-stub file for compatibility with flake8-pyi. +# Note that this rule could be applied here in the future. + +field1: Literal[1] # OK +field2: Literal[1] | Literal[2] # OK + +def func1(arg1: Literal[1] | Literal[2]): # OK + print(arg1) + + +def func2() -> Literal[1] | Literal[2]: # OK + return "my Literal[1]ing" + + +field3: Literal[1] | Literal[2] | str # OK +field4: str | Literal[1] | Literal[2] # OK +field5: Literal[1] | str | Literal[2] # OK +field6: Literal[1] | bool | Literal[2] | str # OK +field7 = Literal[1] | Literal[2] # OK +field8: Literal[1] | (Literal[2] | str) # OK +field9: Literal[1] | (Literal[2] | str) # OK +field10: (Literal[1] | str) | Literal[2] # OK +field11: dict[Literal[1] | Literal[2], str] # OK diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI030.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI030.pyi new file mode 100644 index 0000000000..e92af925df --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI030.pyi @@ -0,0 +1,86 @@ +import typing +import typing_extensions +from typing import Literal + +# Shouldn't affect non-union field types. +field1: Literal[1] # OK + +# Should emit for duplicate field types. +field2: Literal[1] | Literal[2] # Error + +# Should emit for union types in arguments. +def func1(arg1: Literal[1] | Literal[2]): # Error + print(arg1) + + +# Should emit for unions in return types. +def func2() -> Literal[1] | Literal[2]: # Error + return "my Literal[1]ing" + + +# Should emit in longer unions, even if not directly adjacent. +field3: Literal[1] | Literal[2] | str # Error +field4: str | Literal[1] | Literal[2] # Error +field5: Literal[1] | str | Literal[2] # Error +field6: Literal[1] | bool | Literal[2] | str # Error + +# Should emit for non-type unions. +field7 = Literal[1] | Literal[2] # Error + +# Should emit for parenthesized unions. +field8: Literal[1] | (Literal[2] | str) # Error + +# Should handle user parentheses when fixing. +field9: Literal[1] | (Literal[2] | str) # Error +field10: (Literal[1] | str) | Literal[2] # Error + +# Should emit for union in generic parent type. +field11: dict[Literal[1] | Literal[2], str] # Error + +# Should emit for unions with more than two cases +field12: Literal[1] | Literal[2] | Literal[3] # Error +field13: Literal[1] | Literal[2] | Literal[3] | Literal[4] # Error + +# Should emit for unions with more than two cases, even if not directly adjacent +field14: Literal[1] | Literal[2] | str | Literal[3] # Error + +# Should emit for unions with mixed literal internal types +field15: Literal[1] | Literal["foo"] | Literal[True] # Error + +# Shouldn't emit for duplicate field types with same value; covered by Y016 +field16: Literal[1] | Literal[1] # OK + +# Shouldn't emit if in new parent type +field17: Literal[1] | dict[Literal[2], str] # OK + +# Shouldn't emit if not in a union parent +field18: dict[Literal[1], Literal[2]] # OK + +# Should respect name of literal type used +field19: typing.Literal[1] | typing.Literal[2] # Error + +# Should emit in cases with newlines +field20: typing.Union[ + Literal[ + 1 # test + ], + Literal[2], +] # Error, newline and comment will not be emitted in message + +# Should handle multiple unions with multiple members +field21: Literal[1, 2] | Literal[3, 4] # Error + +# Should emit in cases with `typing.Union` instead of `|` +field22: typing.Union[Literal[1], Literal[2]] # Error + +# Should emit in cases with `typing_extensions.Literal` +field23: typing_extensions.Literal[1] | typing_extensions.Literal[2] # Error + +# Should emit in cases with nested `typing.Union` +field24: typing.Union[Literal[1], typing.Union[Literal[2], str]] # Error + +# Should emit in cases with mixed `typing.Union` and `|` +field25: typing.Union[Literal[1], Literal[2] | str] # Error + +# Should emit only once in cases with multiple nested `typing.Union` +field24: typing.Union[Literal[1], typing.Union[Literal[2], typing.Union[Literal[3], Literal[4]]]] # Error diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI036.py b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI036.py new file mode 100644 index 0000000000..57f71dc39e --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI036.py @@ -0,0 +1,75 @@ +import builtins +import types +import typing +from collections.abc import Awaitable +from types import TracebackType +from typing import Any, Type + +import _typeshed +import typing_extensions +from _typeshed import Unused + +class GoodOne: + def __exit__(self, *args: object) -> None: ... + async def __aexit__(self, *args) -> str: ... + +class GoodTwo: + def __exit__(self, typ: type[builtins.BaseException] | None, *args: builtins.object) -> bool | None: ... + async def __aexit__(self, /, typ: Type[BaseException] | None, *args: object, **kwargs) -> bool: ... + +class GoodThree: + def __exit__(self, __typ: typing.Type[BaseException] | None, exc: BaseException | None, *args: object) -> None: ... + async def __aexit__(self, typ: typing_extensions.Type[BaseException] | None, __exc: BaseException | None, *args: object) -> None: ... + +class GoodFour: + 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: types.TracebackType | None, *args: list[None]) -> None: ... + +class GoodFive: + def __exit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None, weird_extra_arg: int = ..., *args: int, **kwargs: str) -> None: ... + async def __aexit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None) -> Awaitable[None]: ... + +class GoodSix: + def __exit__(self, typ: object, exc: builtins.object, tb: object) -> None: ... + async def __aexit__(self, typ: object, exc: object, tb: builtins.object) -> None: ... + +class GoodSeven: + def __exit__(self, *args: Unused) -> bool: ... + async def __aexit__(self, typ: Type[BaseException] | None, *args: _typeshed.Unused) -> Awaitable[None]: ... + +class GoodEight: + def __exit__(self, __typ: typing.Type[BaseException] | None, exc: BaseException | None, *args: _typeshed.Unused) -> bool: ... + async def __aexit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None, weird_extra_arg: int = ..., *args: Unused, **kwargs: Unused) -> Awaitable[None]: ... + +class GoodNine: + def __exit__(self, __typ: typing.Union[typing.Type[BaseException] , None], exc: typing.Union[BaseException , None], *args: _typeshed.Unused) -> bool: ... + async def __aexit__(self, typ: typing.Union[typing.Type[BaseException], None], exc: typing.Union[BaseException , None], tb: typing.Union[TracebackType , None], weird_extra_arg: int = ..., *args: Unused, **kwargs: Unused) -> Awaitable[None]: ... + +class GoodTen: + def __exit__(self, __typ: typing.Optional[typing.Type[BaseException]], exc: typing.Optional[BaseException], *args: _typeshed.Unused) -> bool: ... + async def __aexit__(self, typ: typing.Optional[typing.Type[BaseException]], exc: typing.Optional[BaseException], tb: typing.Optional[TracebackType], weird_extra_arg: int = ..., *args: Unused, **kwargs: Unused) -> Awaitable[None]: ... + + +class BadOne: + def __exit__(self, *args: Any) -> None: ... # PYI036: Bad star-args annotation + async def __aexit__(self) -> None: ... # PYI036: Missing args + +class BadTwo: + def __exit__(self, typ, exc, tb, weird_extra_arg) -> None: ... # PYI036: Extra arg must have default + async def __aexit__(self, typ, exc, tb, *, weird_extra_arg) -> None: ...# PYI036: Extra arg must have default + +class BadThree: + def __exit__(self, typ: type[BaseException], exc: BaseException | None, tb: TracebackType | None) -> None: ... # PYI036: First arg has bad annotation + async def __aexit__(self, __typ: type[BaseException] | None, __exc: BaseException, __tb: TracebackType) -> bool | None: ... # PYI036: Second arg has bad annotation + +class BadFour: + def __exit__(self, typ: typing.Optional[type[BaseException]], exc: typing.Union[BaseException, None], tb: TracebackType) -> None: ... # PYI036: Third arg has bad annotation + async def __aexit__(self, __typ: type[BaseException] | None, __exc: BaseException | None, __tb: typing.Union[TracebackType, None, int]) -> bool | None: ... # PYI036: Third arg has bad annotation + +class BadFive: + def __exit__(self, typ: BaseException | None, *args: list[str]) -> bool: ... # PYI036: Bad star-args annotation + async def __aexit__(self, /, typ: type[BaseException] | None, *args: Any) -> Awaitable[None]: ... # PYI036: Bad star-args annotation + +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 diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI036.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI036.pyi new file mode 100644 index 0000000000..a49791aa1b --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI036.pyi @@ -0,0 +1,75 @@ +import builtins +import types +import typing +from collections.abc import Awaitable +from types import TracebackType +from typing import Any, Type + +import _typeshed +import typing_extensions +from _typeshed import Unused + +class GoodOne: + def __exit__(self, *args: object) -> None: ... + async def __aexit__(self, *args) -> str: ... + +class GoodTwo: + def __exit__(self, typ: type[builtins.BaseException] | None, *args: builtins.object) -> bool | None: ... + async def __aexit__(self, /, typ: Type[BaseException] | None, *args: object, **kwargs) -> bool: ... + +class GoodThree: + def __exit__(self, __typ: typing.Type[BaseException] | None, exc: BaseException | None, *args: object) -> None: ... + async def __aexit__(self, typ: typing_extensions.Type[BaseException] | None, __exc: BaseException | None, *args: object) -> None: ... + +class GoodFour: + 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: types.TracebackType | None, *args: list[None]) -> None: ... + +class GoodFive: + def __exit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None, weird_extra_arg: int = ..., *args: int, **kwargs: str) -> None: ... + async def __aexit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None) -> Awaitable[None]: ... + +class GoodSix: + def __exit__(self, typ: object, exc: builtins.object, tb: object) -> None: ... + async def __aexit__(self, typ: object, exc: object, tb: builtins.object) -> None: ... + +class GoodSeven: + def __exit__(self, *args: Unused) -> bool: ... + async def __aexit__(self, typ: Type[BaseException] | None, *args: _typeshed.Unused) -> Awaitable[None]: ... + +class GoodEight: + def __exit__(self, __typ: typing.Type[BaseException] | None, exc: BaseException | None, *args: _typeshed.Unused) -> bool: ... + async def __aexit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None, weird_extra_arg: int = ..., *args: Unused, **kwargs: Unused) -> Awaitable[None]: ... + +class GoodNine: + def __exit__(self, __typ: typing.Union[typing.Type[BaseException] , None], exc: typing.Union[BaseException , None], *args: _typeshed.Unused) -> bool: ... + async def __aexit__(self, typ: typing.Union[typing.Type[BaseException], None], exc: typing.Union[BaseException , None], tb: typing.Union[TracebackType , None], weird_extra_arg: int = ..., *args: Unused, **kwargs: Unused) -> Awaitable[None]: ... + +class GoodTen: + def __exit__(self, __typ: typing.Optional[typing.Type[BaseException]], exc: typing.Optional[BaseException], *args: _typeshed.Unused) -> bool: ... + async def __aexit__(self, typ: typing.Optional[typing.Type[BaseException]], exc: typing.Optional[BaseException], tb: typing.Optional[TracebackType], weird_extra_arg: int = ..., *args: Unused, **kwargs: Unused) -> Awaitable[None]: ... + + +class BadOne: + def __exit__(self, *args: Any) -> None: ... # PYI036: Bad star-args annotation + async def __aexit__(self) -> None: ... # PYI036: Missing args + +class BadTwo: + def __exit__(self, typ, exc, tb, weird_extra_arg) -> None: ... # PYI036: Extra arg must have default + async def __aexit__(self, typ, exc, tb, *, weird_extra_arg1, weird_extra_arg2) -> None: ...# PYI036: kwargs must have default + +class BadThree: + def __exit__(self, typ: type[BaseException], exc: BaseException | None, tb: TracebackType | None) -> None: ... # PYI036: First arg has bad annotation + async def __aexit__(self, __typ: type[BaseException] | None, __exc: BaseException, __tb: TracebackType) -> bool | None: ... # PYI036: Second arg has bad annotation + +class BadFour: + def __exit__(self, typ: typing.Optional[type[BaseException]], exc: typing.Union[BaseException, None], tb: TracebackType) -> None: ... # PYI036: Third arg has bad annotation + async def __aexit__(self, __typ: type[BaseException] | None, __exc: BaseException | None, __tb: typing.Union[TracebackType, None, int]) -> bool | None: ... # PYI036: Third arg has bad annotation + +class BadFive: + def __exit__(self, typ: BaseException | None, *args: list[str]) -> bool: ... # PYI036: Bad star-args annotation + async def __aexit__(self, /, typ: type[BaseException] | None, *args: Any) -> Awaitable[None]: ... # PYI036: Bad star-args annotation + +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 diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI044.py b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI044.py new file mode 100644 index 0000000000..6439df7f6d --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI044.py @@ -0,0 +1,7 @@ +# Bad import. +from __future__ import annotations # Not PYI044 (not a stubfile). + +# Good imports. +from __future__ import Something +import sys +from socket import AF_INET diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI044.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI044.pyi new file mode 100644 index 0000000000..18018deee6 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI044.pyi @@ -0,0 +1,7 @@ +# Bad import. +from __future__ import annotations # PYI044. + +# Good imports. +from __future__ import Something +import sys +from socket import AF_INET diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI053.py b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI053.py index 15631d15f2..8b2811eb63 100644 --- a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI053.py +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI053.py @@ -36,3 +36,11 @@ bar: str = "51 character stringgggggggggggggggggggggggggggggggg" baz: bytes = b"50 character byte stringgggggggggggggggggggggggggg" qux: bytes = b"51 character byte stringggggggggggggggggggggggggggg\xff" + + +class Demo: + """Docstrings are excluded from this rule. Some padding.""" + + +def func() -> None: + """Docstrings are excluded from this rule. Some padding.""" diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI053.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI053.pyi index d2f55531a2..71064d9bdb 100644 --- a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI053.pyi +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI053.pyi @@ -28,3 +28,9 @@ bar: str = "51 character stringgggggggggggggggggggggggggggggggg" # Error: PYI05 baz: bytes = b"50 character byte stringgggggggggggggggggggggggggg" # OK qux: bytes = b"51 character byte stringggggggggggggggggggggggggggg\xff" # Error: PYI053 + +class Demo: + """Docstrings are excluded from this rule. Some padding.""" # OK + +def func() -> None: + """Docstrings are excluded from this rule. Some padding.""" # OK diff --git a/crates/ruff/resources/test/fixtures/flake8_pytest_style/PT016.py b/crates/ruff/resources/test/fixtures/flake8_pytest_style/PT016.py index 4f06451450..288d5d9be6 100644 --- a/crates/ruff/resources/test/fixtures/flake8_pytest_style/PT016.py +++ b/crates/ruff/resources/test/fixtures/flake8_pytest_style/PT016.py @@ -1,17 +1,25 @@ import pytest -def test_xxx(): - pytest.fail("this is a failure") # Test OK arg +# OK +def f(): + pytest.fail("this is a failure") -def test_xxx(): - pytest.fail(msg="this is a failure") # Test OK kwarg +def f(): + pytest.fail(msg="this is a failure") -def test_xxx(): # Error +def f(): + pytest.fail(reason="this is a failure") + + +# Errors +def f(): pytest.fail() pytest.fail("") pytest.fail(f"") pytest.fail(msg="") pytest.fail(msg=f"") + pytest.fail(reason="") + pytest.fail(reason=f"") diff --git a/crates/ruff/resources/test/fixtures/flake8_pytest_style/PT018.py b/crates/ruff/resources/test/fixtures/flake8_pytest_style/PT018.py index 9bc5fbe877..cedd7ba170 100644 --- a/crates/ruff/resources/test/fixtures/flake8_pytest_style/PT018.py +++ b/crates/ruff/resources/test/fixtures/flake8_pytest_style/PT018.py @@ -21,6 +21,13 @@ def test_error(): assert something and something_else == """error message """ + assert ( + something + and something_else + == """error +message +""" + ) # recursive case assert not (a or not (b or c)) @@ -31,14 +38,6 @@ def test_error(): assert not (something or something_else and something_third), "with message" # detected, but no autofix for mixed conditions (e.g. `a or b and c`) assert not (something or something_else and something_third) - # detected, but no autofix for parenthesized conditions - assert ( - something - and something_else - == """error -message -""" - ) assert something # OK diff --git a/crates/ruff/resources/test/fixtures/flake8_raise/RSE102.py b/crates/ruff/resources/test/fixtures/flake8_raise/RSE102.py index aa80fa5145..ce75d7ab7c 100644 --- a/crates/ruff/resources/test/fixtures/flake8_raise/RSE102.py +++ b/crates/ruff/resources/test/fixtures/flake8_raise/RSE102.py @@ -29,6 +29,26 @@ raise TypeError( # Hello, world! ) +# OK raise AssertionError +# OK raise AttributeError("test message") + + +def return_error(): + return ValueError("Something") + + +# OK +raise return_error() + + +class Class: + @staticmethod + def error(): + return ValueError("Something") + + +# OK +raise Class.error() diff --git a/crates/ruff/resources/test/fixtures/flake8_simplify/SIM110.py b/crates/ruff/resources/test/fixtures/flake8_simplify/SIM110.py index b02ac7c28c..cc2527e092 100644 --- a/crates/ruff/resources/test/fixtures/flake8_simplify/SIM110.py +++ b/crates/ruff/resources/test/fixtures/flake8_simplify/SIM110.py @@ -171,3 +171,17 @@ def f(): if x.isdigit(): return True return False + +async def f(): + # OK + for x in iterable: + if await check(x): + return True + return False + +async def f(): + # SIM110 + for x in iterable: + if check(x): + return True + return False diff --git a/crates/ruff/resources/test/fixtures/flake8_simplify/SIM117.py b/crates/ruff/resources/test/fixtures/flake8_simplify/SIM117.py index 3c99535e43..4053754f02 100644 --- a/crates/ruff/resources/test/fixtures/flake8_simplify/SIM117.py +++ b/crates/ruff/resources/test/fixtures/flake8_simplify/SIM117.py @@ -33,17 +33,17 @@ with A() as a: print("hello") a() -# OK +# OK, can't merge async with and with. async with A() as a: with B() as b: print("hello") -# OK +# OK, can't merge async with and with. with A() as a: async with B() as b: print("hello") -# OK +# SIM117 async with A() as a: async with B() as b: print("hello") @@ -99,4 +99,25 @@ with A("01ß9💣2ℝ8901ß9💣2ℝ8901ß9💣2ℝ89") as a: # SIM117 (not auto-fixable too long) with A("01ß9💣2ℝ8901ß9💣2ℝ8901ß9💣2ℝ890") as a: with B("01ß9💣2ℝ8901ß9💣2ℝ8901ß9💣2ℝ89") as b: - print("hello") \ No newline at end of file + print("hello") + +# From issue #3025. +async def main(): + async with A() as a: # SIM117. + async with B() as b: + print("async-inside!") + + return 0 + +# OK. Can't merge across different kinds of with statements. +with a as a2: + async with b as b2: + with c as c2: + async with d as d2: + f(a2, b2, c2, d2) + +# OK. Can't merge across different kinds of with statements. +async with b as b2: + with c as c2: + async with d as d2: + f(b2, c2, d2) diff --git a/crates/ruff/resources/test/fixtures/flake8_todos/TD002.py b/crates/ruff/resources/test/fixtures/flake8_todos/TD002.py index 3c4867516f..438d6a1e6a 100644 --- a/crates/ruff/resources/test/fixtures/flake8_todos/TD002.py +++ b/crates/ruff/resources/test/fixtures/flake8_todos/TD002.py @@ -1,6 +1,12 @@ # T002 - accepted # TODO (evanrittenhouse): this has an author -# TODO(evanrittenhouse): this also has an author +# TODO(evanrittenhouse): this has an author +# TODO (evanrittenhouse) and more: this has an author +# TODO(evanrittenhouse) and more: this has an author +# TODO@mayrholu: this has an author +# TODO @mayrholu: this has an author +# TODO@mayrholu and more: this has an author +# TODO @mayrholu and more: this has an author # T002 - errors # TODO: this has no author # FIXME: neither does this diff --git a/crates/ruff/resources/test/fixtures/flake8_type_checking/TCH002.py b/crates/ruff/resources/test/fixtures/flake8_type_checking/TCH002.py index 82d6d2f10b..9248c10775 100644 --- a/crates/ruff/resources/test/fixtures/flake8_type_checking/TCH002.py +++ b/crates/ruff/resources/test/fixtures/flake8_type_checking/TCH002.py @@ -164,3 +164,11 @@ def f(): ) x: DataFrame = 2 + + +def f(): + global Member + + from module import Member + + x: Member = 1 diff --git a/crates/ruff/resources/test/fixtures/isort/case_sensitive.py b/crates/ruff/resources/test/fixtures/isort/case_sensitive.py new file mode 100644 index 0000000000..6f500358ee --- /dev/null +++ b/crates/ruff/resources/test/fixtures/isort/case_sensitive.py @@ -0,0 +1,9 @@ +import A +import B +import b +import C +import d +import E +import f +from g import a, B, c +from h import A, b, C diff --git a/crates/ruff/resources/test/fixtures/isort/skip.py b/crates/ruff/resources/test/fixtures/isort/skip.py index 5cf668879b..97d38fdb15 100644 --- a/crates/ruff/resources/test/fixtures/isort/skip.py +++ b/crates/ruff/resources/test/fixtures/isort/skip.py @@ -26,3 +26,9 @@ def f(): import os # isort:skip import collections import abc + + +def f(): + import sys; import os # isort:skip + import sys; import os # isort:skip # isort:skip + import sys; import os diff --git a/crates/ruff/resources/test/fixtures/isort/split.py b/crates/ruff/resources/test/fixtures/isort/split.py index e4beaec563..c82885853a 100644 --- a/crates/ruff/resources/test/fixtures/isort/split.py +++ b/crates/ruff/resources/test/fixtures/isort/split.py @@ -19,3 +19,13 @@ if True: import D import B + + +import e +import f + +# isort: split +# isort: split + +import d +import c diff --git a/crates/ruff/resources/test/fixtures/jupyter/after_fix.ipynb b/crates/ruff/resources/test/fixtures/jupyter/after_fix.ipynb new file mode 100644 index 0000000000..407665029b --- /dev/null +++ b/crates/ruff/resources/test/fixtures/jupyter/after_fix.ipynb @@ -0,0 +1,37 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": {}, + "outputs": [], + "source": [ + "import math\n", + "\n", + "math.pi" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python (ruff)", + "language": "python", + "name": "ruff" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/crates/ruff/resources/test/fixtures/jupyter/before_fix.ipynb b/crates/ruff/resources/test/fixtures/jupyter/before_fix.ipynb new file mode 100644 index 0000000000..fdaaa2819c --- /dev/null +++ b/crates/ruff/resources/test/fixtures/jupyter/before_fix.ipynb @@ -0,0 +1,38 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": {}, + "outputs": [], + "source": [ + "import math\n", + "import os\n", + "\n", + "math.pi" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python (ruff)", + "language": "python", + "name": "ruff" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/crates/ruff/resources/test/fixtures/jupyter/cell/code_and_magic.json b/crates/ruff/resources/test/fixtures/jupyter/cell/code_and_magic.json new file mode 100644 index 0000000000..5fc3d268f4 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/jupyter/cell/code_and_magic.json @@ -0,0 +1,8 @@ +{ + "execution_count": null, + "cell_type": "code", + "id": "1", + "metadata": {}, + "outputs": [], + "source": ["def foo():\n", " pass\n", "\n", "%timeit foo()"] +} diff --git a/crates/ruff/resources/test/fixtures/jupyter/cell/markdown.json b/crates/ruff/resources/test/fixtures/jupyter/cell/markdown.json new file mode 100644 index 0000000000..00b7245742 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/jupyter/cell/markdown.json @@ -0,0 +1,6 @@ +{ + "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": ["This is a markdown cell\n", "Some more content"] +} diff --git a/crates/ruff/resources/test/fixtures/jupyter/cell/only_code.json b/crates/ruff/resources/test/fixtures/jupyter/cell/only_code.json new file mode 100644 index 0000000000..c36b77bbeb --- /dev/null +++ b/crates/ruff/resources/test/fixtures/jupyter/cell/only_code.json @@ -0,0 +1,8 @@ +{ + "execution_count": null, + "cell_type": "code", + "id": "1", + "metadata": {}, + "outputs": [], + "source": ["def foo():\n", " pass"] +} diff --git a/crates/ruff/resources/test/fixtures/jupyter/cell/only_magic.json b/crates/ruff/resources/test/fixtures/jupyter/cell/only_magic.json new file mode 100644 index 0000000000..515ba814fc --- /dev/null +++ b/crates/ruff/resources/test/fixtures/jupyter/cell/only_magic.json @@ -0,0 +1,8 @@ +{ + "execution_count": null, + "cell_type": "code", + "id": "1", + "metadata": {}, + "outputs": [], + "source": "%timeit print('hello world')" +} diff --git a/crates/ruff/resources/test/fixtures/jupyter/isort.ipynb b/crates/ruff/resources/test/fixtures/jupyter/isort.ipynb new file mode 100644 index 0000000000..aef5ff2e8b --- /dev/null +++ b/crates/ruff/resources/test/fixtures/jupyter/isort.ipynb @@ -0,0 +1,51 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "0c7535f6-43cb-423f-bfe1-d263b8f55da0", + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "import random\n", + "import math" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c066fa1a-5682-47af-8c17-5afec3cf4ad0", + "metadata": {}, + "outputs": [], + "source": [ + "from typing import Any\n", + "import collections\n", + "# Newline should be added here\n", + "def foo():\n", + " pass" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python (ruff)", + "language": "python", + "name": "ruff" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/crates/ruff/resources/test/fixtures/jupyter/isort_expected.ipynb b/crates/ruff/resources/test/fixtures/jupyter/isort_expected.ipynb new file mode 100644 index 0000000000..009c598e71 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/jupyter/isort_expected.ipynb @@ -0,0 +1,53 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "663ba955-baca-4f34-9ebb-840d2573ae3f", + "metadata": {}, + "outputs": [], + "source": [ + "import math\n", + "import random\n", + "from pathlib import Path" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d0adfe23-8aea-47e9-bf67-d856cfcb96ea", + "metadata": {}, + "outputs": [], + "source": [ + "import collections\n", + "from typing import Any\n", + "\n", + "\n", + "# Newline should be added here\n", + "def foo():\n", + " pass" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python (ruff)", + "language": "python", + "name": "ruff" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/crates/ruff/resources/test/fixtures/jupyter/no_trailing_newline.ipynb b/crates/ruff/resources/test/fixtures/jupyter/no_trailing_newline.ipynb new file mode 100644 index 0000000000..4c7df0a39e --- /dev/null +++ b/crates/ruff/resources/test/fixtures/jupyter/no_trailing_newline.ipynb @@ -0,0 +1,38 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "4cec6161-f594-446c-ab65-37395bbb3127", + "metadata": {}, + "outputs": [], + "source": [ + "import math\n", + "import os\n", + "\n", + "_ = math.pi" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python (ruff)", + "language": "python", + "name": "ruff" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/crates/ruff/resources/test/fixtures/jupyter/valid.ipynb b/crates/ruff/resources/test/fixtures/jupyter/valid.ipynb index 5d16c271ee..63ca4467a2 100644 --- a/crates/ruff/resources/test/fixtures/jupyter/valid.ipynb +++ b/crates/ruff/resources/test/fixtures/jupyter/valid.ipynb @@ -3,6 +3,16 @@ { "cell_type": "code", "execution_count": 1, + "metadata": { + "ExecuteTime": { + "end_time": "2023-03-08T23:01:09.782916Z", + "start_time": "2023-03-08T23:01:09.705831Z" + }, + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "outputs": [ { "name": "stdout", @@ -19,32 +29,26 @@ " print(f\"cell one: {y}\")\n", "\n", "unused_variable()" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "start_time": "2023-03-08T23:01:09.705831Z", - "end_time": "2023-03-08T23:01:09.782916Z" - } - } + ] }, { "cell_type": "markdown", + "metadata": {}, "source": [ "Let's do another mistake" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", "execution_count": 2, "metadata": { - "collapsed": true, "ExecuteTime": { - "start_time": "2023-03-08T23:01:09.733809Z", - "end_time": "2023-03-08T23:01:09.915760Z" + "end_time": "2023-03-08T23:01:09.915760Z", + "start_time": "2023-03-08T23:01:09.733809Z" + }, + "collapsed": true, + "jupyter": { + "outputs_hidden": true } }, "outputs": [ @@ -62,27 +66,66 @@ "\n", "mutable_argument()\n" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's create an empty cell" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Multi-line empty cell!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"after empty cells\")" + ] } ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python (ruff)", "language": "python", - "name": "python3" + "name": "ruff" }, "language_info": { "codemirror_mode": { "name": "ipython", - "version": 2 + "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.6" + "pygments_lexer": "ipython3", + "version": "3.11.3" } }, "nbformat": 4, - "nbformat_minor": 0 + "nbformat_minor": 4 } diff --git a/crates/ruff/resources/test/fixtures/numpy/NPY002.py b/crates/ruff/resources/test/fixtures/numpy/NPY002.py index d0e2274e6d..129b270cab 100644 --- a/crates/ruff/resources/test/fixtures/numpy/NPY002.py +++ b/crates/ruff/resources/test/fixtures/numpy/NPY002.py @@ -1,5 +1,6 @@ # Do this (new version) from numpy.random import default_rng + rng = default_rng() vals = rng.standard_normal(10) more_vals = rng.standard_normal(10) @@ -7,11 +8,13 @@ numbers = rng.integers(high, size=5) # instead of this (legacy version) from numpy import random + vals = random.standard_normal(10) more_vals = random.standard_normal(10) numbers = random.integers(high, size=5) import numpy + numpy.random.seed() numpy.random.get_state() numpy.random.set_state() diff --git a/crates/ruff/resources/test/fixtures/numpy/NPY003.py b/crates/ruff/resources/test/fixtures/numpy/NPY003.py new file mode 100644 index 0000000000..6d6f369771 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/numpy/NPY003.py @@ -0,0 +1,15 @@ +import numpy as np + +np.round_(np.random.rand(5, 5), 2) +np.product(np.random.rand(5, 5)) +np.cumproduct(np.random.rand(5, 5)) +np.sometrue(np.random.rand(5, 5)) +np.alltrue(np.random.rand(5, 5)) + +from numpy import round_, product, cumproduct, sometrue, alltrue + +round_(np.random.rand(5, 5), 2) +product(np.random.rand(5, 5)) +cumproduct(np.random.rand(5, 5)) +sometrue(np.random.rand(5, 5)) +alltrue(np.random.rand(5, 5)) diff --git a/crates/ruff/resources/test/fixtures/pandas_vet/PD002.py b/crates/ruff/resources/test/fixtures/pandas_vet/PD002.py index 99dc33a327..4d1fc96b59 100644 --- a/crates/ruff/resources/test/fixtures/pandas_vet/PD002.py +++ b/crates/ruff/resources/test/fixtures/pandas_vet/PD002.py @@ -4,7 +4,9 @@ x = pd.DataFrame() x.drop(["a"], axis=1, inplace=True) -x.drop(["a"], axis=1, inplace=True) +x.y.drop(["a"], axis=1, inplace=True) + +x["y"].drop(["a"], axis=1, inplace=True) x.drop( inplace=True, @@ -23,6 +25,7 @@ x.drop(["a"], axis=1, **kwargs, inplace=True) x.drop(["a"], axis=1, inplace=True, **kwargs) f(x.drop(["a"], axis=1, inplace=True)) -x.apply(lambda x: x.sort_values('a', inplace=True)) +x.apply(lambda x: x.sort_values("a", inplace=True)) import torch -torch.m.ReLU(inplace=True) # safe because this isn't a pandas call + +torch.m.ReLU(inplace=True) # safe because this isn't a pandas call diff --git a/crates/ruff/resources/test/fixtures/pep8_naming/N805.py b/crates/ruff/resources/test/fixtures/pep8_naming/N805.py index d45a9c3118..99fcf2ed0c 100644 --- a/crates/ruff/resources/test/fixtures/pep8_naming/N805.py +++ b/crates/ruff/resources/test/fixtures/pep8_naming/N805.py @@ -1,4 +1,4 @@ -from abc import ABCMeta +import abc import pydantic @@ -19,6 +19,10 @@ class Class: def class_method(cls): pass + @abc.abstractclassmethod + def abstract_class_method(cls): + pass + @staticmethod def static_method(x): return x @@ -41,7 +45,7 @@ class Class: ... -class MetaClass(ABCMeta): +class MetaClass(abc.ABCMeta): def bad_method(self): pass diff --git a/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N801.py b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N801.py new file mode 100644 index 0000000000..3266975f8c --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N801.py @@ -0,0 +1,11 @@ +class badAllowed: + pass + +class stillBad: + pass + +class BAD_ALLOWED: + pass + +class STILL_BAD: + pass diff --git a/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N802.py b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N802.py new file mode 100644 index 0000000000..5bd0717e9b --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N802.py @@ -0,0 +1,14 @@ +import unittest + +def badAllowed(): + pass + +def stillBad(): + pass + +class Test(unittest.TestCase): + def badAllowed(self): + return super().tearDown() + + def stillBad(self): + return super().tearDown() diff --git a/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N803.py b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N803.py new file mode 100644 index 0000000000..2c2d7ba2be --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N803.py @@ -0,0 +1,12 @@ +def func(_, a, badAllowed): + return _, a, badAllowed + +def func(_, a, stillBad): + return _, a, stillBad + +class Class: + def method(self, _, a, badAllowed): + return _, a, badAllowed + + def method(self, _, a, stillBad): + return _, a, stillBad diff --git a/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N804.py b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N804.py new file mode 100644 index 0000000000..c3f9598417 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N804.py @@ -0,0 +1,22 @@ +from abc import ABCMeta + + +class Class: + def __init_subclass__(self, default_name, **kwargs): + ... + + @classmethod + def badAllowed(self, x, /, other): + ... + + @classmethod + def stillBad(self, x, /, other): + ... + + +class MetaClass(ABCMeta): + def badAllowed(self): + pass + + def stillBad(self): + pass diff --git a/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N805.py b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N805.py new file mode 100644 index 0000000000..590df0bd6d --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N805.py @@ -0,0 +1,59 @@ +import abc + +import pydantic + + +class Class: + def badAllowed(this): + pass + + def stillBad(this): + pass + + if False: + + def badAllowed(this): + pass + + def stillBad(this): + pass + + @pydantic.validator + def badAllowed(cls, my_field: str) -> str: + pass + + @pydantic.validator + def stillBad(cls, my_field: str) -> str: + pass + + @pydantic.validator("my_field") + def badAllowed(cls, my_field: str) -> str: + pass + + @pydantic.validator("my_field") + def stillBad(cls, my_field: str) -> str: + pass + + @classmethod + def badAllowed(cls): + pass + + @classmethod + def stillBad(cls): + pass + + @abc.abstractclassmethod + def badAllowed(cls): + pass + + @abc.abstractclassmethod + def stillBad(cls): + pass + + +class PosOnlyClass: + def badAllowed(this, blah, /, self, something: str): + pass + + def stillBad(this, blah, /, self, something: str): + pass diff --git a/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N806.py b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N806.py new file mode 100644 index 0000000000..7ece45c61b --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N806.py @@ -0,0 +1,6 @@ +def assign(): + badAllowed = 0 + stillBad = 0 + + BAD_ALLOWED = 0 + STILL_BAD = 0 diff --git a/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N807.py b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N807.py new file mode 100644 index 0000000000..e8d3c5ed24 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N807.py @@ -0,0 +1,13 @@ +def __badAllowed__(): + pass + +def __stillBad__(): + pass + + +def nested(): + def __badAllowed__(): + pass + + def __stillBad__(): + pass diff --git a/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N811.py b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N811.py new file mode 100644 index 0000000000..fbb20f0399 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N811.py @@ -0,0 +1,5 @@ +import mod.BAD_ALLOWED as badAllowed +import mod.STILL_BAD as stillBad + +from mod import BAD_ALLOWED as badAllowed +from mod import STILL_BAD as stillBad diff --git a/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N812.py b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N812.py new file mode 100644 index 0000000000..be6180f86d --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N812.py @@ -0,0 +1,5 @@ +import mod.badallowed as badAllowed +import mod.stillbad as stillBad + +from mod import badallowed as BadAllowed +from mod import stillbad as StillBad diff --git a/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N813.py b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N813.py new file mode 100644 index 0000000000..aa44f32b62 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N813.py @@ -0,0 +1,8 @@ +import mod.BadAllowed as badallowed +import mod.stillBad as stillbad + +from mod import BadAllowed as badallowed +from mod import StillBad as stillbad + +from mod import BadAllowed as bad_allowed +from mod import StillBad as still_bad diff --git a/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N814.py b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N814.py new file mode 100644 index 0000000000..ac7a530cf0 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N814.py @@ -0,0 +1,8 @@ +import mod.BadAllowed as BADALLOWED +import mod.StillBad as STILLBAD + +from mod import BadAllowed as BADALLOWED +from mod import StillBad as STILLBAD + +from mod import BadAllowed as BAD_ALLOWED +from mod import StillBad as STILL_BAD diff --git a/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N815.py b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N815.py new file mode 100644 index 0000000000..02c39544ca --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N815.py @@ -0,0 +1,19 @@ +class C: + badAllowed = 0 + stillBad = 0 + + _badAllowed = 0 + _stillBad = 0 + + bad_Allowed = 0 + still_Bad = 0 + +class D(TypedDict): + badAllowed: bool + stillBad: bool + + _badAllowed: list + _stillBad: list + + bad_Allowed: set + still_Bad: set diff --git a/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N816.py b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N816.py new file mode 100644 index 0000000000..5ae20d20fe --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N816.py @@ -0,0 +1,8 @@ +badAllowed = 0 +stillBad = 0 + +_badAllowed = 0 +_stillBad = 0 + +bad_Allowed = 0 +still_Bad = 0 diff --git a/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N817.py b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N817.py new file mode 100644 index 0000000000..27a5ce4299 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N817.py @@ -0,0 +1,5 @@ +import mod.BadAllowed as BA +import mod.StillBad as SB + +from mod import BadAllowed as BA +from mod import StillBad as SB diff --git a/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N818.py b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N818.py new file mode 100644 index 0000000000..57f6887a96 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N818.py @@ -0,0 +1,11 @@ +class BadAllowed(Exception): + pass + +class StillBad(Exception): + pass + +class BadAllowed(AnotherError): + pass + +class StillBad(AnotherError): + pass diff --git a/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N999/badAllowed/__init__.py b/crates/ruff/resources/test/fixtures/pep8_naming/ignore_names/N999/badAllowed/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/ruff/resources/test/fixtures/perflint/PERF101.py b/crates/ruff/resources/test/fixtures/perflint/PERF101.py new file mode 100644 index 0000000000..6123e6d652 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/perflint/PERF101.py @@ -0,0 +1,52 @@ +foo_tuple = (1, 2, 3) +foo_list = [1, 2, 3] +foo_set = {1, 2, 3} +foo_dict = {1: 2, 3: 4} +foo_int = 123 + +for i in list(foo_tuple): # PERF101 + pass + +for i in list(foo_list): # PERF101 + pass + +for i in list(foo_set): # PERF101 + pass + +for i in list((1, 2, 3)): # PERF101 + pass + +for i in list([1, 2, 3]): # PERF101 + pass + +for i in list({1, 2, 3}): # PERF101 + pass + +for i in list( + { + 1, + 2, + 3, + } +): + pass + +for i in list( # Comment + {1, 2, 3} +): # PERF101 + pass + +for i in list(foo_dict): # Ok + pass + +for i in list(1): # Ok + pass + +for i in list(foo_int): # Ok + pass + + +import itertools + +for i in itertools.product(foo_int): # Ok + pass diff --git a/crates/ruff/resources/test/fixtures/perflint/PERF102.py b/crates/ruff/resources/test/fixtures/perflint/PERF102.py new file mode 100644 index 0000000000..8167138ca6 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/perflint/PERF102.py @@ -0,0 +1,71 @@ +some_dict = {"a": 12, "b": 32, "c": 44} + +for _, value in some_dict.items(): # PERF102 + print(value) + + +for key, _ in some_dict.items(): # PERF102 + print(key) + + +for weird_arg_name, _ in some_dict.items(): # PERF102 + print(weird_arg_name) + + +for name, (_, _) in some_dict.items(): # PERF102 + pass + + +for name, (value1, _) in some_dict.items(): # OK + pass + + +for (key1, _), (_, _) in some_dict.items(): # PERF102 + pass + + +for (_, (_, _)), (value, _) in some_dict.items(): # PERF102 + pass + + +for (_, key2), (value1, _) in some_dict.items(): # OK + pass + + +for ((_, key2), (value1, _)) in some_dict.items(): # OK + pass + + +for ((_, key2), (_, _)) in some_dict.items(): # PERF102 + pass + + +for (_, _, _, variants), (r_language, _, _, _) in some_dict.items(): # OK + pass + + +for (_, _, (_, variants)), (_, (_, (r_language, _))) in some_dict.items(): # OK + pass + + +for key, value in some_dict.items(): # OK + print(key, value) + + +for _, value in some_dict.items(12): # OK + print(value) + + +for key in some_dict.keys(): # OK + print(key) + + +for value in some_dict.values(): # OK + print(value) + + +for name, (_, _) in (some_function()).items(): # PERF102 + pass + +for name, (_, _) in (some_function().some_attribute).items(): # PERF102 + pass diff --git a/crates/ruff/resources/test/fixtures/perflint/PERF203.py b/crates/ruff/resources/test/fixtures/perflint/PERF203.py new file mode 100644 index 0000000000..ec3ca5feee --- /dev/null +++ b/crates/ruff/resources/test/fixtures/perflint/PERF203.py @@ -0,0 +1,28 @@ +for i in range(10): + try: # PERF203 + print(f"{i}") + except: + print("error") + +try: + for i in range(10): + print(f"{i}") +except: + print("error") + +i = 0 +while i < 10: # PERF203 + try: + print(f"{i}") + except: + print("error") + + i += 1 + +try: + i = 0 + while i < 10: + print(f"{i}") + i += 1 +except: + print("error") diff --git a/crates/ruff/resources/test/fixtures/perflint/PERF401.py b/crates/ruff/resources/test/fixtures/perflint/PERF401.py new file mode 100644 index 0000000000..2c4434c8ed --- /dev/null +++ b/crates/ruff/resources/test/fixtures/perflint/PERF401.py @@ -0,0 +1,47 @@ +def f(): + items = [1, 2, 3, 4] + result = [] + for i in items: + if i % 2: + result.append(i) # PERF401 + + +def f(): + items = [1, 2, 3, 4] + result = [] + for i in items: + result.append(i * i) # PERF401 + + +def f(): + items = [1, 2, 3, 4] + result = [] + for i in items: + if i % 2: + result.append(i) # PERF401 + elif i % 2: + result.append(i) # PERF401 + else: + result.append(i) # PERF401 + + +def f(): + items = [1, 2, 3, 4] + result = [] + for i in items: + result.append(i) # OK + + +def f(): + items = [1, 2, 3, 4] + result = {} + for i in items: + result[i].append(i) # OK + + +def f(): + items = [1, 2, 3, 4] + result = [] + for i in items: + if i not in result: + result.append(i) # OK diff --git a/crates/ruff/resources/test/fixtures/perflint/PERF402.py b/crates/ruff/resources/test/fixtures/perflint/PERF402.py new file mode 100644 index 0000000000..55f3e08cbc --- /dev/null +++ b/crates/ruff/resources/test/fixtures/perflint/PERF402.py @@ -0,0 +1,26 @@ +def f(): + items = [1, 2, 3, 4] + result = [] + for i in items: + result.append(i) # PERF402 + + +def f(): + items = [1, 2, 3, 4] + result = [] + for i in items: + result.insert(0, i) # PERF402 + + +def f(): + items = [1, 2, 3, 4] + result = [] + for i in items: + result.append(i * i) # OK + + +def f(): + items = [1, 2, 3, 4] + result = {} + for i in items: + result[i].append(i * i) # OK diff --git a/crates/ruff/resources/test/fixtures/pycodestyle/E70.py b/crates/ruff/resources/test/fixtures/pycodestyle/E70.py index bfbec79124..9b9fde824c 100644 --- a/crates/ruff/resources/test/fixtures/pycodestyle/E70.py +++ b/crates/ruff/resources/test/fixtures/pycodestyle/E70.py @@ -60,3 +60,9 @@ match *0, 1, *2: #: class Foo: match: Optional[Match] = None +#: E702:2:4 +while 1: + 1;... +#: E703:2:1 +0\ +; diff --git a/crates/ruff/resources/test/fixtures/pycodestyle/E731.py b/crates/ruff/resources/test/fixtures/pycodestyle/E731.py index 4464c0c8b9..c207c7dae9 100644 --- a/crates/ruff/resources/test/fixtures/pycodestyle/E731.py +++ b/crates/ruff/resources/test/fixtures/pycodestyle/E731.py @@ -1,51 +1,135 @@ -#: E731 -f = lambda x: 2 * x -#: E731 -f = lambda x: 2 * x -#: E731 -while False: - this = lambda y, z: 2 * x -#: E731 -f = lambda: (yield 1) -#: E731 -f = lambda: (yield from g()) -#: E731 -class F: +def scope(): + # E731 f = lambda x: 2 * x -f = object() -f.method = lambda: "Method" -f = {} -f["a"] = lambda x: x**2 -f = [] -f.append(lambda x: x**2) -f = g = lambda x: x**2 -lambda: "no-op" +def scope(): + # E731 + f = lambda x: 2 * x -# Annotated -from typing import Callable, ParamSpec -P = ParamSpec("P") +def scope(): + # E731 + while False: + this = lambda y, z: 2 * x + + +def scope(): + # E731 + f = lambda: (yield 1) + + +def scope(): + # E731 + f = lambda: (yield from g()) + + +def scope(): + # OK + f = object() + f.method = lambda: "Method" + + +def scope(): + # OK + f = {} + f["a"] = lambda x: x**2 + + +def scope(): + # OK + f = [] + f.append(lambda x: x**2) + + +def scope(): + # OK + f = g = lambda x: x**2 + + +def scope(): + # OK + lambda: "no-op" + + +class Scope: + # E731 + f = lambda x: 2 * x + + +class Scope: + from typing import Callable + + # E731 + f: Callable[[int], int] = lambda x: 2 * x + + +def scope(): + # E731 + from typing import Callable + + x: Callable[[int], int] + if True: + x = lambda: 1 + else: + x = lambda: 2 + return x + + +def scope(): + # E731 + + from typing import Callable, ParamSpec + + # ParamSpec cannot be used in this context, so do not preserve the annotation. + P = ParamSpec("P") + f: Callable[P, int] = lambda *args: len(args) + + +def scope(): + # E731 + + from typing import Callable + + f: Callable[[], None] = lambda: None + + +def scope(): + # E731 + + from typing import Callable + + f: Callable[..., None] = lambda a, b: None + + +def scope(): + # E731 + + from typing import Callable + + f: Callable[[int], int] = lambda x: 2 * x -# ParamSpec cannot be used in this context, so do not preserve the annotation. -f: Callable[P, int] = lambda *args: len(args) -f: Callable[[], None] = lambda: None -f: Callable[..., None] = lambda a, b: None -f: Callable[[int], int] = lambda x: 2 * x # Let's use the `Callable` type from `collections.abc` instead. -from collections.abc import Callable +def scope(): + # E731 -f: Callable[[str, int], str] = lambda a, b: a * b -f: Callable[[str, int], tuple[str, int]] = lambda a, b: (a, b) -f: Callable[[str, int, list[str]], list[str]] = lambda a, b, /, c: [*c, a * b] + from collections.abc import Callable + + f: Callable[[str, int], str] = lambda a, b: a * b -# Override `Callable` -class Callable: - pass +def scope(): + # E731 + + from collections.abc import Callable + + f: Callable[[str, int], tuple[str, int]] = lambda a, b: (a, b) -# Do not copy the annotation from here on out. -f: Callable[[str, int], str] = lambda a, b: a * b +def scope(): + # E731 + + from collections.abc import Callable + + f: Callable[[str, int, list[str]], list[str]] = lambda a, b, /, c: [*c, a * b] diff --git a/crates/ruff/resources/test/fixtures/pycodestyle/W605_0.py b/crates/ruff/resources/test/fixtures/pycodestyle/W605_0.py index 5322443e87..287e71a1d9 100644 --- a/crates/ruff/resources/test/fixtures/pycodestyle/W605_0.py +++ b/crates/ruff/resources/test/fixtures/pycodestyle/W605_0.py @@ -19,6 +19,14 @@ with \_ somewhere in the middle """ +#: W605:1:38 +value = 'new line\nand invalid escape \_ here' + + +def f(): + #: W605:1:11 + return'\.png$' + #: Okay regex = r'\.png$' regex = '\\.png$' diff --git a/crates/ruff/resources/test/fixtures/pycodestyle/W605_1.py b/crates/ruff/resources/test/fixtures/pycodestyle/W605_1.py index 5322443e87..20bf0ea14c 100644 --- a/crates/ruff/resources/test/fixtures/pycodestyle/W605_1.py +++ b/crates/ruff/resources/test/fixtures/pycodestyle/W605_1.py @@ -19,6 +19,11 @@ with \_ somewhere in the middle """ + +def f(): + #: W605:1:11 + return'\.png$' + #: Okay regex = r'\.png$' regex = '\\.png$' diff --git a/crates/ruff/resources/test/fixtures/pydocstyle/D301.py b/crates/ruff/resources/test/fixtures/pydocstyle/D301.py new file mode 100644 index 0000000000..1e6c8eef07 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pydocstyle/D301.py @@ -0,0 +1,29 @@ +def double_quotes_backslash(): + """Sum\\mary.""" + + +def double_quotes_backslash_raw(): + r"""Sum\mary.""" + + +def double_quotes_backslash_uppercase(): + R"""Sum\\mary.""" + + +def make_unique_pod_id(pod_id: str) -> str | None: + r""" + Generate a unique Pod name. + + Kubernetes pod names must consist of one or more lowercase + rfc1035/rfc1123 labels separated by '.' with a maximum length of 253 + characters. + + Name must pass the following regex for validation + ``^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`` + + For more details, see: + https://github.com/kubernetes/kubernetes/blob/release-1.1/docs/design/identifiers.md + + :param pod_id: requested pod name + :return: ``str`` valid Pod name of appropriate length + """ diff --git a/crates/ruff/resources/test/fixtures/pydocstyle/D410.py b/crates/ruff/resources/test/fixtures/pydocstyle/D410.py new file mode 100644 index 0000000000..b9aec80568 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pydocstyle/D410.py @@ -0,0 +1,25 @@ +def f(a: int, b: int) -> int: + """Showcase function. + + Parameters + ---------- + a : int + _description_ + b : int + _description_ + Returns + ------- + int + _description + """ + return b - a + + +def f() -> int: + """Showcase function. + + Parameters + ---------- + Returns + ------- + """ diff --git a/crates/ruff/resources/test/fixtures/pydocstyle/sections.py b/crates/ruff/resources/test/fixtures/pydocstyle/sections.py index 4bd805065c..fef7f2fba1 100644 --- a/crates/ruff/resources/test/fixtures/pydocstyle/sections.py +++ b/crates/ruff/resources/test/fixtures/pydocstyle/sections.py @@ -513,3 +513,19 @@ def implicit_string_concatenation(): A value of some sort. """"Extra content" + + +def replace_equals_with_dash(): + """Equal length equals should be replaced with dashes. + + Parameters + ========== + """ + + +def replace_equals_with_dash2(): + """Here, the length of equals is not the same. + + Parameters + =========== + """ diff --git a/crates/ruff/resources/test/fixtures/pyflakes/F401_18.py b/crates/ruff/resources/test/fixtures/pyflakes/F401_18.py index 8fbb7c70a0..d23a05ef48 100644 --- a/crates/ruff/resources/test/fixtures/pyflakes/F401_18.py +++ b/crates/ruff/resources/test/fixtures/pyflakes/F401_18.py @@ -1,9 +1,11 @@ -"""Testing that multiple submodule imports are handled correctly.""" +"""Test that straight `__future__` imports are considered unused.""" -# The logic goes through each of the shadowed bindings upwards to get all the unused -# imports. It should only detect imports, not any other kind of binding. -multiprocessing = None -import multiprocessing.connection -import multiprocessing.pool -import multiprocessing.queues +def f(): + import __future__ + + +def f(): + import __future__ + + print(__future__.absolute_import) diff --git a/crates/ruff/resources/test/fixtures/pyflakes/F401_20.py b/crates/ruff/resources/test/fixtures/pyflakes/F401_20.py new file mode 100644 index 0000000000..8fbb7c70a0 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pyflakes/F401_20.py @@ -0,0 +1,9 @@ +"""Testing that multiple submodule imports are handled correctly.""" + +# The logic goes through each of the shadowed bindings upwards to get all the unused +# imports. It should only detect imports, not any other kind of binding. +multiprocessing = None + +import multiprocessing.connection +import multiprocessing.pool +import multiprocessing.queues diff --git a/crates/ruff/resources/test/fixtures/pyflakes/F541.py b/crates/ruff/resources/test/fixtures/pyflakes/F541.py index 8a30463c47..09c10216eb 100644 --- a/crates/ruff/resources/test/fixtures/pyflakes/F541.py +++ b/crates/ruff/resources/test/fixtures/pyflakes/F541.py @@ -37,7 +37,10 @@ f"{{test}}" f'{{ 40 }}' f"{{a {{x}}" f"{{{{x}}}}" +""f"" +''f"" +(""f""r"") # To be fixed # Error: f-string: single '}' is not allowed at line 41 column 8 -# f"\{{x}}" +# f"\{{x}}" diff --git a/crates/ruff/resources/test/fixtures/pyflakes/F601.py b/crates/ruff/resources/test/fixtures/pyflakes/F601.py index 3a42484852..e8e9e6a89f 100644 --- a/crates/ruff/resources/test/fixtures/pyflakes/F601.py +++ b/crates/ruff/resources/test/fixtures/pyflakes/F601.py @@ -48,3 +48,8 @@ x = { x = {"a": 1, "a": 1} x = {"a": 1, "b": 2, "a": 1} + +x = { + ('a', 'b'): 'asdf', + ('a', 'b'): 'qwer', +} diff --git a/crates/ruff/resources/test/fixtures/pyflakes/F841_3.py b/crates/ruff/resources/test/fixtures/pyflakes/F841_3.py index 28d5af1f3b..526c999441 100644 --- a/crates/ruff/resources/test/fixtures/pyflakes/F841_3.py +++ b/crates/ruff/resources/test/fixtures/pyflakes/F841_3.py @@ -108,3 +108,42 @@ def f(): def f(): toplevel = tt = 1 + + +def f(provided: int) -> int: + match provided: + case [_, *x]: + pass + + +def f(provided: int) -> int: + match provided: + case x: + pass + + +def f(provided: int) -> int: + match provided: + case Foo(bar) as x: + pass + + +def f(provided: int) -> int: + match provided: + case {"foo": 0, **x}: + pass + + +def f(provided: int) -> int: + match provided: + case {**x}: + pass + + +global CONSTANT + + +def f() -> None: + global CONSTANT + CONSTANT = 1 + CONSTANT = 2 diff --git a/crates/ruff/resources/test/fixtures/pyflakes/multi_statement_lines.py b/crates/ruff/resources/test/fixtures/pyflakes/multi_statement_lines.py index d4e8712933..16233c83db 100644 --- a/crates/ruff/resources/test/fixtures/pyflakes/multi_statement_lines.py +++ b/crates/ruff/resources/test/fixtures/pyflakes/multi_statement_lines.py @@ -1,4 +1,3 @@ - if True: import foo1; x = 1 import foo2; x = 1 @@ -11,7 +10,6 @@ if True: import foo4 \ ; x = 1 - if True: x = 1; import foo5 @@ -20,12 +18,10 @@ if True: x = 1; \ import foo6 - if True: x = 1 \ ; import foo7 - if True: x = 1; import foo8; x = 1 x = 1; import foo9; x = 1 @@ -40,12 +36,27 @@ if True: ;import foo11 \ ;x = 1 +if True: + x = 1; \ + \ + import foo12 + +if True: + x = 1; \ +\ + import foo13 + + +if True: + x = 1; \ + # \ + import foo14 # Continuation, but not as the last content in the file. x = 1; \ -import foo12 +import foo15 # Continuation, followed by end-of-file. (Removing `import foo` would cause a syntax # error.) x = 1; \ -import foo13 +import foo16 \ No newline at end of file diff --git a/crates/ruff/resources/test/fixtures/pylint/global_statement.py b/crates/ruff/resources/test/fixtures/pylint/global_statement.py index 69bcc36775..1d28c61955 100644 --- a/crates/ruff/resources/test/fixtures/pylint/global_statement.py +++ b/crates/ruff/resources/test/fixtures/pylint/global_statement.py @@ -73,3 +73,10 @@ def override_class(): pass CLASS() + + +def multiple_assignment(): + """Should warn on every assignment.""" + global CONSTANT # [global-statement] + CONSTANT = 1 + CONSTANT = 2 diff --git a/crates/ruff/resources/test/fixtures/pylint/iteration_over_set.py b/crates/ruff/resources/test/fixtures/pylint/iteration_over_set.py index 1b22612e52..901dd3f196 100644 --- a/crates/ruff/resources/test/fixtures/pylint/iteration_over_set.py +++ b/crates/ruff/resources/test/fixtures/pylint/iteration_over_set.py @@ -36,3 +36,6 @@ for item in set(("apples", "lemons", "water")): # set constructor is fine for number in {i for i in range(10)}: # set comprehensions are fine print(number) + +for item in {*numbers_set, 4, 5, 6}: # set unpacking is fine + print(f"I like {item}.") diff --git a/crates/ruff/resources/test/fixtures/pylint/redefined_loop_name.py b/crates/ruff/resources/test/fixtures/pylint/redefined_loop_name.py index b99bfe9323..6b9b499714 100644 --- a/crates/ruff/resources/test/fixtures/pylint/redefined_loop_name.py +++ b/crates/ruff/resources/test/fixtures/pylint/redefined_loop_name.py @@ -31,6 +31,21 @@ with None as i: with None as j: # ok pass +# Async with -> with, variable reused +async with None as i: + with None as i: # error + pass + +# Async with -> with, different variable +async with None as i: + with None as j: # ok + pass + +# Async for -> for, variable reused +async for i in []: + for i in []: # error + pass + # For -> for -> for, doubly nested variable reuse for i in []: for j in []: diff --git a/crates/ruff/resources/test/fixtures/pylint/repeated_equality_comparison_target.py b/crates/ruff/resources/test/fixtures/pylint/repeated_equality_comparison_target.py new file mode 100644 index 0000000000..f82e19a761 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pylint/repeated_equality_comparison_target.py @@ -0,0 +1,34 @@ +# Errors. +foo == "a" or foo == "b" + +foo != "a" and foo != "b" + +foo == "a" or foo == "b" or foo == "c" + +foo != "a" and foo != "b" and foo != "c" + +foo == a or foo == "b" or foo == 3 # Mixed types. + +# False negatives (the current implementation doesn't support Yoda conditions). +"a" == foo or "b" == foo or "c" == foo + +"a" != foo and "b" != foo and "c" != foo + +"a" == foo or foo == "b" or "c" == foo + +# OK +foo == "a" and foo == "b" and foo == "c" # `and` mixed with `==`. + +foo != "a" or foo != "b" or foo != "c" # `or` mixed with `!=`. + +foo == a or foo == b() or foo == c # Call expression. + +foo != a or foo() != b or foo != c # Call expression. + +foo in {"a", "b", "c"} # Uses membership test already. + +foo not in {"a", "b", "c"} # Uses membership test already. + +foo == "a" # Single comparison. + +foo != "a" # Single comparison. diff --git a/crates/ruff/resources/test/fixtures/pylint/single_string_slots.py b/crates/ruff/resources/test/fixtures/pylint/single_string_slots.py new file mode 100644 index 0000000000..b7a2dac915 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pylint/single_string_slots.py @@ -0,0 +1,35 @@ +# Errors. +class Foo: + __slots__ = "bar" + + def __init__(self, bar): + self.bar = bar + + +class Foo: + __slots__: str = "bar" + + def __init__(self, bar): + self.bar = bar + + +class Foo: + __slots__: str = f"bar" + + def __init__(self, bar): + self.bar = bar + + +# Non-errors. +class Foo: + __slots__ = ("bar",) + + def __init__(self, bar): + self.bar = bar + + +class Foo: + __slots__: tuple[str, ...] = ("bar",) + + def __init__(self, bar): + self.bar = bar diff --git a/crates/ruff/resources/test/fixtures/pylint/type_bivariance.py b/crates/ruff/resources/test/fixtures/pylint/type_bivariance.py new file mode 100644 index 0000000000..6084d97232 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pylint/type_bivariance.py @@ -0,0 +1,37 @@ +from typing import ParamSpec, TypeVar + +# Errors. + +T = TypeVar("T", covariant=True, contravariant=True) +T = TypeVar(name="T", covariant=True, contravariant=True) + +T = ParamSpec("T", covariant=True, contravariant=True) +T = ParamSpec(name="T", covariant=True, contravariant=True) + +# Non-errors. + +T = TypeVar("T") +T = TypeVar("T", covariant=False) +T = TypeVar("T", contravariant=False) +T = TypeVar("T", covariant=False, contravariant=False) +T = TypeVar("T", covariant=True) +T = TypeVar("T", covariant=True, contravariant=False) +T = TypeVar(name="T", covariant=True, contravariant=False) +T = TypeVar(name="T", covariant=True) +T = TypeVar("T", contravariant=True) +T = TypeVar("T", covariant=False, contravariant=True) +T = TypeVar(name="T", covariant=False, contravariant=True) +T = TypeVar(name="T", contravariant=True) + +T = ParamSpec("T") +T = ParamSpec("T", covariant=False) +T = ParamSpec("T", contravariant=False) +T = ParamSpec("T", covariant=False, contravariant=False) +T = ParamSpec("T", covariant=True) +T = ParamSpec("T", covariant=True, contravariant=False) +T = ParamSpec(name="T", covariant=True, contravariant=False) +T = ParamSpec(name="T", covariant=True) +T = ParamSpec("T", contravariant=True) +T = ParamSpec("T", covariant=False, contravariant=True) +T = ParamSpec(name="T", covariant=False, contravariant=True) +T = ParamSpec(name="T", contravariant=True) diff --git a/crates/ruff/resources/test/fixtures/pylint/type_name_incorrect_variance.py b/crates/ruff/resources/test/fixtures/pylint/type_name_incorrect_variance.py new file mode 100644 index 0000000000..f000a2ba0b --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pylint/type_name_incorrect_variance.py @@ -0,0 +1,68 @@ +from typing import ParamSpec, TypeVar + +# Errors. + +T = TypeVar("T", covariant=True) +T = TypeVar("T", covariant=True, contravariant=False) +T = TypeVar("T", contravariant=True) +T = TypeVar("T", covariant=False, contravariant=True) +P = ParamSpec("P", covariant=True) +P = ParamSpec("P", covariant=True, contravariant=False) +P = ParamSpec("P", contravariant=True) +P = ParamSpec("P", covariant=False, contravariant=True) + +T_co = TypeVar("T_co") +T_co = TypeVar("T_co", covariant=False) +T_co = TypeVar("T_co", contravariant=False) +T_co = TypeVar("T_co", covariant=False, contravariant=False) +T_co = TypeVar("T_co", contravariant=True) +T_co = TypeVar("T_co", covariant=False, contravariant=True) +P_co = ParamSpec("P_co") +P_co = ParamSpec("P_co", covariant=False) +P_co = ParamSpec("P_co", contravariant=False) +P_co = ParamSpec("P_co", covariant=False, contravariant=False) +P_co = ParamSpec("P_co", contravariant=True) +P_co = ParamSpec("P_co", covariant=False, contravariant=True) + +T_contra = TypeVar("T_contra") +T_contra = TypeVar("T_contra", covariant=False) +T_contra = TypeVar("T_contra", contravariant=False) +T_contra = TypeVar("T_contra", covariant=False, contravariant=False) +T_contra = TypeVar("T_contra", covariant=True) +T_contra = TypeVar("T_contra", covariant=True, contravariant=False) +P_contra = ParamSpec("P_contra") +P_contra = ParamSpec("P_contra", covariant=False) +P_contra = ParamSpec("P_contra", contravariant=False) +P_contra = ParamSpec("P_contra", covariant=False, contravariant=False) +P_contra = ParamSpec("P_contra", covariant=True) +P_contra = ParamSpec("P_contra", covariant=True, contravariant=False) + +# Non-errors. + +T = TypeVar("T") +T = TypeVar("T", covariant=False) +T = TypeVar("T", contravariant=False) +T = TypeVar("T", covariant=False, contravariant=False) +P = ParamSpec("P") +P = ParamSpec("P", covariant=False) +P = ParamSpec("P", contravariant=False) +P = ParamSpec("P", covariant=False, contravariant=False) + +T_co = TypeVar("T_co", covariant=True) +T_co = TypeVar("T_co", covariant=True, contravariant=False) +P_co = ParamSpec("P_co", covariant=True) +P_co = ParamSpec("P_co", covariant=True, contravariant=False) + +T_contra = TypeVar("T_contra", contravariant=True) +T_contra = TypeVar("T_contra", covariant=False, contravariant=True) +P_contra = ParamSpec("P_contra", contravariant=True) +P_contra = ParamSpec("P_contra", covariant=False, contravariant=True) + +# Bivariate types are errors, but not covered by this check. + +T = TypeVar("T", covariant=True, contravariant=True) +P = ParamSpec("P", covariant=True, contravariant=True) +T_co = TypeVar("T_co", covariant=True, contravariant=True) +P_co = ParamSpec("P_co", covariant=True, contravariant=True) +T_contra = TypeVar("T_contra", covariant=True, contravariant=True) +P_contra = ParamSpec("P_contra", covariant=True, contravariant=True) diff --git a/crates/ruff/resources/test/fixtures/pylint/type_param_name_mismatch.py b/crates/ruff/resources/test/fixtures/pylint/type_param_name_mismatch.py new file mode 100644 index 0000000000..267ae9a38b --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pylint/type_param_name_mismatch.py @@ -0,0 +1,56 @@ +from typing import TypeVar, ParamSpec, NewType, TypeVarTuple + +# Errors. + +X = TypeVar("T") +X = TypeVar(name="T") + +Y = ParamSpec("T") +Y = ParamSpec(name="T") + +Z = NewType("T", int) +Z = NewType(name="T", tp=int) + +Ws = TypeVarTuple("Ts") +Ws = TypeVarTuple(name="Ts") + +# Non-errors. + +T = TypeVar("T") +T = TypeVar(name="T") + +T = ParamSpec("T") +T = ParamSpec(name="T") + +T = NewType("T", int) +T = NewType(name="T", tp=int) + +Ts = TypeVarTuple("Ts") +Ts = TypeVarTuple(name="Ts") + +# Errors, but not covered by this rule. + +# Non-string literal name. +T = TypeVar(some_str) +T = TypeVar(name=some_str) +T = TypeVar(1) +T = TypeVar(name=1) +T = ParamSpec(some_str) +T = ParamSpec(name=some_str) +T = ParamSpec(1) +T = ParamSpec(name=1) +T = NewType(some_str, int) +T = NewType(name=some_str, tp=int) +T = NewType(1, int) +T = NewType(name=1, tp=int) +Ts = TypeVarTuple(some_str) +Ts = TypeVarTuple(name=some_str) +Ts = TypeVarTuple(1) +Ts = TypeVarTuple(name=1) + +# No names provided. +T = TypeVar() +T = ParamSpec() +T = NewType() +T = NewType(tp=int) +Ts = TypeVarTuple() diff --git a/crates/ruff/resources/test/fixtures/pyproject.toml b/crates/ruff/resources/test/fixtures/pyproject.toml index 950e30dfb8..5525b69dea 100644 --- a/crates/ruff/resources/test/fixtures/pyproject.toml +++ b/crates/ruff/resources/test/fixtures/pyproject.toml @@ -1,53 +1,8 @@ [tool.ruff] -allowed-confusables = ["−", "ρ", "∗"] line-length = 88 extend-exclude = [ "excluded_file.py", "migrations", "with_excluded_file/other_excluded_file.py", ] -external = ["V101"] per-file-ignores = { "__init__.py" = ["F401"] } - -[tool.ruff.flake8-bugbear] -extend-immutable-calls = ["fastapi.Depends", "fastapi.Query"] - -[tool.ruff.flake8-builtins] -builtins-ignorelist = ["id", "dir"] - -[tool.ruff.flake8-quotes] -inline-quotes = "single" -multiline-quotes = "double" -docstring-quotes = "double" -avoid-escape = true - -[tool.ruff.mccabe] -max-complexity = 10 - -[tool.ruff.pep8-naming] -classmethod-decorators = ["pydantic.validator"] - -[tool.ruff.flake8-tidy-imports] -ban-relative-imports = "parents" - -[tool.ruff.flake8-tidy-imports.banned-api] -"cgi".msg = "The cgi module is deprecated." -"typing.TypedDict".msg = "Use typing_extensions.TypedDict instead." - -[tool.ruff.flake8-errmsg] -max-string-length = 20 - -[tool.ruff.flake8-import-conventions.aliases] -pandas = "pd" - -[tool.ruff.flake8-import-conventions.extend-aliases] -"dask.dataframe" = "dd" - -[tool.ruff.flake8-pytest-style] -fixture-parentheses = false -parametrize-names-type = "csv" -parametrize-values-type = "tuple" -parametrize-values-row-type = "list" -raises-require-match-for = ["Exception", "TypeError", "KeyError"] -raises-extend-require-match-for = ["requests.RequestException"] -mark-parentheses = false diff --git a/crates/ruff/resources/test/fixtures/pyupgrade/UP004.py b/crates/ruff/resources/test/fixtures/pyupgrade/UP004.py index a114f15017..723733f938 100644 --- a/crates/ruff/resources/test/fixtures/pyupgrade/UP004.py +++ b/crates/ruff/resources/test/fixtures/pyupgrade/UP004.py @@ -134,6 +134,19 @@ class A( ... +class A(object, object): + ... + + +@decorator() +class A(object): + ... + +@decorator() # class A(object): +class A(object): + ... + + object = A diff --git a/crates/ruff/resources/test/fixtures/pyupgrade/UP007.py b/crates/ruff/resources/test/fixtures/pyupgrade/UP007.py index 56591e565c..287652b030 100644 --- a/crates/ruff/resources/test/fixtures/pyupgrade/UP007.py +++ b/crates/ruff/resources/test/fixtures/pyupgrade/UP007.py @@ -27,6 +27,14 @@ def f(x: typing.Union[(str, int), float]) -> None: ... +def f(x: typing.Union[(int,)]) -> None: + ... + + +def f(x: typing.Union[()]) -> None: + ... + + def f(x: "Union[str, int, Union[float, bytes]]") -> None: ... diff --git a/crates/ruff/resources/test/fixtures/pyupgrade/UP012.py b/crates/ruff/resources/test/fixtures/pyupgrade/UP012.py index 266e8431cc..879f3842ad 100644 --- a/crates/ruff/resources/test/fixtures/pyupgrade/UP012.py +++ b/crates/ruff/resources/test/fixtures/pyupgrade/UP012.py @@ -70,3 +70,8 @@ print("foo".encode()) # print(b"foo") "abc" "def" )).encode() + +(f"foo{bar}").encode("utf-8") +(f"foo{bar}").encode(encoding="utf-8") +("unicode text©").encode("utf-8") +("unicode text©").encode(encoding="utf-8") diff --git a/crates/ruff/resources/test/fixtures/pyupgrade/UP032_0.py b/crates/ruff/resources/test/fixtures/pyupgrade/UP032_0.py index c45821bc96..fbdeb1dd7e 100644 --- a/crates/ruff/resources/test/fixtures/pyupgrade/UP032_0.py +++ b/crates/ruff/resources/test/fixtures/pyupgrade/UP032_0.py @@ -54,6 +54,14 @@ print("foo {} ".format(x)) '''{[b]}'''.format(a) +"{}".format( + 1 +) + +"123456789 {}".format( + 1111111111111111111111111111111111111111111111111111111111111111111111111, +) + ### # Non-errors ### @@ -87,6 +95,9 @@ r'"\N{snowman} {}".format(a)' "{a}" "{b}".format(a=1, b=1) +"123456789 {}".format( + 11111111111111111111111111111111111111111111111111111111111111111111111111, +) async def c(): return "{}".format(await 3) diff --git a/crates/ruff/resources/test/fixtures/pyupgrade/UP035.py b/crates/ruff/resources/test/fixtures/pyupgrade/UP035.py index aefd7064d1..a185c4867f 100644 --- a/crates/ruff/resources/test/fixtures/pyupgrade/UP035.py +++ b/crates/ruff/resources/test/fixtures/pyupgrade/UP035.py @@ -48,3 +48,12 @@ if True: from collections import ( # OK from a import b + +# Ok: `typing_extensions` contains backported improvements. +from typing_extensions import SupportsIndex + +# Ok: `typing_extensions` contains backported improvements. +from typing_extensions import NamedTuple + +# Ok: `typing_extensions` supports `frozen_default` (backported from 3.12). +from typing_extensions import dataclass_transform diff --git a/crates/ruff/resources/test/fixtures/pyupgrade/UP039.py b/crates/ruff/resources/test/fixtures/pyupgrade/UP039.py new file mode 100644 index 0000000000..ba44e1deb9 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pyupgrade/UP039.py @@ -0,0 +1,43 @@ +# Errors +class A(): + pass + + +class A() \ + : + pass + + +class A \ + (): + pass + + +@decorator() +class A(): + pass + +@decorator +class A(): + pass + +# OK +class A: + pass + + +class A(A): + pass + + +class A(metaclass=type): + pass + + +@decorator() +class A: + pass + +@decorator +class A: + pass diff --git a/crates/ruff/resources/test/fixtures/ruff/RUF008.py b/crates/ruff/resources/test/fixtures/ruff/RUF008.py index 3a40f7f094..978b88b0c7 100644 --- a/crates/ruff/resources/test/fixtures/ruff/RUF008.py +++ b/crates/ruff/resources/test/fixtures/ruff/RUF008.py @@ -5,12 +5,11 @@ from typing import ClassVar, Sequence KNOWINGLY_MUTABLE_DEFAULT = [] -@dataclass() +@dataclass class A: mutable_default: list[int] = [] immutable_annotation: typing.Sequence[int] = [] without_annotation = [] - ignored_via_comment: list[int] = [] # noqa: RUF008 correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT perfectly_fine: list[int] = field(default_factory=list) class_variable: typing.ClassVar[list[int]] = [] @@ -21,7 +20,6 @@ class B: mutable_default: list[int] = [] immutable_annotation: Sequence[int] = [] without_annotation = [] - ignored_via_comment: list[int] = [] # noqa: RUF008 correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT perfectly_fine: list[int] = field(default_factory=list) class_variable: ClassVar[list[int]] = [] diff --git a/crates/ruff/resources/test/fixtures/ruff/RUF009.py b/crates/ruff/resources/test/fixtures/ruff/RUF009.py index 3ba1aad6be..f1cc836ffc 100644 --- a/crates/ruff/resources/test/fixtures/ruff/RUF009.py +++ b/crates/ruff/resources/test/fixtures/ruff/RUF009.py @@ -6,6 +6,7 @@ from fractions import Fraction from pathlib import Path from typing import ClassVar, NamedTuple + def default_function() -> list[int]: return [] @@ -25,12 +26,13 @@ class A: fine_timedelta: datetime.timedelta = datetime.timedelta(hours=7) fine_tuple: tuple[int] = tuple([1]) fine_regex: re.Pattern = re.compile(r".*") - fine_float: float = float('-inf') + fine_float: float = float("-inf") fine_int: int = int(12) fine_complex: complex = complex(1, 2) fine_str: str = str("foo") fine_bool: bool = bool("foo") - fine_fraction: Fraction = Fraction(1,2) + fine_fraction: Fraction = Fraction(1, 2) + DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES = ImmutableType(40) DEFAULT_A_FOR_ALL_DATACLASSES = A([1, 2, 3]) @@ -45,3 +47,25 @@ class B: okay_variant: A = DEFAULT_A_FOR_ALL_DATACLASSES fine_dataclass_function: list[int] = field(default_factory=list) + + +class IntConversionDescriptor: + def __init__(self, *, default): + self._default = default + + def __set_name__(self, owner, name): + self._name = "_" + name + + def __get__(self, obj, type): + if obj is None: + return self._default + + return getattr(obj, self._name, self._default) + + def __set__(self, obj, value): + setattr(obj, self._name, int(value)) + + +@dataclass +class InventoryItem: + quantity_on_hand: IntConversionDescriptor = IntConversionDescriptor(default=100) diff --git a/crates/ruff/resources/test/fixtures/ruff/RUF010.py b/crates/ruff/resources/test/fixtures/ruff/RUF010.py index 6416b79c9e..031e08412f 100644 --- a/crates/ruff/resources/test/fixtures/ruff/RUF010.py +++ b/crates/ruff/resources/test/fixtures/ruff/RUF010.py @@ -36,10 +36,5 @@ f"{ascii(bla)}" # OK ) -f"{str(bla)}" # RUF010 - -f"{str(bla):20}" # RUF010 - -f"{bla!s}" # RUF010 - -f"{bla!s:20}" # OK +# OK +f"{str({})}" diff --git a/crates/ruff/resources/test/fixtures/ruff/RUF012.py b/crates/ruff/resources/test/fixtures/ruff/RUF012.py new file mode 100644 index 0000000000..9be4b88c76 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/ruff/RUF012.py @@ -0,0 +1,37 @@ +from typing import ClassVar, Sequence, Final + + +class A: + __slots__ = { + "mutable_default": "A mutable default value", + } + + mutable_default: list[int] = [] + immutable_annotation: Sequence[int] = [] + without_annotation = [] + class_variable: ClassVar[list[int]] = [] + final_variable: Final[list[int]] = [] + + +from dataclasses import dataclass, field + + +@dataclass +class C: + mutable_default: list[int] = [] + immutable_annotation: Sequence[int] = [] + without_annotation = [] + perfectly_fine: list[int] = field(default_factory=list) + class_variable: ClassVar[list[int]] = [] + final_variable: Final[list[int]] = [] + + +from pydantic import BaseModel + + +class D(BaseModel): + mutable_default: list[int] = [] + immutable_annotation: Sequence[int] = [] + without_annotation = [] + class_variable: ClassVar[list[int]] = [] + final_variable: Final[list[int]] = [] diff --git a/crates/ruff/resources/test/fixtures/ruff/RUF013_0.py b/crates/ruff/resources/test/fixtures/ruff/RUF013_0.py new file mode 100644 index 0000000000..21c5908979 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/ruff/RUF013_0.py @@ -0,0 +1,243 @@ +import typing +from typing import Annotated, Any, Literal, Optional, Tuple, Union + + +def f(arg: int): + pass + + +def f(arg=None): + pass + + +def f(arg: Any = None): + pass + + +def f(arg: object = None): + pass + + +def f(arg: int = None): # RUF013 + pass + + +def f(arg: str = None): # RUF013 + pass + + +def f(arg: typing.List[str] = None): # RUF013 + pass + + +def f(arg: Tuple[str] = None): # RUF013 + pass + + +# Optional + + +def f(arg: Optional[int] = None): + pass + + +def f(arg: typing.Optional[int] = None): + pass + + +# Union + + +def f(arg: Union[None, int] = None): + pass + + +def f(arg: Union[str, None] = None): + pass + + +def f(arg: typing.Union[int, str, None] = None): + pass + + +def f(arg: Union[int, str, Any] = None): + pass + + +def f(arg: Union = None): # RUF013 + pass + + +def f(arg: Union[int, str] = None): # RUF013 + pass + + +def f(arg: typing.Union[int, str] = None): # RUF013 + pass + + +# PEP 604 Union + + +def f(arg: None | int = None): + pass + + +def f(arg: int | None = None): + pass + + +def f(arg: int | float | str | None = None): + pass + + +def f(arg: int | float = None): # RUF013 + pass + + +def f(arg: int | float | str | bytes = None): # RUF013 + pass + + +# Literal + + +def f(arg: None = None): + pass + + +def f(arg: Literal[1, 2, None, 3] = None): + pass + + +def f(arg: Literal[1, "foo"] = None): # RUF013 + pass + + +def f(arg: typing.Literal[1, "foo", True] = None): # RUF013 + pass + + +# Annotated + + +def f(arg: Annotated[Optional[int], ...] = None): + pass + + +def f(arg: Annotated[Union[int, None], ...] = None): + pass + + +def f(arg: Annotated[Any, ...] = None): + pass + + +def f(arg: Annotated[int, ...] = None): # RUF013 + pass + + +def f(arg: Annotated[Annotated[int | str, ...], ...] = None): # RUF013 + pass + + +# Multiple arguments + + +def f( + arg1: Optional[int] = None, + arg2: Union[int, None] = None, + arg3: Literal[1, 2, None, 3] = None, +): + pass + + +def f( + arg1: int = None, # RUF013 + arg2: Union[int, float] = None, # RUF013 + arg3: Literal[1, 2, 3] = None, # RUF013 +): + pass + + +# Nested + + +def f(arg: Literal[1, "foo", Literal[True, None]] = None): + pass + + +def f(arg: Union[int, Union[float, Union[str, None]]] = None): + pass + + +def f(arg: Union[int, Union[float, Optional[str]]] = None): + pass + + +def f(arg: Union[int, Literal[True, None]] = None): + pass + + +def f(arg: Union[Annotated[int, ...], Annotated[Optional[float], ...]] = None): + pass + + +def f(arg: Union[Annotated[int, ...], Union[str, bytes]] = None): # RUF013 + pass + + +# Quoted + + +def f(arg: "int" = None): # RUF013 + pass + + +def f(arg: "str" = None): # RUF013 + pass + + +def f(arg: "st" "r" = None): # RUF013 + pass + + +def f(arg: "Optional[int]" = None): + pass + + +def f(arg: Union["int", "str"] = None): # RUF013 + pass + + +def f(arg: Union["int", "None"] = None): + pass + + +def f(arg: Union["No" "ne", "int"] = None): + pass + + +# Avoid flagging when there's a parse error in the forward reference +def f(arg: Union["<>", "int"] = None): + pass + + +# Type aliases + +Text = str | bytes + + +def f(arg: Text = None): + pass + + +def f(arg: "Text" = None): + pass + + +from custom_typing import MaybeInt + + +def f(arg: MaybeInt = None): + pass diff --git a/crates/ruff/resources/test/fixtures/ruff/RUF013_1.py b/crates/ruff/resources/test/fixtures/ruff/RUF013_1.py new file mode 100644 index 0000000000..e270aaf3d8 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/ruff/RUF013_1.py @@ -0,0 +1,5 @@ +# No `typing.Optional` import + + +def f(arg: int = None): # RUF011 + pass diff --git a/crates/ruff/resources/test/fixtures/ruff/RUF014.py b/crates/ruff/resources/test/fixtures/ruff/RUF014.py new file mode 100644 index 0000000000..d1ae40f3ca --- /dev/null +++ b/crates/ruff/resources/test/fixtures/ruff/RUF014.py @@ -0,0 +1,185 @@ +def after_return(): + return "reachable" + return "unreachable" + +async def also_works_on_async_functions(): + return "reachable" + return "unreachable" + +def if_always_true(): + if True: + return "reachable" + return "unreachable" + +def if_always_false(): + if False: + return "unreachable" + return "reachable" + +def if_elif_always_false(): + if False: + return "unreachable" + elif False: + return "also unreachable" + return "reachable" + +def if_elif_always_true(): + if False: + return "unreachable" + elif True: + return "reachable" + return "also unreachable" + +def ends_with_if(): + if False: + return "unreachable" + else: + return "reachable" + +def infinite_loop(): + while True: + continue + return "unreachable" + +''' TODO: we could determine these, but we don't yet. +def for_range_return(): + for i in range(10): + if i == 5: + return "reachable" + return "unreachable" + +def for_range_else(): + for i in range(111): + if i == 5: + return "reachable" + else: + return "unreachable" + return "also unreachable" + +def for_range_break(): + for i in range(13): + return "reachable" + return "unreachable" + +def for_range_if_break(): + for i in range(1110): + if True: + return "reachable" + return "unreachable" +''' + +def match_wildcard(status): + match status: + case _: + return "reachable" + return "unreachable" + +def match_case_and_wildcard(status): + match status: + case 1: + return "reachable" + case _: + return "reachable" + return "unreachable" + +def raise_exception(): + raise Exception + return "unreachable" + +def while_false(): + while False: + return "unreachable" + return "reachable" + +def while_false_else(): + while False: + return "unreachable" + else: + return "reachable" + +def while_false_else_return(): + while False: + return "unreachable" + else: + return "reachable" + return "also unreachable" + +def while_true(): + while True: + return "reachable" + return "unreachable" + +def while_true_else(): + while True: + return "reachable" + else: + return "unreachable" + +def while_true_else_return(): + while True: + return "reachable" + else: + return "unreachable" + return "also unreachable" + +def while_false_var_i(): + i = 0 + while False: + i += 1 + return i + +def while_true_var_i(): + i = 0 + while True: + i += 1 + return i + +def while_infinite(): + while True: + pass + return "unreachable" + +def while_if_true(): + while True: + if True: + return "reachable" + return "unreachable" + +# Test case found in the Bokeh repository that trigger a false positive. +def bokeh1(self, obj: BytesRep) -> bytes: + data = obj["data"] + + if isinstance(data, str): + return base64.b64decode(data) + elif isinstance(data, Buffer): + buffer = data + else: + id = data["id"] + + if id in self._buffers: + buffer = self._buffers[id] + else: + self.error(f"can't resolve buffer '{id}'") + + return buffer.data + +''' +TODO: because `try` statements aren't handled this triggers a false positive as +the last statement is reached, but the rules thinks it isn't (it doesn't +see/process the break statement). + +# Test case found in the Bokeh repository that trigger a false positive. +def bokeh2(self, host: str = DEFAULT_HOST, port: int = DEFAULT_PORT) -> None: + self.stop_serving = False + while True: + try: + self.server = HTTPServer((host, port), HtmlOnlyHandler) + self.host = host + self.port = port + break + except OSError: + log.debug(f"port {port} is in use, trying to next one") + port += 1 + + self.thread = threading.Thread(target=self._run_web_server) +''' diff --git a/crates/ruff/resources/test/fixtures/ruff/RUF015.py b/crates/ruff/resources/test/fixtures/ruff/RUF015.py new file mode 100644 index 0000000000..e19602de8d --- /dev/null +++ b/crates/ruff/resources/test/fixtures/ruff/RUF015.py @@ -0,0 +1,44 @@ +x = range(10) + +# RUF015 +list(x)[0] +list(x)[:1] +list(x)[:1:1] +list(x)[:1:2] +tuple(x)[0] +tuple(x)[:1] +tuple(x)[:1:1] +tuple(x)[:1:2] +list(i for i in x)[0] +list(i for i in x)[:1] +list(i for i in x)[:1:1] +list(i for i in x)[:1:2] +[i for i in x][0] +[i for i in x][:1] +[i for i in x][:1:1] +[i for i in x][:1:2] + +# OK (not indexing (solely) the first element) +list(x) +list(x)[1] +list(x)[-1] +list(x)[1:] +list(x)[:3:2] +list(x)[::2] +list(x)[::] +[i for i in x] +[i for i in x][1] +[i for i in x][-1] +[i for i in x][1:] +[i for i in x][:3:2] +[i for i in x][::2] +[i for i in x][::] + +# OK (doesn't mirror the underlying list) +[i + 1 for i in x][0] +[i for i in x if i > 5][0] +[(i, i + 1) for i in x][0] + +# OK (multiple generators) +y = range(10) +[i + j for i in x for j in y][0] diff --git a/crates/ruff/resources/test/fixtures/ruff/RUF016.py b/crates/ruff/resources/test/fixtures/ruff/RUF016.py new file mode 100644 index 0000000000..545ad2ec53 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/ruff/RUF016.py @@ -0,0 +1,115 @@ +# Should not emit for valid access with index +var = "abc"[0] +var = f"abc"[0] +var = [1, 2, 3][0] +var = (1, 2, 3)[0] +var = b"abc"[0] + +# Should not emit for valid access with slice +var = "abc"[0:2] +var = f"abc"[0:2] +var = b"abc"[0:2] +var = [1, 2, 3][0:2] +var = (1, 2, 3)[0:2] +var = [1, 2, 3][None:2] +var = [1, 2, 3][0:None] +var = [1, 2, 3][:2] +var = [1, 2, 3][0:] + +# Should emit for invalid access on strings +var = "abc"["x"] +var = f"abc"["x"] + +# Should emit for invalid access on bytes +var = b"abc"["x"] + +# Should emit for invalid access on lists and tuples +var = [1, 2, 3]["x"] +var = (1, 2, 3)["x"] + +# Should emit for invalid access on list comprehensions +var = [x for x in range(10)]["x"] + +# Should emit for invalid access using tuple +var = "abc"[1, 2] + +# Should emit for invalid access using string +var = [1, 2]["x"] + +# Should emit for invalid access using float +var = [1, 2][0.25] + +# Should emit for invalid access using dict +var = [1, 2][{"x": "y"}] + +# Should emit for invalid access using dict comp +var = [1, 2][{x: "y" for x in range(2)}] + +# Should emit for invalid access using list +var = [1, 2][2, 3] + +# Should emit for invalid access using list comp +var = [1, 2][[x for x in range(2)]] + +# Should emit on invalid access using set +var = [1, 2][{"x", "y"}] + +# Should emit on invalid access using set comp +var = [1, 2][{x for x in range(2)}] + +# Should emit on invalid access using bytes +var = [1, 2][b"x"] + +# Should emit for non-integer slice start +var = [1, 2, 3]["x":2] +var = [1, 2, 3][f"x":2] +var = [1, 2, 3][1.2:2] +var = [1, 2, 3][{"x"}:2] +var = [1, 2, 3][{x for x in range(2)}:2] +var = [1, 2, 3][{"x": x for x in range(2)}:2] +var = [1, 2, 3][[x for x in range(2)]:2] + +# Should emit for non-integer slice end +var = [1, 2, 3][0:"x"] +var = [1, 2, 3][0:f"x"] +var = [1, 2, 3][0:1.2] +var = [1, 2, 3][0:{"x"}] +var = [1, 2, 3][0:{x for x in range(2)}] +var = [1, 2, 3][0:{"x": x for x in range(2)}] +var = [1, 2, 3][0:[x for x in range(2)]] + +# Should emit for non-integer slice step +var = [1, 2, 3][0:1:"x"] +var = [1, 2, 3][0:1:f"x"] +var = [1, 2, 3][0:1:1.2] +var = [1, 2, 3][0:1:{"x"}] +var = [1, 2, 3][0:1:{x for x in range(2)}] +var = [1, 2, 3][0:1:{"x": x for x in range(2)}] +var = [1, 2, 3][0:1:[x for x in range(2)]] + +# Should emit for non-integer slice start and end; should emit twice with specific ranges +var = [1, 2, 3]["x":"y"] + +# Should emit once for repeated invalid access +var = [1, 2, 3]["x"]["y"]["z"] + +# Cannot emit on invalid access using variable in index +x = "x" +var = "abc"[x] + +# Cannot emit on invalid access using call +def func(): + return 1 +var = "abc"[func()] + +# Cannot emit on invalid access using a variable in parent +x = [1, 2, 3] +var = x["y"] + +# Cannot emit for invalid access on byte array +var = bytearray(b"abc")["x"] + +# Cannot emit for slice bound using variable +x = "x" +var = [1, 2, 3][0:x] +var = [1, 2, 3][x:1] diff --git a/crates/ruff/resources/test/fixtures/tryceratops/TRY301.py b/crates/ruff/resources/test/fixtures/tryceratops/TRY301.py index da7c82e487..00544aa856 100644 --- a/crates/ruff/resources/test/fixtures/tryceratops/TRY301.py +++ b/crates/ruff/resources/test/fixtures/tryceratops/TRY301.py @@ -57,3 +57,10 @@ def fine(): a = process() # This throws the exception now finally: print("finally") + + +def fine(): + try: + raise ValueError("a doesn't exist") + except TypeError: # A different exception is caught + print("A different exception is caught") diff --git a/crates/ruff/src/autofix/edits.rs b/crates/ruff/src/autofix/edits.rs index 623e1876ed..8526815d2a 100644 --- a/crates/ruff/src/autofix/edits.rs +++ b/crates/ruff/src/autofix/edits.rs @@ -1,8 +1,8 @@ //! Interface for generating autofix edits from higher-level actions (e.g., "remove an argument"). use anyhow::{bail, Result}; use ruff_text_size::{TextLen, TextRange, TextSize}; -use rustpython_parser::ast::{self, Excepthandler, Expr, Keyword, Ranged, Stmt}; -use rustpython_parser::{lexer, Mode, Tok}; +use rustpython_parser::ast::{self, ExceptHandler, Expr, Keyword, Ranged, Stmt}; +use rustpython_parser::{lexer, Mode}; use ruff_diagnostics::Edit; use ruff_python_ast::helpers; @@ -29,7 +29,6 @@ pub(crate) fn delete_stmt( parent: Option<&Stmt>, locator: &Locator, indexer: &Indexer, - stylist: &Stylist, ) -> Edit { if parent .map(|parent| is_lone_child(stmt, parent)) @@ -39,18 +38,15 @@ pub(crate) fn delete_stmt( // it with a `pass`. Edit::range_replacement("pass".to_string(), stmt.range()) } else { - if let Some(semicolon) = trailing_semicolon(stmt, locator) { + if let Some(semicolon) = trailing_semicolon(stmt.end(), locator) { let next = next_stmt_break(semicolon, locator); Edit::deletion(stmt.start(), next) - } else if helpers::has_leading_content(stmt, locator) { + } else if helpers::has_leading_content(stmt.start(), locator) { Edit::range_deletion(stmt.range()) - } else if helpers::preceded_by_continuation(stmt, indexer, locator) { - if is_end_of_file(stmt, locator) && locator.is_at_start_of_line(stmt.start()) { - // Special-case: a file can't end in a continuation. - Edit::range_replacement(stylist.line_ending().to_string(), stmt.range()) - } else { - Edit::range_deletion(stmt.range()) - } + } else if let Some(start) = + helpers::preceded_by_continuations(stmt.start(), locator, indexer) + { + Edit::range_deletion(TextRange::new(start, stmt.end())) } else { let range = locator.full_lines_range(stmt.range()); Edit::range_deletion(range) @@ -64,11 +60,11 @@ pub(crate) fn remove_unused_imports<'a>( stmt: &Stmt, parent: Option<&Stmt>, locator: &Locator, - indexer: &Indexer, stylist: &Stylist, + indexer: &Indexer, ) -> Result { match codemods::remove_imports(unused_imports, stmt, locator, stylist)? { - None => Ok(delete_stmt(stmt, parent, locator, indexer, stylist)), + None => Ok(delete_stmt(stmt, parent, locator, indexer)), Some(content) => Ok(Edit::range_replacement(content, stmt.range())), } } @@ -102,7 +98,7 @@ pub(crate) fn remove_argument( // Case 1: there is only one argument. let mut count = 0u32; for (tok, range) in lexer::lex_starts_at(contents, Mode::Module, call_at).flatten() { - if matches!(tok, Tok::Lpar) { + if tok.is_lpar() { if count == 0 { fix_start = Some(if remove_parentheses { range.start() @@ -113,7 +109,7 @@ pub(crate) fn remove_argument( count = count.saturating_add(1); } - if matches!(tok, Tok::Rpar) { + if tok.is_rpar() { count = count.saturating_sub(1); if count == 0 { fix_end = Some(if remove_parentheses { @@ -135,11 +131,11 @@ pub(crate) fn remove_argument( let mut seen_comma = false; for (tok, range) in lexer::lex_starts_at(contents, Mode::Module, call_at).flatten() { if seen_comma { - if matches!(tok, Tok::NonLogicalNewline) { + if tok.is_non_logical_newline() { // Also delete any non-logical newlines after the comma. continue; } - fix_end = Some(if matches!(tok, Tok::Newline) { + fix_end = Some(if tok.is_newline() { range.end() } else { range.start() @@ -149,7 +145,7 @@ pub(crate) fn remove_argument( if range.start() == expr_range.start() { fix_start = Some(range.start()); } - if fix_start.is_some() && matches!(tok, Tok::Comma) { + if fix_start.is_some() && tok.is_comma() { seen_comma = true; } } @@ -161,7 +157,7 @@ pub(crate) fn remove_argument( fix_end = Some(expr_range.end()); break; } - if matches!(tok, Tok::Comma) { + if tok.is_comma() { fix_start = Some(range.start()); } } @@ -218,7 +214,7 @@ fn is_lone_child(child: &Stmt, parent: &Stmt) -> bool { || is_only(orelse, child) || is_only(finalbody, child) || handlers.iter().any(|handler| match handler { - Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { + ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { body, .. }) => is_only(body, child), }) @@ -238,15 +234,15 @@ fn is_lone_child(child: &Stmt, parent: &Stmt) -> bool { /// Return the location of a trailing semicolon following a `Stmt`, if it's part /// of a multi-statement line. -fn trailing_semicolon(stmt: &Stmt, locator: &Locator) -> Option { - let contents = locator.after(stmt.end()); +fn trailing_semicolon(offset: TextSize, locator: &Locator) -> Option { + let contents = locator.after(offset); for line in NewlineWithTrailingNewline::from(contents) { let trimmed = line.trim_whitespace_start(); if trimmed.starts_with(';') { let colon_offset = line.text_len() - trimmed.text_len(); - return Some(stmt.end() + line.start() + colon_offset); + return Some(offset + line.start() + colon_offset); } if !trimmed.starts_with('\\') { @@ -284,16 +280,11 @@ fn next_stmt_break(semicolon: TextSize, locator: &Locator) -> TextSize { locator.line_end(start_location) } -/// Return `true` if a `Stmt` occurs at the end of a file. -fn is_end_of_file(stmt: &Stmt, locator: &Locator) -> bool { - stmt.end() == locator.contents().text_len() -} - #[cfg(test)] mod tests { use anyhow::Result; use ruff_text_size::TextSize; - use rustpython_parser::ast::Suite; + use rustpython_parser::ast::{Ranged, Suite}; use rustpython_parser::Parse; use ruff_python_ast::source_code::Locator; @@ -306,29 +297,38 @@ mod tests { let program = Suite::parse(contents, "")?; let stmt = program.first().unwrap(); let locator = Locator::new(contents); - assert_eq!(trailing_semicolon(stmt, &locator), None); + assert_eq!(trailing_semicolon(stmt.end(), &locator), None); let contents = "x = 1; y = 1"; let program = Suite::parse(contents, "")?; let stmt = program.first().unwrap(); let locator = Locator::new(contents); - assert_eq!(trailing_semicolon(stmt, &locator), Some(TextSize::from(5))); + assert_eq!( + trailing_semicolon(stmt.end(), &locator), + Some(TextSize::from(5)) + ); let contents = "x = 1 ; y = 1"; let program = Suite::parse(contents, "")?; let stmt = program.first().unwrap(); let locator = Locator::new(contents); - assert_eq!(trailing_semicolon(stmt, &locator), Some(TextSize::from(6))); + assert_eq!( + trailing_semicolon(stmt.end(), &locator), + Some(TextSize::from(6)) + ); - let contents = r#" + let contents = r" x = 1 \ ; y = 1 -"# +" .trim(); let program = Suite::parse(contents, "")?; let stmt = program.first().unwrap(); let locator = Locator::new(contents); - assert_eq!(trailing_semicolon(stmt, &locator), Some(TextSize::from(10))); + assert_eq!( + trailing_semicolon(stmt.end(), &locator), + Some(TextSize::from(10)) + ); Ok(()) } @@ -349,10 +349,10 @@ x = 1 \ TextSize::from(6) ); - let contents = r#" + let contents = r" x = 1 \ ; y = 1 -"# +" .trim(); let locator = Locator::new(contents); assert_eq!( diff --git a/crates/ruff/src/autofix/mod.rs b/crates/ruff/src/autofix/mod.rs index df4fe0e69c..a666171b54 100644 --- a/crates/ruff/src/autofix/mod.rs +++ b/crates/ruff/src/autofix/mod.rs @@ -2,23 +2,31 @@ use std::collections::BTreeSet; use itertools::Itertools; use nohash_hasher::IntSet; -use ruff_text_size::{TextRange, TextSize}; +use ruff_text_size::{TextLen, TextRange, TextSize}; use rustc_hash::FxHashMap; use ruff_diagnostics::{Diagnostic, Edit, Fix, IsolationLevel}; use ruff_python_ast::source_code::Locator; +use crate::autofix::source_map::SourceMap; use crate::linter::FixTable; use crate::registry::{AsRule, Rule}; pub(crate) mod codemods; pub(crate) mod edits; +pub(crate) mod source_map; + +pub(crate) struct FixResult { + /// The resulting source code, after applying all fixes. + pub(crate) code: String, + /// The number of fixes applied for each [`Rule`]. + pub(crate) fixes: FixTable, + /// Source map for the fixed source code. + pub(crate) source_map: SourceMap, +} /// Auto-fix errors in a file, and write the fixed source code to disk. -pub(crate) fn fix_file( - diagnostics: &[Diagnostic], - locator: &Locator, -) -> Option<(String, FixTable)> { +pub(crate) fn fix_file(diagnostics: &[Diagnostic], locator: &Locator) -> Option { let mut with_fixes = diagnostics .iter() .filter(|diag| diag.fix.is_some()) @@ -35,12 +43,13 @@ pub(crate) fn fix_file( fn apply_fixes<'a>( diagnostics: impl Iterator, locator: &'a Locator<'a>, -) -> (String, FixTable) { +) -> FixResult { let mut output = String::with_capacity(locator.len()); let mut last_pos: Option = None; let mut applied: BTreeSet<&Edit> = BTreeSet::default(); let mut isolated: IntSet = IntSet::default(); let mut fixed = FxHashMap::default(); + let mut source_map = SourceMap::default(); for (rule, fix) in diagnostics .filter_map(|diagnostic| { @@ -84,9 +93,15 @@ fn apply_fixes<'a>( let slice = locator.slice(TextRange::new(last_pos.unwrap_or_default(), edit.start())); output.push_str(slice); + // Add the start source marker for the patch. + source_map.push_start_marker(edit, output.text_len()); + // Add the patch itself. output.push_str(edit.content().unwrap_or_default()); + // Add the end source marker for the added patch. + source_map.push_end_marker(edit, output.text_len()); + // Track that the edit was applied. last_pos = Some(edit.end()); applied.insert(edit); @@ -99,7 +114,11 @@ fn apply_fixes<'a>( let slice = locator.after(last_pos.unwrap_or_default()); output.push_str(slice); - (output, fixed) + FixResult { + code: output, + fixes: fixed, + source_map, + } } /// Compare two fixes. @@ -130,7 +149,8 @@ mod tests { use ruff_diagnostics::Fix; use ruff_python_ast::source_code::Locator; - use crate::autofix::apply_fixes; + use crate::autofix::source_map::SourceMarker; + use crate::autofix::{apply_fixes, FixResult}; use crate::rules::pycodestyle::rules::MissingNewlineAtEndOfFile; #[allow(deprecated)] @@ -150,9 +170,59 @@ mod tests { fn empty_file() { let locator = Locator::new(r#""#); let diagnostics = create_diagnostics([]); - let (contents, fixed) = apply_fixes(diagnostics.iter(), &locator); - assert_eq!(contents, ""); - assert_eq!(fixed.values().sum::(), 0); + let FixResult { + code, + fixes, + source_map, + } = apply_fixes(diagnostics.iter(), &locator); + assert_eq!(code, ""); + assert_eq!(fixes.values().sum::(), 0); + assert!(source_map.markers().is_empty()); + } + + #[test] + fn apply_one_insertion() { + let locator = Locator::new( + r#" +import os + +print("hello world") +"# + .trim(), + ); + let diagnostics = create_diagnostics([Edit::insertion( + "import sys\n".to_string(), + TextSize::new(10), + )]); + let FixResult { + code, + fixes, + source_map, + } = apply_fixes(diagnostics.iter(), &locator); + assert_eq!( + code, + r#" +import os +import sys + +print("hello world") +"# + .trim() + ); + assert_eq!(fixes.values().sum::(), 1); + assert_eq!( + source_map.markers(), + &[ + SourceMarker { + source: 10.into(), + dest: 10.into(), + }, + SourceMarker { + source: 10.into(), + dest: 21.into(), + }, + ] + ); } #[test] @@ -169,16 +239,33 @@ class A(object): TextSize::new(8), TextSize::new(14), )]); - let (contents, fixed) = apply_fixes(diagnostics.iter(), &locator); + let FixResult { + code, + fixes, + source_map, + } = apply_fixes(diagnostics.iter(), &locator); assert_eq!( - contents, + code, r#" class A(Bar): ... "# .trim(), ); - assert_eq!(fixed.values().sum::(), 1); + assert_eq!(fixes.values().sum::(), 1); + assert_eq!( + source_map.markers(), + &[ + SourceMarker { + source: 8.into(), + dest: 8.into(), + }, + SourceMarker { + source: 14.into(), + dest: 11.into(), + }, + ] + ); } #[test] @@ -191,16 +278,33 @@ class A(object): .trim(), ); let diagnostics = create_diagnostics([Edit::deletion(TextSize::new(7), TextSize::new(15))]); - let (contents, fixed) = apply_fixes(diagnostics.iter(), &locator); + let FixResult { + code, + fixes, + source_map, + } = apply_fixes(diagnostics.iter(), &locator); assert_eq!( - contents, + code, r#" class A: ... "# .trim() ); - assert_eq!(fixed.values().sum::(), 1); + assert_eq!(fixes.values().sum::(), 1); + assert_eq!( + source_map.markers(), + &[ + SourceMarker { + source: 7.into(), + dest: 7.into() + }, + SourceMarker { + source: 15.into(), + dest: 7.into() + } + ] + ); } #[test] @@ -216,17 +320,42 @@ class A(object, object, object): Edit::deletion(TextSize::from(8), TextSize::from(16)), Edit::deletion(TextSize::from(22), TextSize::from(30)), ]); - let (contents, fixed) = apply_fixes(diagnostics.iter(), &locator); + let FixResult { + code, + fixes, + source_map, + } = apply_fixes(diagnostics.iter(), &locator); assert_eq!( - contents, + code, r#" class A(object): ... "# .trim() ); - assert_eq!(fixed.values().sum::(), 2); + assert_eq!(fixes.values().sum::(), 2); + assert_eq!( + source_map.markers(), + &[ + SourceMarker { + source: 8.into(), + dest: 8.into() + }, + SourceMarker { + source: 16.into(), + dest: 8.into() + }, + SourceMarker { + source: 22.into(), + dest: 14.into(), + }, + SourceMarker { + source: 30.into(), + dest: 14.into(), + } + ] + ); } #[test] @@ -242,15 +371,32 @@ class A(object): Edit::deletion(TextSize::from(7), TextSize::from(15)), Edit::replacement("ignored".to_string(), TextSize::from(9), TextSize::from(11)), ]); - let (contents, fixed) = apply_fixes(diagnostics.iter(), &locator); + let FixResult { + code, + fixes, + source_map, + } = apply_fixes(diagnostics.iter(), &locator); assert_eq!( - contents, + code, r#" class A: ... "# .trim(), ); - assert_eq!(fixed.values().sum::(), 1); + assert_eq!(fixes.values().sum::(), 1); + assert_eq!( + source_map.markers(), + &[ + SourceMarker { + source: 7.into(), + dest: 7.into(), + }, + SourceMarker { + source: 15.into(), + dest: 7.into(), + } + ] + ); } } diff --git a/crates/ruff/src/autofix/source_map.rs b/crates/ruff/src/autofix/source_map.rs new file mode 100644 index 0000000000..9b6ea21e17 --- /dev/null +++ b/crates/ruff/src/autofix/source_map.rs @@ -0,0 +1,59 @@ +use ruff_text_size::TextSize; + +use ruff_diagnostics::Edit; + +/// Lightweight sourcemap marker representing the source and destination +/// position for an [`Edit`]. +#[derive(Debug, PartialEq, Eq)] +pub(crate) struct SourceMarker { + /// Position of the marker in the original source. + pub(crate) source: TextSize, + /// Position of the marker in the transformed code. + pub(crate) dest: TextSize, +} + +/// A collection of [`SourceMarker`]. +/// +/// Sourcemaps are used to map positions in the original source to positions in +/// the transformed code. Here, only the boundaries of edits are tracked instead +/// of every single character. +#[derive(Default, PartialEq, Eq)] +pub(crate) struct SourceMap(Vec); + +impl SourceMap { + /// Returns a slice of all the markers in the sourcemap in the order they + /// were added. + pub(crate) fn markers(&self) -> &[SourceMarker] { + &self.0 + } + + /// Push the start marker for an [`Edit`]. + /// + /// The `output_length` is the length of the transformed string before the + /// edit is applied. + pub(crate) fn push_start_marker(&mut self, edit: &Edit, output_length: TextSize) { + self.0.push(SourceMarker { + source: edit.start(), + dest: output_length, + }); + } + + /// Push the end marker for an [`Edit`]. + /// + /// The `output_length` is the length of the transformed string after the + /// edit has been applied. + pub(crate) fn push_end_marker(&mut self, edit: &Edit, output_length: TextSize) { + if edit.is_insertion() { + self.0.push(SourceMarker { + source: edit.start(), + dest: output_length, + }); + } else { + // Deletion or replacement + self.0.push(SourceMarker { + source: edit.end(), + dest: output_length, + }); + } + } +} diff --git a/crates/ruff/src/checkers/ast/deferred.rs b/crates/ruff/src/checkers/ast/deferred.rs index ab74d51234..fdf375f30d 100644 --- a/crates/ruff/src/checkers/ast/deferred.rs +++ b/crates/ruff/src/checkers/ast/deferred.rs @@ -1,13 +1,14 @@ use ruff_text_size::TextRange; use rustpython_parser::ast::Expr; -use ruff_python_semantic::model::Snapshot; +use ruff_python_semantic::{ScopeId, Snapshot}; /// A collection of AST nodes that are deferred for later analysis. /// Used to, e.g., store functions, whose bodies shouldn't be analyzed until all /// module-level definitions have been analyzed. #[derive(Debug, Default)] pub(crate) struct Deferred<'a> { + pub(crate) scopes: Vec, pub(crate) string_type_definitions: Vec<(TextRange, &'a str, Snapshot)>, pub(crate) future_type_definitions: Vec<(&'a Expr, Snapshot)>, pub(crate) functions: Vec, diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 481bddf0ce..930bcc35c7 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -5,32 +5,26 @@ use log::error; use ruff_text_size::{TextRange, TextSize}; use rustpython_format::cformat::{CFormatError, CFormatErrorType}; use rustpython_parser::ast::{ - self, Arg, Arguments, Comprehension, Constant, Excepthandler, Expr, ExprContext, Keyword, - Operator, Pattern, Ranged, Stmt, Suite, Unaryop, + self, Arg, ArgWithDefault, Arguments, Comprehension, Constant, ExceptHandler, Expr, + ExprContext, Keyword, Operator, Pattern, Ranged, Stmt, Suite, UnaryOp, }; -use ruff_diagnostics::{Diagnostic, IsolationLevel}; +use ruff_diagnostics::{Diagnostic, Fix, IsolationLevel}; use ruff_python_ast::all::{extract_all_names, AllNamesFlags}; use ruff_python_ast::helpers::{extract_handled_exceptions, to_module_path}; +use ruff_python_ast::identifier::{Identifier, TryIdentifier}; use ruff_python_ast::source_code::{Generator, Indexer, Locator, Quote, Stylist}; use ruff_python_ast::str::trailing_quote; use ruff_python_ast::types::Node; use ruff_python_ast::typing::{parse_type_annotation, AnnotationKind}; -use ruff_python_ast::visitor::{walk_excepthandler, walk_pattern, Visitor}; +use ruff_python_ast::visitor::{walk_except_handler, walk_pattern, Visitor}; use ruff_python_ast::{cast, helpers, str, visitor}; -use ruff_python_semantic::analyze; -use ruff_python_semantic::analyze::branch_detection; -use ruff_python_semantic::analyze::typing::{Callable, SubscriptKind}; -use ruff_python_semantic::analyze::visibility::ModuleSource; -use ruff_python_semantic::binding::{ - Binding, BindingFlags, BindingId, BindingKind, Exceptions, Export, FromImportation, - Importation, StarImportation, SubmoduleImportation, +use ruff_python_semantic::analyze::{branch_detection, typing, visibility}; +use ruff_python_semantic::{ + Binding, BindingFlags, BindingId, BindingKind, ContextualizedDefinition, Exceptions, + ExecutionContext, Export, FromImport, Globals, Import, Module, ModuleKind, ResolvedRead, Scope, + ScopeId, ScopeKind, SemanticModel, SemanticModelFlags, StarImport, SubmoduleImport, }; -use ruff_python_semantic::context::ExecutionContext; -use ruff_python_semantic::definition::{ContextualizedDefinition, Module, ModuleKind}; -use ruff_python_semantic::globals::Globals; -use ruff_python_semantic::model::{ResolvedRead, SemanticModel, SemanticModelFlags}; -use ruff_python_semantic::scope::{Scope, ScopeId, ScopeKind}; use ruff_python_stdlib::builtins::{BUILTINS, MAGIC_GLOBALS}; use ruff_python_stdlib::path::is_python_stub_file; @@ -42,6 +36,7 @@ use crate::importer::Importer; use crate::noqa::NoqaMapping; use crate::registry::Rule; use crate::rules::flake8_builtins::helpers::AnyShadowing; + use crate::rules::{ airflow, flake8_2020, flake8_annotations, flake8_async, flake8_bandit, flake8_blind_except, flake8_boolean_trap, flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_datetimez, @@ -50,7 +45,8 @@ use crate::rules::{ flake8_print, flake8_pyi, flake8_pytest_style, flake8_raise, flake8_return, flake8_self, flake8_simplify, flake8_slots, flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, flake8_use_pathlib, flynt, mccabe, numpy, pandas_vet, pep8_naming, - pycodestyle, pydocstyle, pyflakes, pygrep_hooks, pylint, pyupgrade, ruff, tryceratops, + perflint, pycodestyle, pydocstyle, pyflakes, pygrep_hooks, pylint, pyupgrade, ruff, + tryceratops, }; use crate::settings::types::PythonVersion; use crate::settings::{flags, Settings}; @@ -72,11 +68,11 @@ pub(crate) struct Checker<'a> { pub(crate) indexer: &'a Indexer, pub(crate) importer: Importer<'a>, // Stateful fields. - semantic_model: SemanticModel<'a>, + semantic: SemanticModel<'a>, deferred: Deferred<'a>, pub(crate) diagnostics: Vec, // Check-specific state. - pub(crate) flake8_bugbear_seen: Vec<&'a Expr>, + pub(crate) flake8_bugbear_seen: Vec<&'a ast::ExprName>, } impl<'a> Checker<'a> { @@ -105,7 +101,7 @@ impl<'a> Checker<'a> { stylist, indexer, importer, - semantic_model: SemanticModel::new(&settings.typing_modules, path, module), + semantic: SemanticModel::new(&settings.typing_modules, path, module), deferred: Deferred::default(), diagnostics: Vec::default(), flake8_bugbear_seen: Vec::default(), @@ -148,7 +144,7 @@ impl<'a> Checker<'a> { /// /// If the current expression in the context is not an f-string, returns ``None``. pub(crate) fn f_string_quote_style(&self) -> Option { - let model = &self.semantic_model; + let model = &self.semantic; if !model.in_f_string() { return None; } @@ -174,14 +170,14 @@ impl<'a> Checker<'a> { /// thus be applied whenever we delete a statement, but can otherwise be omitted. pub(crate) fn isolation(&self, parent: Option<&Stmt>) -> IsolationLevel { parent - .and_then(|stmt| self.semantic_model.stmts.node_id(stmt)) + .and_then(|stmt| self.semantic.stmts.node_id(stmt)) .map_or(IsolationLevel::default(), |node_id| { IsolationLevel::Group(node_id.into()) }) } - pub(crate) const fn semantic_model(&self) -> &SemanticModel<'a> { - &self.semantic_model + pub(crate) const fn semantic(&self) -> &SemanticModel<'a> { + &self.semantic } pub(crate) const fn package(&self) -> Option<&'a Path> { @@ -210,7 +206,7 @@ where 'b: 'a, { fn visit_stmt(&mut self, stmt: &'b Stmt) { - self.semantic_model.push_stmt(stmt); + self.semantic.push_stmt(stmt); // Track whether we've seen docstrings, non-imports, etc. match stmt { @@ -221,127 +217,112 @@ where .iter() .any(|alias| alias.name.as_str() == "annotations") { - self.semantic_model.flags |= SemanticModelFlags::FUTURE_ANNOTATIONS; + self.semantic.flags |= SemanticModelFlags::FUTURE_ANNOTATIONS; } } else { - self.semantic_model.flags |= SemanticModelFlags::FUTURES_BOUNDARY; + self.semantic.flags |= SemanticModelFlags::FUTURES_BOUNDARY; } } Stmt::Import(_) => { - self.semantic_model.flags |= SemanticModelFlags::FUTURES_BOUNDARY; + self.semantic.flags |= SemanticModelFlags::FUTURES_BOUNDARY; } _ => { - self.semantic_model.flags |= SemanticModelFlags::FUTURES_BOUNDARY; - if !self.semantic_model.seen_import_boundary() + self.semantic.flags |= SemanticModelFlags::FUTURES_BOUNDARY; + if !self.semantic.seen_import_boundary() && !helpers::is_assignment_to_a_dunder(stmt) - && !helpers::in_nested_block(self.semantic_model.parents()) + && !helpers::in_nested_block(self.semantic.parents()) { - self.semantic_model.flags |= SemanticModelFlags::IMPORT_BOUNDARY; + self.semantic.flags |= SemanticModelFlags::IMPORT_BOUNDARY; } } } // Track each top-level import, to guide import insertions. if matches!(stmt, Stmt::Import(_) | Stmt::ImportFrom(_)) { - if self.semantic_model.at_top_level() { + if self.semantic.at_top_level() { self.importer.visit_import(stmt); } } // Store the flags prior to any further descent, so that we can restore them after visiting // the node. - let flags_snapshot = self.semantic_model.flags; + let flags_snapshot = self.semantic.flags; // Pre-visit. match stmt { Stmt::Global(ast::StmtGlobal { names, range: _ }) => { - let ranges: Vec = helpers::find_names(stmt, self.locator).collect(); - if !self.semantic_model.scope_id.is_global() { - for (name, range) in names.iter().zip(ranges.iter()) { + if !self.semantic.scope_id.is_global() { + for name in names { + if let Some(binding_id) = self.semantic.global_scope().get(name) { + // Mark the binding in the global scope as "rebound" in the current scope. + self.semantic + .add_rebinding_scope(binding_id, self.semantic.scope_id); + } + // Add a binding to the current scope. - let binding_id = self.semantic_model.push_binding( - *range, + let binding_id = self.semantic.push_binding( + name.range(), BindingKind::Global, - BindingFlags::empty(), + BindingFlags::GLOBAL, ); - let scope = self.semantic_model.scope_mut(); + let scope = self.semantic.scope_mut(); scope.add(name, binding_id); } } if self.enabled(Rule::AmbiguousVariableName) { - self.diagnostics - .extend(names.iter().zip(ranges.iter()).filter_map(|(name, range)| { - pycodestyle::rules::ambiguous_variable_name(name, *range) - })); + self.diagnostics.extend(names.iter().filter_map(|name| { + pycodestyle::rules::ambiguous_variable_name(name, name.range()) + })); } } Stmt::Nonlocal(ast::StmtNonlocal { names, range: _ }) => { - let ranges: Vec = helpers::find_names(stmt, self.locator).collect(); - if !self.semantic_model.scope_id.is_global() { - for (name, range) in names.iter().zip(ranges.iter()) { - // Add a binding to the current scope. - let binding_id = self.semantic_model.push_binding( - *range, - BindingKind::Nonlocal, - BindingFlags::empty(), - ); - let scope = self.semantic_model.scope_mut(); - scope.add(name, binding_id); - } - - // Mark the binding in the defining scopes as used too. (Skip the global scope - // and the current scope, and, per standard resolution rules, any class scopes.) - for (name, range) in names.iter().zip(ranges.iter()) { - let binding_id = self - .semantic_model - .scopes - .ancestors(self.semantic_model.scope_id) - .skip(1) - .filter(|scope| !(scope.kind.is_module() || scope.kind.is_class())) - .find_map(|scope| scope.get(name.as_str())); - - if let Some(binding_id) = binding_id { - self.semantic_model.add_local_reference( + if !self.semantic.scope_id.is_global() { + for name in names { + if let Some((scope_id, binding_id)) = self.semantic.nonlocal(name) { + // Mark the binding as "used". + self.semantic.add_local_reference( binding_id, - stmt.range(), + name.range(), ExecutionContext::Runtime, ); - } - // Ensure that every nonlocal has an existing binding from a parent scope. - if self.enabled(Rule::NonlocalWithoutBinding) { - if self - .semantic_model - .scopes - .ancestors(self.semantic_model.scope_id) - .skip(1) - .take_while(|scope| !scope.kind.is_module()) - .all(|scope| !scope.declares(name.as_str())) - { + // Mark the binding in the enclosing scope as "rebound" in the current + // scope. + self.semantic + .add_rebinding_scope(binding_id, self.semantic.scope_id); + + // Add a binding to the current scope. + let binding_id = self.semantic.push_binding( + name.range(), + BindingKind::Nonlocal(scope_id), + BindingFlags::NONLOCAL, + ); + let scope = self.semantic.scope_mut(); + scope.add(name, binding_id); + } else { + if self.enabled(Rule::NonlocalWithoutBinding) { self.diagnostics.push(Diagnostic::new( pylint::rules::NonlocalWithoutBinding { name: name.to_string(), }, - *range, + name.range(), )); } } } } - if self.enabled(Rule::AmbiguousVariableName) { - self.diagnostics - .extend(names.iter().zip(ranges.iter()).filter_map(|(name, range)| { - pycodestyle::rules::ambiguous_variable_name(name, *range) - })); + self.diagnostics.extend(names.iter().filter_map(|name| { + pycodestyle::rules::ambiguous_variable_name(name, name.range()) + })); } } Stmt::Break(_) => { if self.enabled(Rule::BreakOutsideLoop) { if let Some(diagnostic) = pyflakes::rules::break_outside_loop( stmt, - &mut self.semantic_model.parents().skip(1), + &mut self.semantic.parents().skip(1), ) { self.diagnostics.push(diagnostic); } @@ -351,7 +332,7 @@ where if self.enabled(Rule::ContinueOutsideLoop) { if let Some(diagnostic) = pyflakes::rules::continue_outside_loop( stmt, - &mut self.semantic_model.parents().skip(1), + &mut self.semantic.parents().skip(1), ) { self.diagnostics.push(diagnostic); } @@ -374,45 +355,34 @@ where .. }) => { if self.enabled(Rule::DjangoNonLeadingReceiverDecorator) { - self.diagnostics - .extend(flake8_django::rules::non_leading_receiver_decorator( - decorator_list, - |expr| self.semantic_model.resolve_call_path(expr), - )); + flake8_django::rules::non_leading_receiver_decorator(self, decorator_list); } - if self.enabled(Rule::AmbiguousFunctionName) { if let Some(diagnostic) = - pycodestyle::rules::ambiguous_function_name(name, || { - helpers::identifier_range(stmt, self.locator) - }) + pycodestyle::rules::ambiguous_function_name(name, || stmt.identifier()) { self.diagnostics.push(diagnostic); } } - if self.enabled(Rule::InvalidStrReturnType) { pylint::rules::invalid_str_return(self, name, body); } - if self.enabled(Rule::InvalidFunctionName) { if let Some(diagnostic) = pep8_naming::rules::invalid_function_name( stmt, name, decorator_list, &self.settings.pep8_naming.ignore_names, - &self.semantic_model, - self.locator, + &self.semantic, ) { self.diagnostics.push(diagnostic); } } - if self.enabled(Rule::InvalidFirstArgumentNameForClassMethod) { if let Some(diagnostic) = pep8_naming::rules::invalid_first_argument_name_for_class_method( self, - self.semantic_model.scope(), + self.semantic.scope(), name, decorator_list, args, @@ -421,12 +391,11 @@ where self.diagnostics.push(diagnostic); } } - if self.enabled(Rule::InvalidFirstArgumentNameForMethod) { if let Some(diagnostic) = pep8_naming::rules::invalid_first_argument_name_for_method( self, - self.semantic_model.scope(), + self.semantic.scope(), name, decorator_list, args, @@ -466,23 +435,28 @@ where if self.enabled(Rule::NoReturnArgumentAnnotationInStub) { flake8_pyi::rules::no_return_argument_annotation(self, args); } + if self.enabled(Rule::BadExitAnnotation) { + flake8_pyi::rules::bad_exit_annotation( + self, + stmt.is_async_function_def_stmt(), + name, + args, + ); + } } - if self.enabled(Rule::DunderFunctionName) { if let Some(diagnostic) = pep8_naming::rules::dunder_function_name( - self.semantic_model.scope(), + self.semantic.scope(), stmt, name, - self.locator, + &self.settings.pep8_naming.ignore_names, ) { self.diagnostics.push(diagnostic); } } - if self.enabled(Rule::GlobalStatement) { pylint::rules::global_statement(self, name); } - if self.enabled(Rule::LRUCacheWithoutParameters) && self.settings.target_version >= PythonVersion::Py38 { @@ -493,11 +467,9 @@ where { pyupgrade::rules::lru_cache_with_maxsize_none(self, decorator_list); } - if self.enabled(Rule::CachedInstanceMethod) { flake8_bugbear::rules::cached_instance_method(self, decorator_list); } - if self.any_enabled(&[ Rule::UnnecessaryReturnNone, Rule::ImplicitReturnValue, @@ -514,7 +486,6 @@ where returns.as_ref().map(|expr| &**expr), ); } - if self.enabled(Rule::UselessReturn) { pylint::rules::useless_return( self, @@ -523,65 +494,52 @@ where returns.as_ref().map(|expr| &**expr), ); } - if self.enabled(Rule::ComplexStructure) { if let Some(diagnostic) = mccabe::rules::function_is_too_complex( stmt, name, body, self.settings.mccabe.max_complexity, - self.locator, ) { self.diagnostics.push(diagnostic); } } - if self.enabled(Rule::HardcodedPasswordDefault) { - self.diagnostics - .extend(flake8_bandit::rules::hardcoded_password_default(args)); + flake8_bandit::rules::hardcoded_password_default(self, args); } - if self.enabled(Rule::PropertyWithParameters) { pylint::rules::property_with_parameters(self, stmt, decorator_list, args); } - if self.enabled(Rule::TooManyArguments) { pylint::rules::too_many_arguments(self, args, stmt); } - if self.enabled(Rule::TooManyReturnStatements) { if let Some(diagnostic) = pylint::rules::too_many_return_statements( stmt, body, self.settings.pylint.max_returns, - self.locator, ) { self.diagnostics.push(diagnostic); } } - if self.enabled(Rule::TooManyBranches) { if let Some(diagnostic) = pylint::rules::too_many_branches( stmt, body, self.settings.pylint.max_branches, - self.locator, ) { self.diagnostics.push(diagnostic); } } - if self.enabled(Rule::TooManyStatements) { if let Some(diagnostic) = pylint::rules::too_many_statements( stmt, body, self.settings.pylint.max_statements, - self.locator, ) { self.diagnostics.push(diagnostic); } } - if self.any_enabled(&[ Rule::PytestFixtureIncorrectParenthesesStyle, Rule::PytestFixturePositionalArgs, @@ -604,21 +562,18 @@ where body, ); } - if self.any_enabled(&[ Rule::PytestParametrizeNamesWrongType, Rule::PytestParametrizeValuesWrongType, ]) { flake8_pytest_style::rules::parametrize(self, decorator_list); } - if self.any_enabled(&[ Rule::PytestIncorrectMarkParenthesesStyle, Rule::PytestUseFixturesWithoutParameters, ]) { flake8_pytest_style::rules::marks(self, decorator_list); } - if self.enabled(Rule::BooleanPositionalArgInFunctionDefinition) { flake8_boolean_trap::rules::check_positional_boolean_in_def( self, @@ -627,7 +582,6 @@ where args, ); } - if self.enabled(Rule::BooleanDefaultValueInFunctionDefinition) { flake8_boolean_trap::rules::check_boolean_default_value_in_function_definition( self, @@ -636,7 +590,6 @@ where args, ); } - if self.enabled(Rule::UnexpectedSpecialMethodSignature) { pylint::rules::unexpected_special_method_signature( self, @@ -644,22 +597,19 @@ where name, decorator_list, args, - self.locator, ); } - if self.enabled(Rule::FStringDocstring) { flake8_bugbear::rules::f_string_docstring(self, body); } - if self.enabled(Rule::YieldInForLoop) { pyupgrade::rules::yield_in_for_loop(self, stmt); } - - if self.semantic_model.scope().kind.is_class() { + if let ScopeKind::Class(class_def) = self.semantic.scope().kind { if self.enabled(Rule::BuiltinAttributeShadowing) { flake8_builtins::rules::builtin_attribute_shadowing( self, + class_def, name, AnyShadowing::from(stmt), ); @@ -673,6 +623,11 @@ where ); } } + #[cfg(feature = "unreachable-code")] + if self.enabled(Rule::UnreachableCode) { + self.diagnostics + .extend(ruff::rules::unreachable::in_function(name, body)); + } } Stmt::Return(_) => { if self.enabled(Rule::ReturnOutsideFunction) { @@ -693,12 +648,8 @@ where }, ) => { if self.enabled(Rule::DjangoNullableModelStringField) { - self.diagnostics - .extend(flake8_django::rules::nullable_model_string_field( - self, body, - )); + flake8_django::rules::nullable_model_string_field(self, body); } - if self.enabled(Rule::DjangoExcludeWithModelForm) { if let Some(diagnostic) = flake8_django::rules::exclude_with_model_form(self, bases, body) @@ -713,50 +664,49 @@ where self.diagnostics.push(diagnostic); } } - if self.enabled(Rule::DjangoModelWithoutDunderStr) { - if let Some(diagnostic) = - flake8_django::rules::model_without_dunder_str(self, bases, body, stmt) - { - self.diagnostics.push(diagnostic); - } - } if self.enabled(Rule::DjangoUnorderedBodyContentInModel) { flake8_django::rules::unordered_body_content_in_model(self, bases, body); } + if !self.is_stub { + if self.enabled(Rule::DjangoModelWithoutDunderStr) { + flake8_django::rules::model_without_dunder_str(self, class_def); + } + } if self.enabled(Rule::GlobalStatement) { pylint::rules::global_statement(self, name); } if self.enabled(Rule::UselessObjectInheritance) { - pyupgrade::rules::useless_object_inheritance(self, stmt, name, bases, keywords); + pyupgrade::rules::useless_object_inheritance(self, class_def); + } + if self.enabled(Rule::UnnecessaryClassParentheses) { + pyupgrade::rules::unnecessary_class_parentheses(self, class_def); } - if self.enabled(Rule::AmbiguousClassName) { - if let Some(diagnostic) = pycodestyle::rules::ambiguous_class_name(name, || { - helpers::identifier_range(stmt, self.locator) - }) { - self.diagnostics.push(diagnostic); - } - } - - if self.enabled(Rule::InvalidClassName) { if let Some(diagnostic) = - pep8_naming::rules::invalid_class_name(stmt, name, self.locator) + pycodestyle::rules::ambiguous_class_name(name, || stmt.identifier()) { self.diagnostics.push(diagnostic); } } - + if self.enabled(Rule::InvalidClassName) { + if let Some(diagnostic) = pep8_naming::rules::invalid_class_name( + stmt, + name, + &self.settings.pep8_naming.ignore_names, + ) { + self.diagnostics.push(diagnostic); + } + } if self.enabled(Rule::ErrorSuffixOnExceptionName) { if let Some(diagnostic) = pep8_naming::rules::error_suffix_on_exception_name( stmt, bases, name, - self.locator, + &self.settings.pep8_naming.ignore_names, ) { self.diagnostics.push(diagnostic); } } - if !self.is_stub { if self.any_enabled(&[ Rule::AbstractBaseClassWithoutAbstractMethod, @@ -778,37 +728,27 @@ where flake8_pyi::rules::ellipsis_in_non_empty_class_body(self, stmt, body); } } - if self.enabled(Rule::PytestIncorrectMarkParenthesesStyle) { flake8_pytest_style::rules::marks(self, decorator_list); } - if self.enabled(Rule::DuplicateClassFieldDefinition) { flake8_pie::rules::duplicate_class_field_definition(self, stmt, body); } - if self.enabled(Rule::NonUniqueEnums) { flake8_pie::rules::non_unique_enums(self, stmt, body); } - - if self.any_enabled(&[ - Rule::MutableDataclassDefault, - Rule::FunctionCallInDataclassDefaultArgument, - ]) && ruff::rules::is_dataclass(&self.semantic_model, decorator_list) - { - if self.enabled(Rule::MutableDataclassDefault) { - ruff::rules::mutable_dataclass_default(self, body); - } - - if self.enabled(Rule::FunctionCallInDataclassDefaultArgument) { - ruff::rules::function_call_in_dataclass_defaults(self, body); - } + if self.enabled(Rule::MutableClassDefault) { + ruff::rules::mutable_class_default(self, class_def); + } + if self.enabled(Rule::MutableDataclassDefault) { + ruff::rules::mutable_dataclass_default(self, class_def); + } + if self.enabled(Rule::FunctionCallInDataclassDefaultArgument) { + ruff::rules::function_call_in_dataclass_default(self, class_def); } - if self.enabled(Rule::FStringDocstring) { flake8_bugbear::rules::f_string_docstring(self, body); } - if self.enabled(Rule::BuiltinVariableShadowing) { flake8_builtins::rules::builtin_variable_shadowing( self, @@ -816,22 +756,21 @@ where AnyShadowing::from(stmt), ); } - if self.enabled(Rule::DuplicateBases) { pylint::rules::duplicate_bases(self, name, bases); } - if self.enabled(Rule::NoSlotsInStrSubclass) { flake8_slots::rules::no_slots_in_str_subclass(self, stmt, class_def); } - if self.enabled(Rule::NoSlotsInTupleSubclass) { flake8_slots::rules::no_slots_in_tuple_subclass(self, stmt, class_def); } - if self.enabled(Rule::NoSlotsInNamedtupleSubclass) { flake8_slots::rules::no_slots_in_namedtuple_subclass(self, stmt, class_def); } + if self.enabled(Rule::SingleStringSlots) { + pylint::rules::single_string_slots(self, class_def); + } } Stmt::Import(ast::StmtImport { names, range: _ }) => { if self.enabled(Rule::MultipleImportsOnOneLine) { @@ -840,9 +779,8 @@ where if self.enabled(Rule::ModuleImportNotAtTopOfFile) { pycodestyle::rules::module_import_not_at_top_of_file(self, stmt, self.locator); } - if self.enabled(Rule::GlobalStatement) { - for name in names.iter() { + for name in names { if let Some(asname) = name.asname.as_ref() { pylint::rules::global_statement(self, asname); } else { @@ -850,7 +788,6 @@ where } } } - if self.enabled(Rule::DeprecatedCElementTree) { pyupgrade::rules::deprecated_c_element_tree(self, stmt); } @@ -859,52 +796,37 @@ where } for alias in names { - if &alias.name == "__future__" { - let name = alias.asname.as_ref().unwrap_or(&alias.name); - self.add_binding( - name, - alias.range(), - BindingKind::FutureImportation, - BindingFlags::empty(), - ); - - if self.enabled(Rule::LateFutureImport) { - if self.semantic_model.seen_futures_boundary() { - self.diagnostics.push(Diagnostic::new( - pyflakes::rules::LateFutureImport, - stmt.range(), - )); - } - } - } else if alias.name.contains('.') && alias.asname.is_none() { + if alias.name.contains('.') && alias.asname.is_none() { // Given `import foo.bar`, `name` would be "foo", and `qualified_name` would be // "foo.bar". let name = alias.name.split('.').next().unwrap(); let qualified_name = &alias.name; self.add_binding( name, - alias.range(), - BindingKind::SubmoduleImportation(SubmoduleImportation { - qualified_name, - }), - BindingFlags::empty(), + alias.identifier(), + BindingKind::SubmoduleImport(SubmoduleImport { qualified_name }), + BindingFlags::EXTERNAL, ); } else { + let mut flags = BindingFlags::EXTERNAL; + if alias.asname.is_some() { + flags |= BindingFlags::ALIAS; + } + if alias + .asname + .as_ref() + .map_or(false, |asname| asname.as_str() == alias.name.as_str()) + { + flags |= BindingFlags::EXPLICIT_EXPORT; + } + let name = alias.asname.as_ref().unwrap_or(&alias.name); let qualified_name = &alias.name; self.add_binding( name, - alias.range(), - BindingKind::Importation(Importation { qualified_name }), - if alias - .asname - .as_ref() - .map_or(false, |asname| asname == &alias.name) - { - BindingFlags::EXPLICIT_EXPORT - } else { - BindingFlags::empty() - }, + alias.identifier(), + BindingKind::Import(Import { qualified_name }), + flags, ); if let Some(asname) = &alias.asname { @@ -917,8 +839,6 @@ where } } } - - // flake8-debugger if self.enabled(Rule::Debugger) { if let Some(diagnostic) = flake8_debugger::rules::debugger_import(stmt, None, &alias.name) @@ -926,8 +846,6 @@ where self.diagnostics.push(diagnostic); } } - - // flake8_tidy_imports if self.enabled(Rule::BannedApi) { flake8_tidy_imports::rules::name_or_parent_is_banned( self, @@ -935,8 +853,6 @@ where alias, ); } - - // pylint if !self.is_stub { if self.enabled(Rule::UselessImportAlias) { pylint::rules::useless_import_alias(self, alias); @@ -958,67 +874,69 @@ where if self.enabled(Rule::ConstantImportedAsNonConstant) { if let Some(diagnostic) = pep8_naming::rules::constant_imported_as_non_constant( - name, asname, alias, stmt, + name, + asname, + alias, + stmt, + &self.settings.pep8_naming.ignore_names, ) { self.diagnostics.push(diagnostic); } } - if self.enabled(Rule::LowercaseImportedAsNonLowercase) { if let Some(diagnostic) = pep8_naming::rules::lowercase_imported_as_non_lowercase( - name, asname, alias, stmt, + name, + asname, + alias, + stmt, + &self.settings.pep8_naming.ignore_names, ) { self.diagnostics.push(diagnostic); } } - if self.enabled(Rule::CamelcaseImportedAsLowercase) { if let Some(diagnostic) = pep8_naming::rules::camelcase_imported_as_lowercase( - name, asname, alias, stmt, + name, + asname, + alias, + stmt, + &self.settings.pep8_naming.ignore_names, ) { self.diagnostics.push(diagnostic); } } - if self.enabled(Rule::CamelcaseImportedAsConstant) { if let Some(diagnostic) = pep8_naming::rules::camelcase_imported_as_constant( - name, asname, alias, stmt, + name, + asname, + alias, + stmt, + &self.settings.pep8_naming.ignore_names, ) { self.diagnostics.push(diagnostic); } } - if self.enabled(Rule::CamelcaseImportedAsAcronym) { if let Some(diagnostic) = pep8_naming::rules::camelcase_imported_as_acronym( - name, asname, alias, stmt, + name, + asname, + alias, + stmt, + &self.settings.pep8_naming.ignore_names, ) { self.diagnostics.push(diagnostic); } } } - - if self.enabled(Rule::UnconventionalImportAlias) { - if let Some(diagnostic) = - flake8_import_conventions::rules::conventional_import_alias( - stmt, - &alias.name, - alias.asname.as_deref(), - &self.settings.flake8_import_conventions.aliases, - ) - { - self.diagnostics.push(diagnostic); - } - } - if self.enabled(Rule::BannedImportAlias) { if let Some(asname) = &alias.asname { if let Some(diagnostic) = @@ -1033,7 +951,6 @@ where } } } - if self.enabled(Rule::PytestIncorrectPytestImport) { if let Some(diagnostic) = flake8_pytest_style::rules::import( stmt, @@ -1058,9 +975,8 @@ where if self.enabled(Rule::ModuleImportNotAtTopOfFile) { pycodestyle::rules::module_import_not_at_top_of_file(self, stmt, self.locator); } - if self.enabled(Rule::GlobalStatement) { - for name in names.iter() { + for name in names { if let Some(asname) = name.asname.as_ref() { pylint::rules::global_statement(self, asname); } else { @@ -1068,7 +984,6 @@ where } } } - if self.enabled(Rule::UnnecessaryFutureImport) && self.settings.target_version >= PythonVersion::Py37 { @@ -1108,7 +1023,6 @@ where } } } - if self.enabled(Rule::PytestIncorrectPytestImport) { if let Some(diagnostic) = flake8_pytest_style::rules::import_from(stmt, module, level) @@ -1118,8 +1032,8 @@ where } if self.is_stub { - if self.enabled(Rule::UnaliasedCollectionsAbcSetImport) { - flake8_pyi::rules::unaliased_collections_abc_set_import(self, import_from); + if self.enabled(Rule::FutureAnnotationsInStub) { + flake8_pyi::rules::from_future_import(self, import_from); } } for alias in names { @@ -1128,17 +1042,16 @@ where self.add_binding( name, - alias.range(), - BindingKind::FutureImportation, + alias.identifier(), + BindingKind::FutureImport, BindingFlags::empty(), ); if self.enabled(Rule::FutureFeatureNotDefined) { pyflakes::rules::future_feature_not_defined(self, alias); } - if self.enabled(Rule::LateFutureImport) { - if self.semantic_model.seen_futures_boundary() { + if self.semantic.seen_futures_boundary() { self.diagnostics.push(Diagnostic::new( pyflakes::rules::LateFutureImport, stmt.range(), @@ -1146,12 +1059,12 @@ where } } } else if &alias.name == "*" { - self.semantic_model + self.semantic .scope_mut() - .add_star_import(StarImportation { level, module }); + .add_star_import(StarImport { level, module }); if self.enabled(Rule::UndefinedLocalWithNestedImportStarUsage) { - let scope = self.semantic_model.scope(); + let scope = self.semantic.scope(); if !matches!(scope.kind, ScopeKind::Module) { self.diagnostics.push(Diagnostic::new( pyflakes::rules::UndefinedLocalWithNestedImportStarUsage { @@ -1161,7 +1074,6 @@ where )); } } - if self.enabled(Rule::UndefinedLocalWithImportStar) { self.diagnostics.push(Diagnostic::new( pyflakes::rules::UndefinedLocalWithImportStar { @@ -1181,6 +1093,18 @@ where } } + let mut flags = BindingFlags::EXTERNAL; + if alias.asname.is_some() { + flags |= BindingFlags::ALIAS; + } + if alias + .asname + .as_ref() + .map_or(false, |asname| asname.as_str() == alias.name.as_str()) + { + flags |= BindingFlags::EXPLICIT_EXPORT; + } + // Given `from foo import bar`, `name` would be "bar" and `qualified_name` would // be "foo.bar". Given `from foo import bar as baz`, `name` would be "baz" // and `qualified_name` would be "foo.bar". @@ -1189,20 +1113,11 @@ where helpers::format_import_from_member(level, module, &alias.name); self.add_binding( name, - alias.range(), - BindingKind::FromImportation(FromImportation { qualified_name }), - if alias - .asname - .as_ref() - .map_or(false, |asname| asname == &alias.name) - { - BindingFlags::EXPLICIT_EXPORT - } else { - BindingFlags::empty() - }, + alias.identifier(), + BindingKind::FromImport(FromImport { qualified_name }), + flags, ); } - if self.enabled(Rule::RelativeImports) { if let Some(diagnostic) = flake8_tidy_imports::rules::banned_relative_import( self, @@ -1215,8 +1130,6 @@ where self.diagnostics.push(diagnostic); } } - - // flake8-debugger if self.enabled(Rule::Debugger) { if let Some(diagnostic) = flake8_debugger::rules::debugger_import(stmt, module, &alias.name) @@ -1224,22 +1137,6 @@ where self.diagnostics.push(diagnostic); } } - - if self.enabled(Rule::UnconventionalImportAlias) { - let qualified_name = - helpers::format_import_from_member(level, module, &alias.name); - if let Some(diagnostic) = - flake8_import_conventions::rules::conventional_import_alias( - stmt, - &qualified_name, - alias.asname.as_deref(), - &self.settings.flake8_import_conventions.aliases, - ) - { - self.diagnostics.push(diagnostic); - } - } - if self.enabled(Rule::BannedImportAlias) { if let Some(asname) = &alias.asname { let qualified_name = @@ -1256,7 +1153,6 @@ where } } } - if let Some(asname) = &alias.asname { if self.enabled(Rule::ConstantImportedAsNonConstant) { if let Some(diagnostic) = @@ -1265,12 +1161,12 @@ where asname, alias, stmt, + &self.settings.pep8_naming.ignore_names, ) { self.diagnostics.push(diagnostic); } } - if self.enabled(Rule::LowercaseImportedAsNonLowercase) { if let Some(diagnostic) = pep8_naming::rules::lowercase_imported_as_non_lowercase( @@ -1278,12 +1174,12 @@ where asname, alias, stmt, + &self.settings.pep8_naming.ignore_names, ) { self.diagnostics.push(diagnostic); } } - if self.enabled(Rule::CamelcaseImportedAsLowercase) { if let Some(diagnostic) = pep8_naming::rules::camelcase_imported_as_lowercase( @@ -1291,12 +1187,12 @@ where asname, alias, stmt, + &self.settings.pep8_naming.ignore_names, ) { self.diagnostics.push(diagnostic); } } - if self.enabled(Rule::CamelcaseImportedAsConstant) { if let Some(diagnostic) = pep8_naming::rules::camelcase_imported_as_constant( @@ -1304,12 +1200,12 @@ where asname, alias, stmt, + &self.settings.pep8_naming.ignore_names, ) { self.diagnostics.push(diagnostic); } } - if self.enabled(Rule::CamelcaseImportedAsAcronym) { if let Some(diagnostic) = pep8_naming::rules::camelcase_imported_as_acronym( @@ -1317,13 +1213,12 @@ where asname, alias, stmt, + &self.settings.pep8_naming.ignore_names, ) { self.diagnostics.push(diagnostic); } } - - // pylint if !self.is_stub { if self.enabled(Rule::UselessImportAlias) { pylint::rules::useless_import_alias(self, alias); @@ -1331,7 +1226,6 @@ where } } } - if self.enabled(Rule::ImportSelf) { if let Some(diagnostic) = pylint::rules::import_from_self(level, module, names, self.module_path) @@ -1339,7 +1233,6 @@ where self.diagnostics.push(diagnostic); } } - if self.enabled(Rule::BannedImportFrom) { if let Some(diagnostic) = flake8_import_conventions::rules::banned_import_from( stmt, @@ -1356,9 +1249,9 @@ where pyflakes::rules::raise_not_implemented(self, expr); } } - if self.enabled(Rule::CannotRaiseLiteral) { + if self.enabled(Rule::RaiseLiteral) { if let Some(exc) = exc { - flake8_bugbear::rules::cannot_raise_literal(self, exc); + flake8_bugbear::rules::raise_literal(self, exc); } } if self.any_enabled(&[ @@ -1416,14 +1309,14 @@ where test, body, orelse, - self.semantic_model.stmt_parent(), + self.semantic.stmt_parent(), ); } if self.enabled(Rule::IfWithSameArms) { flake8_simplify::rules::if_with_same_arms( self, stmt, - self.semantic_model.stmt_parent(), + self.semantic.stmt_parent(), ); } if self.enabled(Rule::NeedlessBool) { @@ -1436,14 +1329,14 @@ where test, body, orelse, - self.semantic_model.stmt_parent(), + self.semantic.stmt_parent(), ); } if self.enabled(Rule::IfElseBlockInsteadOfIfExp) { flake8_simplify::rules::use_ternary_operator( self, stmt, - self.semantic_model.stmt_parent(), + self.semantic.stmt_parent(), ); } if self.enabled(Rule::IfElseBlockInsteadOfDictGet) { @@ -1453,7 +1346,7 @@ where test, body, orelse, - self.semantic_model.stmt_parent(), + self.semantic.stmt_parent(), ); } if self.enabled(Rule::TypeCheckWithoutTypeError) { @@ -1462,7 +1355,7 @@ where body, test, orelse, - self.semantic_model.stmt_parent(), + self.semantic.stmt_parent(), ); } if self.enabled(Rule::OutdatedVersionBlock) { @@ -1475,13 +1368,58 @@ where self.diagnostics.push(diagnostic); } } + if self.is_stub { + if self.any_enabled(&[ + Rule::UnrecognizedVersionInfoCheck, + Rule::PatchVersionComparison, + Rule::WrongTupleLengthVersionComparison, + ]) { + if let Expr::BoolOp(ast::ExprBoolOp { values, .. }) = test.as_ref() { + for value in values { + flake8_pyi::rules::unrecognized_version_info(self, value); + } + } else { + flake8_pyi::rules::unrecognized_version_info(self, test); + } + } + if self.any_enabled(&[ + Rule::UnrecognizedPlatformCheck, + Rule::UnrecognizedPlatformName, + ]) { + if let Expr::BoolOp(ast::ExprBoolOp { values, .. }) = test.as_ref() { + for value in values { + flake8_pyi::rules::unrecognized_platform(self, value); + } + } else { + flake8_pyi::rules::unrecognized_platform(self, test); + } + } + if self.enabled(Rule::BadVersionInfoComparison) { + if let Expr::BoolOp(ast::ExprBoolOp { values, .. }) = test.as_ref() { + for value in values { + flake8_pyi::rules::bad_version_info_comparison(self, value); + } + } else { + flake8_pyi::rules::bad_version_info_comparison(self, test); + } + } + if self.enabled(Rule::ComplexIfStatementInStub) { + if let Expr::BoolOp(ast::ExprBoolOp { values, .. }) = test.as_ref() { + for value in values { + flake8_pyi::rules::complex_if_statement_in_stub(self, value); + } + } else { + flake8_pyi::rules::complex_if_statement_in_stub(self, test); + } + } + } } Stmt::Assert(ast::StmtAssert { test, msg, range: _, }) => { - if !self.semantic_model.in_type_checking_block() { + if !self.semantic.in_type_checking_block() { if self.enabled(Rule::Assert) { self.diagnostics .push(flake8_bandit::rules::assert_used(stmt)); @@ -1511,9 +1449,10 @@ where pygrep_hooks::rules::non_existent_mock_method(self, test); } } - Stmt::With(ast::StmtWith { items, body, .. }) => { + Stmt::With(ast::StmtWith { items, body, .. }) + | Stmt::AsyncWith(ast::StmtAsyncWith { items, body, .. }) => { if self.enabled(Rule::AssertRaisesException) { - flake8_bugbear::rules::assert_raises_exception(self, stmt, items); + flake8_bugbear::rules::assert_raises_exception(self, items); } if self.enabled(Rule::PytestRaisesWithMultipleStatements) { flake8_pytest_style::rules::complex_raises(self, stmt, items, body); @@ -1523,11 +1462,11 @@ where self, stmt, body, - self.semantic_model.stmt_parent(), + self.semantic.stmt_parent(), ); } if self.enabled(Rule::RedefinedLoopName) { - pylint::rules::redefined_loop_name(self, &Node::Stmt(stmt)); + pylint::rules::redefined_loop_name(self, stmt); } } Stmt::While(ast::StmtWhile { body, orelse, .. }) => { @@ -1537,6 +1476,9 @@ where if self.enabled(Rule::UselessElseOnLoop) { pylint::rules::useless_else_on_loop(self, stmt, body, orelse); } + if self.enabled(Rule::TryExceptInLoop) { + perflint::rules::try_except_in_loop(self, body); + } } Stmt::For(ast::StmtFor { target, @@ -1553,7 +1495,7 @@ where .. }) => { if self.enabled(Rule::UnusedLoopControlVariable) { - self.deferred.for_loops.push(self.semantic_model.snapshot()); + self.deferred.for_loops.push(self.semantic.snapshot()); } if self.enabled(Rule::LoopVariableOverridesIterator) { flake8_bugbear::rules::loop_variable_overrides_iterator(self, target, iter); @@ -1568,7 +1510,7 @@ where pylint::rules::useless_else_on_loop(self, stmt, body, orelse); } if self.enabled(Rule::RedefinedLoopName) { - pylint::rules::redefined_loop_name(self, &Node::Stmt(stmt)); + pylint::rules::redefined_loop_name(self, stmt); } if self.enabled(Rule::IterationOverSet) { pylint::rules::iteration_over_set(self, iter); @@ -1578,12 +1520,27 @@ where flake8_simplify::rules::convert_for_loop_to_any_all( self, stmt, - self.semantic_model.sibling_stmt(), + self.semantic.sibling_stmt(), ); } if self.enabled(Rule::InDictKeys) { flake8_simplify::rules::key_in_dict_for(self, target, iter); } + if self.enabled(Rule::TryExceptInLoop) { + perflint::rules::try_except_in_loop(self, body); + } + } + if self.enabled(Rule::IncorrectDictIterator) { + perflint::rules::incorrect_dict_iterator(self, target, iter); + } + if self.enabled(Rule::ManualListComprehension) { + perflint::rules::manual_list_comprehension(self, target, body); + } + if self.enabled(Rule::ManualListCopy) { + perflint::rules::manual_list_copy(self, target, body); + } + if self.enabled(Rule::UnnecessaryListCast) { + perflint::rules::unnecessary_list_cast(self, iter); } } Stmt::Try(ast::StmtTry { @@ -1620,9 +1577,7 @@ where pyupgrade::rules::os_error_alias_handlers(self, handlers); } if self.enabled(Rule::PytestAssertInExcept) { - self.diagnostics.extend( - flake8_pytest_style::rules::assert_in_exception_handler(handlers), - ); + flake8_pytest_style::rules::assert_in_exception_handler(self, handlers); } if self.enabled(Rule::SuppressibleException) { flake8_simplify::rules::suppressible_exception( @@ -1663,14 +1618,10 @@ where flake8_bugbear::rules::assignment_to_os_environ(self, targets); } if self.enabled(Rule::HardcodedPasswordString) { - if let Some(diagnostic) = - flake8_bandit::rules::assign_hardcoded_password_string(value, targets) - { - self.diagnostics.push(diagnostic); - } + flake8_bandit::rules::assign_hardcoded_password_string(self, value, targets); } if self.enabled(Rule::GlobalStatement) { - for target in targets.iter() { + for target in targets { if let Expr::Name(ast::ExprName { id, .. }) = target { pylint::rules::global_statement(self, id); } @@ -1708,6 +1659,15 @@ where self.diagnostics.push(diagnostic); } } + if self.settings.rules.enabled(Rule::TypeParamNameMismatch) { + pylint::rules::type_param_name_mismatch(self, value, targets); + } + if self.settings.rules.enabled(Rule::TypeNameIncorrectVariance) { + pylint::rules::type_name_incorrect_variance(self, value); + } + if self.settings.rules.enabled(Rule::TypeBivariance) { + pylint::rules::type_bivariance(self, value); + } if self.is_stub { if self.any_enabled(&[ Rule::UnprefixedTypeParam, @@ -1716,7 +1676,7 @@ where ]) { // Ignore assignments in function bodies; those are covered by other rules. if !self - .semantic_model + .semantic .scopes() .any(|scope| scope.kind.is_any_function()) { @@ -1765,7 +1725,7 @@ where if self.enabled(Rule::AssignmentDefaultInStub) { // Ignore assignments in function bodies; those are covered by other rules. if !self - .semantic_model + .semantic .scopes() .any(|scope| scope.kind.is_any_function()) { @@ -1781,10 +1741,7 @@ where ); } } - if self - .semantic_model - .match_typing_expr(annotation, "TypeAlias") - { + if self.semantic.match_typing_expr(annotation, "TypeAlias") { if self.enabled(Rule::SnakeCaseTypeAlias) { flake8_pyi::rules::snake_case_type_alias(self, target); } @@ -1796,7 +1753,7 @@ where } Stmt::Delete(ast::StmtDelete { targets, range: _ }) => { if self.enabled(Rule::GlobalStatement) { - for target in targets.iter() { + for target in targets { if let Expr::Name(ast::ExprName { id, .. }) = target { pylint::rules::global_statement(self, id); } @@ -1818,7 +1775,7 @@ where } if self.enabled(Rule::AsyncioDanglingTask) { if let Some(diagnostic) = ruff::rules::asyncio_dangling_task(value, |expr| { - self.semantic_model.resolve_call_path(expr) + self.semantic.resolve_call_path(expr) }) { self.diagnostics.push(diagnostic); } @@ -1831,7 +1788,6 @@ where match stmt { Stmt::FunctionDef(ast::StmtFunctionDef { body, - name, args, decorator_list, returns, @@ -1839,7 +1795,6 @@ where }) | Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { body, - name, args, decorator_list, returns, @@ -1853,39 +1808,29 @@ where // Function annotations are always evaluated at runtime, unless future annotations // are enabled. - let runtime_annotation = !self.semantic_model.future_annotations(); + let runtime_annotation = !self.semantic.future_annotations(); - for arg in &args.posonlyargs { - if let Some(expr) = &arg.annotation { + for arg_with_default in args + .posonlyargs + .iter() + .chain(&args.args) + .chain(&args.kwonlyargs) + { + if let Some(expr) = &arg_with_default.def.annotation { if runtime_annotation { - self.visit_type_definition(expr); + self.visit_runtime_annotation(expr); } else { self.visit_annotation(expr); }; } - } - for arg in &args.args { - if let Some(expr) = &arg.annotation { - if runtime_annotation { - self.visit_type_definition(expr); - } else { - self.visit_annotation(expr); - }; + if let Some(expr) = &arg_with_default.default { + self.visit_expr(expr); } } if let Some(arg) = &args.vararg { if let Some(expr) = &arg.annotation { if runtime_annotation { - self.visit_type_definition(expr); - } else { - self.visit_annotation(expr); - }; - } - } - for arg in &args.kwonlyargs { - if let Some(expr) = &arg.annotation { - if runtime_annotation { - self.visit_type_definition(expr); + self.visit_runtime_annotation(expr); } else { self.visit_annotation(expr); }; @@ -1894,7 +1839,7 @@ where if let Some(arg) = &args.kwarg { if let Some(expr) = &arg.annotation { if runtime_annotation { - self.visit_type_definition(expr); + self.visit_runtime_annotation(expr); } else { self.visit_annotation(expr); }; @@ -1902,44 +1847,31 @@ where } for expr in returns { if runtime_annotation { - self.visit_type_definition(expr); + self.visit_runtime_annotation(expr); } else { self.visit_annotation(expr); }; } - for expr in &args.kw_defaults { - self.visit_expr(expr); - } - for expr in &args.defaults { - self.visit_expr(expr); - } - - self.add_binding( - name, - stmt.range(), - BindingKind::FunctionDefinition, - BindingFlags::empty(), - ); let definition = docstrings::extraction::extract_definition( ExtractionTarget::Function, stmt, - self.semantic_model.definition_id, - &self.semantic_model.definitions, + self.semantic.definition_id, + &self.semantic.definitions, ); - self.semantic_model.push_definition(definition); + self.semantic.push_definition(definition); - self.semantic_model.push_scope(match &stmt { + self.semantic.push_scope(match &stmt { Stmt::FunctionDef(stmt) => ScopeKind::Function(stmt), Stmt::AsyncFunctionDef(stmt) => ScopeKind::AsyncFunction(stmt), _ => unreachable!("Expected Stmt::FunctionDef | Stmt::AsyncFunctionDef"), }); - self.deferred.functions.push(self.semantic_model.snapshot()); + self.deferred.functions.push(self.semantic.snapshot()); // Extract any global bindings from the function body. if let Some(globals) = Globals::from_body(body) { - self.semantic_model.set_globals(globals); + self.semantic.set_globals(globals); } } Stmt::ClassDef( @@ -1964,16 +1896,16 @@ where let definition = docstrings::extraction::extract_definition( ExtractionTarget::Class, stmt, - self.semantic_model.definition_id, - &self.semantic_model.definitions, + self.semantic.definition_id, + &self.semantic.definitions, ); - self.semantic_model.push_definition(definition); + self.semantic.push_definition(definition); - self.semantic_model.push_scope(ScopeKind::Class(class_def)); + self.semantic.push_scope(ScopeKind::Class(class_def)); // Extract any global bindings from the class body. if let Some(globals) = Globals::from_body(body) { - self.semantic_model.set_globals(globals); + self.semantic.set_globals(globals); } self.visit_body(body); @@ -1994,7 +1926,7 @@ where }) => { let mut handled_exceptions = Exceptions::empty(); for type_ in extract_handled_exceptions(handlers) { - if let Some(call_path) = self.semantic_model.resolve_call_path(type_) { + if let Some(call_path) = self.semantic.resolve_call_path(type_) { match call_path.as_slice() { ["", "NameError"] => { handled_exceptions |= Exceptions::NAME_ERROR; @@ -2010,14 +1942,11 @@ where } } - self.semantic_model - .handled_exceptions - .push(handled_exceptions); + self.semantic.handled_exceptions.push(handled_exceptions); if self.enabled(Rule::JumpStatementInFinally) { flake8_bugbear::rules::jump_statement_in_finally(self, finalbody); } - if self.enabled(Rule::ContinueInFinally) { if self.settings.target_version <= PythonVersion::Py38 { pylint::rules::continue_in_finally(self, finalbody); @@ -2025,11 +1954,11 @@ where } self.visit_body(body); - self.semantic_model.handled_exceptions.pop(); + self.semantic.handled_exceptions.pop(); - self.semantic_model.flags |= SemanticModelFlags::EXCEPTION_HANDLER; - for excepthandler in handlers { - self.visit_excepthandler(excepthandler); + self.semantic.flags |= SemanticModelFlags::EXCEPTION_HANDLER; + for except_handler in handlers { + self.visit_except_handler(except_handler); } self.visit_body(orelse); @@ -2044,8 +1973,8 @@ where // If we're in a class or module scope, then the annotation needs to be // available at runtime. // See: https://docs.python.org/3/reference/simple_stmts.html#annotated-assignment-statements - let runtime_annotation = if self.semantic_model.future_annotations() { - if self.semantic_model.scope().kind.is_class() { + let runtime_annotation = if self.semantic.future_annotations() { + if self.semantic.scope().kind.is_class() { let baseclasses = &self .settings .flake8_type_checking @@ -2055,30 +1984,27 @@ where .flake8_type_checking .runtime_evaluated_decorators; flake8_type_checking::helpers::runtime_evaluated( - &self.semantic_model, baseclasses, decorators, + &self.semantic, ) } else { false } } else { matches!( - self.semantic_model.scope().kind, + self.semantic.scope().kind, ScopeKind::Class(_) | ScopeKind::Module ) }; if runtime_annotation { - self.visit_type_definition(annotation); + self.visit_runtime_annotation(annotation); } else { self.visit_annotation(annotation); } if let Some(expr) = value { - if self - .semantic_model - .match_typing_expr(annotation, "TypeAlias") - { + if self.semantic.match_typing_expr(annotation, "TypeAlias") { self.visit_type_definition(expr); } else { self.visit_expr(expr); @@ -2116,11 +2042,10 @@ where ) => { self.visit_boolean_test(test); - if analyze::typing::is_type_checking_block(stmt_if, &self.semantic_model) { - if self.semantic_model.at_top_level() { + if typing::is_type_checking_block(stmt_if, &self.semantic) { + if self.semantic.at_top_level() { self.importer.visit_type_checking_block(stmt); } - if self.enabled(Rule::EmptyTypeCheckingBlock) { flake8_type_checking::rules::empty_type_checking_block(self, stmt_if); } @@ -2137,39 +2062,50 @@ where // Post-visit. match stmt { - Stmt::FunctionDef(_) | Stmt::AsyncFunctionDef(_) => { - self.semantic_model.pop_scope(); - self.semantic_model.pop_definition(); - } - Stmt::ClassDef(ast::StmtClassDef { name, .. }) => { - self.semantic_model.pop_scope(); - self.semantic_model.pop_definition(); + Stmt::FunctionDef(ast::StmtFunctionDef { name, .. }) + | Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { name, .. }) => { + let scope_id = self.semantic.scope_id; + self.deferred.scopes.push(scope_id); + self.semantic.pop_scope(); + self.semantic.pop_definition(); self.add_binding( name, - stmt.range(), - BindingKind::ClassDefinition, + stmt.identifier(), + BindingKind::FunctionDefinition(scope_id), + BindingFlags::empty(), + ); + } + Stmt::ClassDef(ast::StmtClassDef { name, .. }) => { + let scope_id = self.semantic.scope_id; + self.deferred.scopes.push(scope_id); + self.semantic.pop_scope(); + self.semantic.pop_definition(); + self.add_binding( + name, + stmt.identifier(), + BindingKind::ClassDefinition(scope_id), BindingFlags::empty(), ); } _ => {} } - self.semantic_model.flags = flags_snapshot; - self.semantic_model.pop_stmt(); + self.semantic.flags = flags_snapshot; + self.semantic.pop_stmt(); } fn visit_annotation(&mut self, expr: &'b Expr) { - let flags_snapshot = self.semantic_model.flags; - self.semantic_model.flags |= SemanticModelFlags::ANNOTATION; + let flags_snapshot = self.semantic.flags; + self.semantic.flags |= SemanticModelFlags::TYPING_ONLY_ANNOTATION; self.visit_type_definition(expr); - self.semantic_model.flags = flags_snapshot; + self.semantic.flags = flags_snapshot; } fn visit_expr(&mut self, expr: &'b Expr) { - if !self.semantic_model.in_f_string() - && !self.semantic_model.in_deferred_type_definition() - && self.semantic_model.in_type_definition() - && self.semantic_model.future_annotations() + if !self.semantic.in_f_string() + && !self.semantic.in_deferred_type_definition() + && self.semantic.in_type_definition() + && self.semantic.future_annotations() { if let Expr::Constant(ast::ExprConstant { value: Constant::Str(value), @@ -2179,21 +2115,21 @@ where self.deferred.string_type_definitions.push(( expr.range(), value, - self.semantic_model.snapshot(), + self.semantic.snapshot(), )); } else { self.deferred .future_type_definitions - .push((expr, self.semantic_model.snapshot())); + .push((expr, self.semantic.snapshot())); } return; } - self.semantic_model.push_expr(expr); + self.semantic.push_expr(expr); // Store the flags prior to any further descent, so that we can restore them after visiting // the node. - let flags_snapshot = self.semantic_model.flags; + let flags_snapshot = self.semantic.flags; // If we're in a boolean test (e.g., the `test` of a `Stmt::If`), but now within a // subexpression (e.g., `a` in `f(a)`), then we're no longer in a boolean test. @@ -2201,29 +2137,30 @@ where expr, Expr::BoolOp(_) | Expr::UnaryOp(ast::ExprUnaryOp { - op: Unaryop::Not, + op: UnaryOp::Not, .. }) ) { - self.semantic_model.flags -= SemanticModelFlags::BOOLEAN_TEST; + self.semantic.flags -= SemanticModelFlags::BOOLEAN_TEST; } // Pre-visit. match expr { - Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => { + Expr::Subscript(subscript @ ast::ExprSubscript { value, slice, .. }) => { // Ex) Optional[...], Union[...] if self.any_enabled(&[ Rule::FutureRewritableTypeAnnotation, Rule::NonPEP604Annotation, ]) { - if let Some(operator) = - analyze::typing::to_pep604_operator(value, slice, &self.semantic_model) + if let Some(operator) = typing::to_pep604_operator(value, slice, &self.semantic) { if self.enabled(Rule::FutureRewritableTypeAnnotation) { - if self.settings.target_version < PythonVersion::Py310 + if !self.is_stub + && self.settings.target_version < PythonVersion::Py310 && self.settings.target_version >= PythonVersion::Py37 - && !self.semantic_model.future_annotations() - && self.semantic_model.in_annotation() + && !self.semantic.future_annotations() + && self.semantic.in_annotation() + && !self.settings.pyupgrade.keep_runtime_typing { flake8_future_annotations::rules::future_rewritable_type_annotation( self, value, @@ -2231,10 +2168,12 @@ where } } if self.enabled(Rule::NonPEP604Annotation) { - if self.settings.target_version >= PythonVersion::Py310 + if self.is_stub + || self.settings.target_version >= PythonVersion::Py310 || (self.settings.target_version >= PythonVersion::Py37 - && self.semantic_model.future_annotations() - && self.semantic_model.in_annotation()) + && self.semantic.future_annotations() + && self.semantic.in_annotation() + && !self.settings.pyupgrade.keep_runtime_typing) { pyupgrade::rules::use_pep604_annotation( self, expr, slice, operator, @@ -2246,10 +2185,11 @@ where // Ex) list[...] if self.enabled(Rule::FutureRequiredTypeAnnotation) { - if self.settings.target_version < PythonVersion::Py39 - && !self.semantic_model.future_annotations() - && self.semantic_model.in_annotation() - && analyze::typing::is_pep585_generic(value, &self.semantic_model) + if !self.is_stub + && self.settings.target_version < PythonVersion::Py39 + && !self.semantic.future_annotations() + && self.semantic.in_annotation() + && typing::is_pep585_generic(value, &self.semantic) { flake8_future_annotations::rules::future_required_type_annotation( self, @@ -2259,10 +2199,31 @@ where } } - if self.semantic_model.match_typing_expr(value, "Literal") { - self.semantic_model.flags |= SemanticModelFlags::LITERAL; + // Ex) Union[...] + if self.any_enabled(&[Rule::UnnecessaryLiteralUnion, Rule::DuplicateUnionMember]) { + // Determine if the current expression is an union + // Avoid duplicate checks if the parent is an `Union[...]` since these rules traverse nested unions + let is_unchecked_union = self + .semantic + .expr_grandparent() + .and_then(Expr::as_subscript_expr) + .map_or(true, |parent| { + !self.semantic.match_typing_expr(&parent.value, "Union") + }); + + if is_unchecked_union { + if self.enabled(Rule::UnnecessaryLiteralUnion) { + flake8_pyi::rules::unnecessary_literal_union(self, expr); + } + if self.enabled(Rule::DuplicateUnionMember) { + flake8_pyi::rules::duplicate_union_member(self, expr); + } + } } + if self.semantic.match_typing_expr(value, "Literal") { + self.semantic.flags |= SemanticModelFlags::LITERAL; + } if self.any_enabled(&[ Rule::SysVersionSlice3, Rule::SysVersion2, @@ -2271,10 +2232,16 @@ where ]) { flake8_2020::rules::subscript(self, value, slice); } - if self.enabled(Rule::UncapitalizedEnvironmentVariables) { flake8_simplify::rules::use_capital_environment_variables(self, expr); } + if self.enabled(Rule::UnnecessaryIterableAllocationForFirstElement) { + ruff::rules::unnecessary_iterable_allocation_for_first_element(self, subscript); + } + + if self.enabled(Rule::InvalidIndexType) { + ruff::rules::invalid_index_type(self, subscript); + } pandas_vet::rules::subscript(self, value, expr); } @@ -2312,6 +2279,9 @@ where if self.enabled(Rule::NumpyDeprecatedTypeAlias) { numpy::rules::deprecated_type_alias(self, expr); } + if self.enabled(Rule::NumpyDeprecatedFunction) { + numpy::rules::deprecated_function(self, expr); + } if self.is_stub { if self.enabled(Rule::CollectionsNamedTuple) { flake8_pyi::rules::collections_named_tuple(self, expr); @@ -2324,24 +2294,28 @@ where Rule::NonPEP585Annotation, ]) { if let Some(replacement) = - analyze::typing::to_pep585_generic(expr, &self.semantic_model) + typing::to_pep585_generic(expr, &self.semantic) { if self.enabled(Rule::FutureRewritableTypeAnnotation) { - if self.settings.target_version < PythonVersion::Py39 + if !self.is_stub + && self.settings.target_version < PythonVersion::Py39 && self.settings.target_version >= PythonVersion::Py37 - && !self.semantic_model.future_annotations() - && self.semantic_model.in_annotation() + && !self.semantic.future_annotations() + && self.semantic.in_annotation() + && !self.settings.pyupgrade.keep_runtime_typing { flake8_future_annotations::rules::future_rewritable_type_annotation( - self, expr, - ); + self, expr, + ); } } if self.enabled(Rule::NonPEP585Annotation) { - if self.settings.target_version >= PythonVersion::Py39 + if self.is_stub + || self.settings.target_version >= PythonVersion::Py39 || (self.settings.target_version >= PythonVersion::Py37 - && self.semantic_model.future_annotations() - && self.semantic_model.in_annotation()) + && self.semantic.future_annotations() + && self.semantic.in_annotation() + && !self.settings.pyupgrade.keep_runtime_typing) { pyupgrade::rules::use_pep585_annotation( self, @@ -2364,10 +2338,11 @@ where } } - if self.semantic_model.scope().kind.is_class() { + if let ScopeKind::Class(class_def) = self.semantic.scope().kind { if self.enabled(Rule::BuiltinAttributeShadowing) { flake8_builtins::rules::builtin_attribute_shadowing( self, + class_def, id, AnyShadowing::from(expr), ); @@ -2386,11 +2361,9 @@ where } ExprContext::Del => self.handle_node_delete(expr), } - if self.enabled(Rule::SixPY3) { flake8_2020::rules::name_or_attribute(self, expr); } - if self.enabled(Rule::LoadBeforeGlobalDeclaration) { pylint::rules::load_before_global_declaration(self, id, expr); } @@ -2401,14 +2374,14 @@ where Rule::FutureRewritableTypeAnnotation, Rule::NonPEP585Annotation, ]) { - if let Some(replacement) = - analyze::typing::to_pep585_generic(expr, &self.semantic_model) - { + if let Some(replacement) = typing::to_pep585_generic(expr, &self.semantic) { if self.enabled(Rule::FutureRewritableTypeAnnotation) { - if self.settings.target_version < PythonVersion::Py39 + if !self.is_stub + && self.settings.target_version < PythonVersion::Py39 && self.settings.target_version >= PythonVersion::Py37 - && !self.semantic_model.future_annotations() - && self.semantic_model.in_annotation() + && !self.semantic.future_annotations() + && self.semantic.in_annotation() + && !self.settings.pyupgrade.keep_runtime_typing { flake8_future_annotations::rules::future_rewritable_type_annotation( self, expr, @@ -2416,10 +2389,12 @@ where } } if self.enabled(Rule::NonPEP585Annotation) { - if self.settings.target_version >= PythonVersion::Py39 + if self.is_stub + || self.settings.target_version >= PythonVersion::Py39 || (self.settings.target_version >= PythonVersion::Py37 - && self.semantic_model.future_annotations() - && self.semantic_model.in_annotation()) + && self.semantic.future_annotations() + && self.semantic.in_annotation() + && !self.settings.pyupgrade.keep_runtime_typing) { pyupgrade::rules::use_pep585_annotation(self, expr, &replacement); } @@ -2437,6 +2412,9 @@ where if self.enabled(Rule::NumpyDeprecatedTypeAlias) { numpy::rules::deprecated_type_alias(self, expr); } + if self.enabled(Rule::NumpyDeprecatedFunction) { + numpy::rules::deprecated_function(self, expr); + } if self.enabled(Rule::DeprecatedMockImport) { pyupgrade::rules::deprecated_mock_attribute(self, expr); } @@ -2456,12 +2434,21 @@ where } pandas_vet::rules::attr(self, attr, value, expr); } - Expr::Call(ast::ExprCall { - func, - args, - keywords, - range: _, - }) => { + Expr::Call( + call @ ast::ExprCall { + func, + args, + keywords, + range: _, + }, + ) => { + if let Expr::Name(ast::ExprName { id, ctx, range: _ }) = func.as_ref() { + if id == "locals" && matches!(ctx, ExprContext::Load) { + let scope = self.semantic.scope_mut(); + scope.set_uses_locals(); + } + } + if self.any_enabled(&[ // pyflakes Rule::StringDotFormatInvalidFormat, @@ -2478,19 +2465,19 @@ where if let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = func.as_ref() { let attr = attr.as_str(); if let Expr::Constant(ast::ExprConstant { - value: Constant::Str(value), + value: Constant::Str(val), .. }) = value.as_ref() { if attr == "join" { // "...".join(...) call if self.enabled(Rule::StaticJoinToFString) { - flynt::rules::static_join_to_fstring(self, expr, value); + flynt::rules::static_join_to_fstring(self, expr, val); } } else if attr == "format" { // "...".format(...) call let location = expr.range(); - match pyflakes::format::FormatSummary::try_from(value.as_ref()) { + match pyflakes::format::FormatSummary::try_from(val.as_ref()) { Err(e) => { if self.enabled(Rule::StringDotFormatInvalidFormat) { self.diagnostics.push(Diagnostic::new( @@ -2534,7 +2521,13 @@ where } if self.enabled(Rule::FString) { - pyupgrade::rules::f_strings(self, &summary, expr); + pyupgrade::rules::f_strings( + self, + &summary, + expr, + value, + self.settings.line_length, + ); } } } @@ -2542,8 +2535,6 @@ where } } } - - // pyupgrade if self.enabled(Rule::TypeOfPrimitive) { pyupgrade::rules::type_of_primitive(self, expr, func, args); } @@ -2574,13 +2565,11 @@ where if self.enabled(Rule::OSErrorAlias) { pyupgrade::rules::os_error_alias_call(self, func); } - if self.enabled(Rule::NonPEP604Isinstance) - && self.settings.target_version >= PythonVersion::Py310 - { - pyupgrade::rules::use_pep604_isinstance(self, expr, func, args); + if self.enabled(Rule::NonPEP604Isinstance) { + if self.settings.target_version >= PythonVersion::Py310 { + pyupgrade::rules::use_pep604_isinstance(self, expr, func, args); + } } - - // flake8-async if self.enabled(Rule::BlockingHttpCallInAsyncFunction) { flake8_async::rules::blocking_http_call(self, expr); } @@ -2590,13 +2579,9 @@ where if self.enabled(Rule::BlockingOsCallInAsyncFunction) { flake8_async::rules::blocking_os_call(self, expr); } - - // flake8-print if self.any_enabled(&[Rule::Print, Rule::PPrint]) { flake8_print::rules::print_call(self, func, keywords); } - - // flake8-bandit if self.any_enabled(&[ Rule::SuspiciousPickleUsage, Rule::SuspiciousMarshalUsage, @@ -2622,8 +2607,9 @@ where ]) { flake8_bandit::rules::suspicious_function_call(self, expr); } - - // flake8-bugbear + if self.enabled(Rule::ReSubPositionalArgs) { + flake8_bugbear::rules::re_sub_positional_args(self, call); + } if self.enabled(Rule::UnreliableCallableCheck) { flake8_bugbear::rules::unreliable_callable_check(self, expr, func, args); } @@ -2644,27 +2630,21 @@ where self, args, keywords, ); } - if self.enabled(Rule::ZipWithoutExplicitStrict) - && self.settings.target_version >= PythonVersion::Py310 - { - flake8_bugbear::rules::zip_without_explicit_strict( - self, expr, func, args, keywords, - ); + if self.enabled(Rule::ZipWithoutExplicitStrict) { + if self.settings.target_version >= PythonVersion::Py310 { + flake8_bugbear::rules::zip_without_explicit_strict( + self, expr, func, args, keywords, + ); + } } if self.enabled(Rule::NoExplicitStacklevel) { flake8_bugbear::rules::no_explicit_stacklevel(self, func, args, keywords); } - - // flake8-pie if self.enabled(Rule::UnnecessaryDictKwargs) { flake8_pie::rules::unnecessary_dict_kwargs(self, expr, keywords); } - - // flake8-bandit if self.enabled(Rule::ExecBuiltin) { - if let Some(diagnostic) = flake8_bandit::rules::exec_used(expr, func) { - self.diagnostics.push(diagnostic); - } + flake8_bandit::rules::exec_used(self, func); } if self.enabled(Rule::BadFilePermissions) { flake8_bandit::rules::bad_file_permissions(self, func, args, keywords); @@ -2687,8 +2667,7 @@ where flake8_bandit::rules::jinja2_autoescape_false(self, func, args, keywords); } if self.enabled(Rule::HardcodedPasswordFuncArg) { - self.diagnostics - .extend(flake8_bandit::rules::hardcoded_password_func_arg(keywords)); + flake8_bandit::rules::hardcoded_password_func_arg(self, keywords); } if self.enabled(Rule::HardcodedSQLExpression) { flake8_bandit::rules::hardcoded_sql_expression(self, expr); @@ -2720,8 +2699,6 @@ where ]) { flake8_bandit::rules::shell_injection(self, func, args, keywords); } - - // flake8-comprehensions if self.enabled(Rule::UnnecessaryGeneratorList) { flake8_comprehensions::rules::unnecessary_generator_list( self, expr, func, args, keywords, @@ -2804,7 +2781,7 @@ where flake8_comprehensions::rules::unnecessary_map( self, expr, - self.semantic_model.expr_parent(), + self.semantic.expr_parent(), func, args, ); @@ -2814,41 +2791,22 @@ where self, expr, func, args, keywords, ); } - - // flake8-boolean-trap if self.enabled(Rule::BooleanPositionalValueInFunctionCall) { flake8_boolean_trap::rules::check_boolean_positional_value_in_function_call( self, args, func, ); } - if let Expr::Name(ast::ExprName { id, ctx, range: _ }) = func.as_ref() { - if id == "locals" && matches!(ctx, ExprContext::Load) { - let scope = self.semantic_model.scope_mut(); - scope.set_uses_locals(); - } - } - - // flake8-debugger if self.enabled(Rule::Debugger) { flake8_debugger::rules::debugger_call(self, expr, func); } - - // pandas-vet if self.enabled(Rule::PandasUseOfInplaceArgument) { - self.diagnostics.extend( - pandas_vet::rules::inplace_argument(self, expr, func, args, keywords) - .into_iter(), - ); + pandas_vet::rules::inplace_argument(self, expr, func, args, keywords); } pandas_vet::rules::call(self, func); if self.enabled(Rule::PandasUseOfPdMerge) { - if let Some(diagnostic) = pandas_vet::rules::use_of_pd_merge(func) { - self.diagnostics.push(diagnostic); - }; + pandas_vet::rules::use_of_pd_merge(self, func); } - - // flake8-datetimez if self.enabled(Rule::CallDatetimeWithoutTzinfo) { flake8_datetimez::rules::call_datetime_without_tzinfo( self, @@ -2903,16 +2861,12 @@ where if self.enabled(Rule::CallDateFromtimestamp) { flake8_datetimez::rules::call_date_fromtimestamp(self, func, expr.range()); } - - // pygrep-hooks if self.enabled(Rule::Eval) { pygrep_hooks::rules::no_eval(self, func); } if self.enabled(Rule::DeprecatedLogWarn) { pygrep_hooks::rules::deprecated_log_warn(self, func); } - - // pylint if self.enabled(Rule::UnnecessaryDirectLambdaCall) { pylint::rules::unnecessary_direct_lambda_call(self, expr, func); } @@ -2931,8 +2885,6 @@ where if self.enabled(Rule::NestedMinMax) { pylint::rules::nested_min_max(self, expr, func, args, keywords); } - - // flake8-pytest-style if self.enabled(Rule::PytestPatchWithLambda) { if let Some(diagnostic) = flake8_pytest_style::rules::patch_with_lambda(func, args, keywords) @@ -2947,25 +2899,20 @@ where self.diagnostics.push(diagnostic); } } - if self.any_enabled(&[ Rule::PytestRaisesWithoutException, Rule::PytestRaisesTooBroad, ]) { flake8_pytest_style::rules::raises_call(self, func, args, keywords); } - if self.enabled(Rule::PytestFailWithoutMessage) { flake8_pytest_style::rules::fail_call(self, func, args, keywords); } - if self.enabled(Rule::PairwiseOverZipped) { if self.settings.target_version >= PythonVersion::Py310 { ruff::rules::pairwise_over_zipped(self, func, args); } } - - // flake8-gettext if self.any_enabled(&[ Rule::FStringInGetTextFuncCall, Rule::FormatInGetTextFuncCall, @@ -2975,33 +2922,24 @@ where &self.settings.flake8_gettext.functions_names, ) { if self.enabled(Rule::FStringInGetTextFuncCall) { - self.diagnostics - .extend(flake8_gettext::rules::f_string_in_gettext_func_call(args)); + flake8_gettext::rules::f_string_in_gettext_func_call(self, args); } if self.enabled(Rule::FormatInGetTextFuncCall) { - self.diagnostics - .extend(flake8_gettext::rules::format_in_gettext_func_call(args)); + flake8_gettext::rules::format_in_gettext_func_call(self, args); } if self.enabled(Rule::PrintfInGetTextFuncCall) { - self.diagnostics - .extend(flake8_gettext::rules::printf_in_gettext_func_call(args)); + flake8_gettext::rules::printf_in_gettext_func_call(self, args); } } - - // flake8-simplify if self.enabled(Rule::UncapitalizedEnvironmentVariables) { flake8_simplify::rules::use_capital_environment_variables(self, expr); } - if self.enabled(Rule::OpenFileWithContextHandler) { flake8_simplify::rules::open_file_with_context_handler(self, func); } - if self.enabled(Rule::DictGetWithNoneDefault) { flake8_simplify::rules::dict_get_with_none_default(self, expr); } - - // flake8-use-pathlib if self.any_enabled(&[ Rule::OsPathAbspath, Rule::OsChmod, @@ -3030,13 +2968,9 @@ where ]) { flake8_use_pathlib::rules::replaceable_by_pathlib(self, func); } - - // numpy if self.enabled(Rule::NumpyLegacyRandom) { - numpy::rules::numpy_legacy_random(self, func); + numpy::rules::legacy_random(self, func); } - - // flake8-logging-format if self.any_enabled(&[ Rule::LoggingStringFormat, Rule::LoggingPercentFormat, @@ -3049,13 +2983,9 @@ where ]) { flake8_logging_format::rules::logging_call(self, func, args, keywords); } - - // pylint logging checker if self.any_enabled(&[Rule::LoggingTooFewArgs, Rule::LoggingTooManyArgs]) { pylint::rules::logging_call(self, func, args, keywords); } - - // flake8-django if self.enabled(Rule::DjangoLocalsInRenderFunction) { flake8_django::rules::locals_in_render_function(self, func, args, keywords); } @@ -3071,7 +3001,6 @@ where ]) { pyflakes::rules::repeated_keys(self, keys, values); } - if self.enabled(Rule::UnnecessarySpread) { flake8_pie::rules::unnecessary_spread(self, keys, values); } @@ -3214,7 +3143,6 @@ where } } } - if self.enabled(Rule::PrintfStringFormatting) { pyupgrade::rules::printf_string_formatting(self, expr, right, self.locator); } @@ -3247,9 +3175,10 @@ where }) => { // Ex) `str | None` if self.enabled(Rule::FutureRequiredTypeAnnotation) { - if self.settings.target_version < PythonVersion::Py310 - && !self.semantic_model.future_annotations() - && self.semantic_model.in_annotation() + if !self.is_stub + && self.settings.target_version < PythonVersion::Py310 + && !self.semantic.future_annotations() + && self.semantic.in_annotation() { flake8_future_annotations::rules::future_required_type_annotation( self, @@ -3258,22 +3187,26 @@ where ); } } - if self.is_stub { if self.enabled(Rule::DuplicateUnionMember) - && self.semantic_model.in_type_definition() - && self.semantic_model.expr_parent().map_or(true, |parent| { - !matches!( - parent, - Expr::BinOp(ast::ExprBinOp { - op: Operator::BitOr, - .. - }) - ) - }) + && self.semantic.in_type_definition() + // Avoid duplicate checks if the parent is an `|` + && !matches!( + self.semantic.expr_parent(), + Some(Expr::BinOp(ast::ExprBinOp { op: Operator::BitOr, ..})) + ) { flake8_pyi::rules::duplicate_union_member(self, expr); } + if self.enabled(Rule::UnnecessaryLiteralUnion) + // Avoid duplicate checks if the parent is an `|` + && !matches!( + self.semantic.expr_parent(), + Some(Expr::BinOp(ast::ExprBinOp { op: Operator::BitOr, ..})) + ) + { + flake8_pyi::rules::unnecessary_literal_union(self, expr); + } } } Expr::UnaryOp(ast::ExprUnaryOp { @@ -3293,11 +3226,9 @@ where check_not_is, ); } - if self.enabled(Rule::UnaryPrefixIncrement) { flake8_bugbear::rules::unary_prefix_increment(self, expr, *op, operand); } - if self.enabled(Rule::NegateEqualOp) { flake8_simplify::rules::negation_with_equal_op(self, expr, *op, operand); } @@ -3327,15 +3258,12 @@ where check_true_false_comparisons, ); } - if self.enabled(Rule::IsLiteral) { pyflakes::rules::invalid_literal_comparison(self, left, ops, comparators, expr); } - if self.enabled(Rule::TypeComparison) { pycodestyle::rules::type_comparison(self, expr, ops, comparators); } - if self.any_enabled(&[ Rule::SysVersionCmpStr3, Rule::SysVersionInfo0Eq3, @@ -3345,64 +3273,31 @@ where ]) { flake8_2020::rules::compare(self, left, ops, comparators); } - if self.enabled(Rule::HardcodedPasswordString) { - self.diagnostics.extend( - flake8_bandit::rules::compare_to_hardcoded_password_string( - left, - comparators, - ), + flake8_bandit::rules::compare_to_hardcoded_password_string( + self, + left, + comparators, ); } - if self.enabled(Rule::ComparisonWithItself) { pylint::rules::comparison_with_itself(self, left, ops, comparators); } - if self.enabled(Rule::ComparisonOfConstant) { pylint::rules::comparison_of_constant(self, left, ops, comparators); } - if self.enabled(Rule::CompareToEmptyString) { pylint::rules::compare_to_empty_string(self, left, ops, comparators); } - if self.enabled(Rule::MagicValueComparison) { pylint::rules::magic_value_comparison(self, left, comparators); } - if self.enabled(Rule::InDictKeys) { flake8_simplify::rules::key_in_dict_compare(self, expr, left, ops, comparators); } - if self.enabled(Rule::YodaConditions) { flake8_simplify::rules::yoda_conditions(self, expr, left, ops, comparators); } - - if self.is_stub { - if self.any_enabled(&[ - Rule::UnrecognizedPlatformCheck, - Rule::UnrecognizedPlatformName, - ]) { - flake8_pyi::rules::unrecognized_platform( - self, - expr, - left, - ops, - comparators, - ); - } - - if self.enabled(Rule::BadVersionInfoComparison) { - flake8_pyi::rules::bad_version_info_comparison( - self, - expr, - left, - ops, - comparators, - ); - } - } } Expr::Constant(ast::ExprConstant { value: Constant::Int(_) | Constant::Float(_) | Constant::Complex { .. }, @@ -3427,14 +3322,14 @@ where kind, range: _, }) => { - if self.semantic_model.in_type_definition() - && !self.semantic_model.in_literal() - && !self.semantic_model.in_f_string() + if self.semantic.in_type_definition() + && !self.semantic.in_literal() + && !self.semantic.in_f_string() { self.deferred.string_type_definitions.push(( expr.range(), value, - self.semantic_model.snapshot(), + self.semantic.snapshot(), )); } if self.enabled(Rule::HardcodedBindAllInterfaces) { @@ -3472,13 +3367,22 @@ where } // Visit the default arguments, but avoid the body, which will be deferred. - for expr in &args.kw_defaults { - self.visit_expr(expr); + for ArgWithDefault { + default, + def: _, + range: _, + } in args + .posonlyargs + .iter() + .chain(&args.args) + .chain(&args.kwonlyargs) + { + if let Some(expr) = &default { + self.visit_expr(expr); + } } - for expr in &args.defaults { - self.visit_expr(expr); - } - self.semantic_model.push_scope(ScopeKind::Lambda(lambda)); + + self.semantic.push_scope(ScopeKind::Lambda(lambda)); } Expr::IfExp(ast::ExprIfExp { test, @@ -3588,11 +3492,13 @@ where } } } - Expr::BoolOp(ast::ExprBoolOp { - op, - values, - range: _, - }) => { + Expr::BoolOp( + bool_op @ ast::ExprBoolOp { + op, + values, + range: _, + }, + ) => { if self.enabled(Rule::RepeatedIsinstanceCalls) { pylint::rules::repeated_isinstance_calls(self, expr, *op, values); } @@ -3617,6 +3523,9 @@ where if self.enabled(Rule::ExprAndFalse) { flake8_simplify::rules::expr_and_false(self, expr); } + if self.enabled(Rule::RepeatedEqualityComparisonTarget) { + pylint::rules::repeated_equality_comparison_target(self, bool_op); + } } _ => {} }; @@ -3652,9 +3561,7 @@ where self.visit_expr(value); } Expr::Lambda(_) => { - self.deferred - .lambdas - .push((expr, self.semantic_model.snapshot())); + self.deferred.lambdas.push((expr, self.semantic.snapshot())); } Expr::IfExp(ast::ExprIfExp { test, @@ -3672,55 +3579,44 @@ where keywords, range: _, }) => { - let callable = self - .semantic_model - .resolve_call_path(func) - .and_then(|call_path| { - if self - .semantic_model - .match_typing_call_path(&call_path, "cast") - { - Some(Callable::Cast) - } else if self - .semantic_model - .match_typing_call_path(&call_path, "NewType") - { - Some(Callable::NewType) - } else if self - .semantic_model - .match_typing_call_path(&call_path, "TypeVar") - { - Some(Callable::TypeVar) - } else if self - .semantic_model - .match_typing_call_path(&call_path, "NamedTuple") - { - Some(Callable::NamedTuple) - } else if self - .semantic_model - .match_typing_call_path(&call_path, "TypedDict") - { - Some(Callable::TypedDict) - } else if [ - "Arg", - "DefaultArg", - "NamedArg", - "DefaultNamedArg", - "VarArg", - "KwArg", + let callable = self.semantic.resolve_call_path(func).and_then(|call_path| { + if self.semantic.match_typing_call_path(&call_path, "cast") { + Some(typing::Callable::Cast) + } else if self.semantic.match_typing_call_path(&call_path, "NewType") { + Some(typing::Callable::NewType) + } else if self.semantic.match_typing_call_path(&call_path, "TypeVar") { + Some(typing::Callable::TypeVar) + } else if self + .semantic + .match_typing_call_path(&call_path, "NamedTuple") + { + Some(typing::Callable::NamedTuple) + } else if self + .semantic + .match_typing_call_path(&call_path, "TypedDict") + { + Some(typing::Callable::TypedDict) + } else if matches!( + call_path.as_slice(), + [ + "mypy_extensions", + "Arg" + | "DefaultArg" + | "NamedArg" + | "DefaultNamedArg" + | "VarArg" + | "KwArg" ] - .iter() - .any(|target| call_path.as_slice() == ["mypy_extensions", target]) - { - Some(Callable::MypyExtension) - } else if call_path.as_slice() == ["", "bool"] { - Some(Callable::Bool) - } else { - None - } - }); + ) { + Some(typing::Callable::MypyExtension) + } else if matches!(call_path.as_slice(), ["", "bool"]) { + Some(typing::Callable::Bool) + } else { + None + } + }); match callable { - Some(Callable::Bool) => { + Some(typing::Callable::Bool) => { self.visit_expr(func); let mut args = args.iter(); if let Some(arg) = args.next() { @@ -3730,7 +3626,7 @@ where self.visit_expr(arg); } } - Some(Callable::Cast) => { + Some(typing::Callable::Cast) => { self.visit_expr(func); let mut args = args.iter(); if let Some(arg) = args.next() { @@ -3740,7 +3636,7 @@ where self.visit_expr(arg); } } - Some(Callable::NewType) => { + Some(typing::Callable::NewType) => { self.visit_expr(func); let mut args = args.iter(); if let Some(arg) = args.next() { @@ -3750,7 +3646,7 @@ where self.visit_type_definition(arg); } } - Some(Callable::TypeVar) => { + Some(typing::Callable::TypeVar) => { self.visit_expr(func); let mut args = args.iter(); if let Some(arg) = args.next() { @@ -3774,7 +3670,7 @@ where } } } - Some(Callable::NamedTuple) => { + Some(typing::Callable::NamedTuple) => { self.visit_expr(func); // Ex) NamedTuple("a", [("a", int)]) @@ -3811,7 +3707,7 @@ where self.visit_type_definition(value); } } - Some(Callable::TypedDict) => { + Some(typing::Callable::TypedDict) => { self.visit_expr(func); // Ex) TypedDict("a", {"a": int}) @@ -3843,7 +3739,7 @@ where self.visit_type_definition(value); } } - Some(Callable::MypyExtension) => { + Some(typing::Callable::MypyExtension) => { self.visit_expr(func); let mut args = args.iter(); @@ -3899,28 +3795,28 @@ where // `obj["foo"]["bar"]`, we need to avoid treating the `obj["foo"]` // portion as an annotation, despite having `ExprContext::Load`. Thus, we track // the `ExprContext` at the top-level. - if self.semantic_model.in_subscript() { + if self.semantic.in_subscript() { visitor::walk_expr(self, expr); } else if matches!(ctx, ExprContext::Store | ExprContext::Del) { - self.semantic_model.flags |= SemanticModelFlags::SUBSCRIPT; + self.semantic.flags |= SemanticModelFlags::SUBSCRIPT; visitor::walk_expr(self, expr); } else { - match analyze::typing::match_annotated_subscript( + match typing::match_annotated_subscript( value, - &self.semantic_model, + &self.semantic, self.settings.typing_modules.iter().map(String::as_str), &self.settings.pyflakes.extend_generics, ) { Some(subscript) => { match subscript { // Ex) Optional[int] - SubscriptKind::AnnotatedSubscript => { + typing::SubscriptKind::AnnotatedSubscript => { self.visit_expr(value); self.visit_type_definition(slice); self.visit_expr_context(ctx); } // Ex) Annotated[int, "Hello, world!"] - SubscriptKind::PEP593AnnotatedSubscript => { + typing::SubscriptKind::PEP593AnnotatedSubscript => { // First argument is a type (including forward references); the // rest are arbitrary Python objects. self.visit_expr(value); @@ -3951,7 +3847,7 @@ where } } Expr::JoinedStr(_) => { - self.semantic_model.flags |= if self.semantic_model.in_f_string() { + self.semantic.flags |= if self.semantic.in_f_string() { SemanticModelFlags::NESTED_F_STRING } else { SemanticModelFlags::F_STRING @@ -3968,18 +3864,19 @@ where | Expr::ListComp(_) | Expr::DictComp(_) | Expr::SetComp(_) => { - self.semantic_model.pop_scope(); + self.deferred.scopes.push(self.semantic.scope_id); + self.semantic.pop_scope(); } _ => {} }; - self.semantic_model.flags = flags_snapshot; - self.semantic_model.pop_expr(); + self.semantic.flags = flags_snapshot; + self.semantic.pop_expr(); } - fn visit_excepthandler(&mut self, excepthandler: &'b Excepthandler) { - match excepthandler { - Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { + fn visit_except_handler(&mut self, except_handler: &'b ExceptHandler) { + match except_handler { + ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { type_, name, body, @@ -3990,7 +3887,7 @@ where if let Some(diagnostic) = pycodestyle::rules::bare_except( type_.as_deref(), body, - excepthandler, + except_handler, self.locator, ) { self.diagnostics.push(diagnostic); @@ -4005,7 +3902,7 @@ where if self.enabled(Rule::TryExceptPass) { flake8_bandit::rules::try_except_pass( self, - excepthandler, + except_handler, type_.as_deref(), name, body, @@ -4015,7 +3912,7 @@ where if self.enabled(Rule::TryExceptContinue) { flake8_bandit::rules::try_except_continue( self, - excepthandler, + except_handler, type_.as_deref(), name, body, @@ -4023,94 +3920,77 @@ where ); } if self.enabled(Rule::ExceptWithEmptyTuple) { - flake8_bugbear::rules::except_with_empty_tuple(self, excepthandler); + flake8_bugbear::rules::except_with_empty_tuple(self, except_handler); } if self.enabled(Rule::ExceptWithNonExceptionClasses) { - flake8_bugbear::rules::except_with_non_exception_classes(self, excepthandler); + flake8_bugbear::rules::except_with_non_exception_classes(self, except_handler); } if self.enabled(Rule::ReraiseNoCause) { tryceratops::rules::reraise_no_cause(self, body); } - if self.enabled(Rule::BinaryOpException) { - pylint::rules::binary_op_exception(self, excepthandler); + pylint::rules::binary_op_exception(self, except_handler); } match name { Some(name) => { + let range = except_handler.try_identifier().unwrap(); + if self.enabled(Rule::AmbiguousVariableName) { - if let Some(diagnostic) = pycodestyle::rules::ambiguous_variable_name( - name, - helpers::excepthandler_name_range(excepthandler, self.locator) - .expect("Failed to find `name` range"), - ) { + if let Some(diagnostic) = + pycodestyle::rules::ambiguous_variable_name(name, range) + { self.diagnostics.push(diagnostic); } } - if self.enabled(Rule::BuiltinVariableShadowing) { flake8_builtins::rules::builtin_variable_shadowing( self, name, - AnyShadowing::from(excepthandler), + AnyShadowing::from(except_handler), ); } - let name_range = - helpers::excepthandler_name_range(excepthandler, self.locator).unwrap(); + // Store the existing binding, if any. + let existing_id = self.semantic.lookup_symbol(name); - if self.semantic_model.scope().has(name) { - self.handle_node_store( - name, - &Expr::Name(ast::ExprName { - id: name.into(), - ctx: ExprContext::Store, - range: name_range, - }), - ); - } - - let definition = self.semantic_model.scope().get(name); - self.handle_node_store( + // Add the bound exception name to the scope. + let binding_id = self.add_binding( name, - &Expr::Name(ast::ExprName { - id: name.into(), - ctx: ExprContext::Store, - range: name_range, - }), + range, + BindingKind::Assignment, + BindingFlags::empty(), ); - walk_excepthandler(self, excepthandler); + walk_except_handler(self, except_handler); - if let Some(binding_id) = { - let scope = self.semantic_model.scope_mut(); - scope.delete(name) - } { - if !self.semantic_model.is_used(binding_id) { - if self.enabled(Rule::UnusedVariable) { - let mut diagnostic = Diagnostic::new( - pyflakes::rules::UnusedVariable { name: name.into() }, - name_range, - ); - if self.patch(Rule::UnusedVariable) { - #[allow(deprecated)] - diagnostic.try_set_fix_from_edit(|| { - pyflakes::fixes::remove_exception_handler_assignment( - excepthandler, - self.locator, - ) - }); - } - self.diagnostics.push(diagnostic); + // If the exception name wasn't used in the scope, emit a diagnostic. + if !self.semantic.is_used(binding_id) { + if self.enabled(Rule::UnusedVariable) { + let mut diagnostic = Diagnostic::new( + pyflakes::rules::UnusedVariable { name: name.into() }, + range, + ); + if self.patch(Rule::UnusedVariable) { + diagnostic.try_set_fix(|| { + pyflakes::fixes::remove_exception_handler_assignment( + except_handler, + self.locator, + ) + .map(Fix::automatic) + }); } + self.diagnostics.push(diagnostic); } } - if let Some(binding_id) = definition { - let scope = self.semantic_model.scope_mut(); - scope.add(name, binding_id); - } + self.add_binding( + name, + range, + BindingKind::UnboundException(existing_id), + BindingFlags::empty(), + ); } - None => walk_excepthandler(self, excepthandler), + None => walk_except_handler(self, except_handler), } } } @@ -4134,13 +4014,13 @@ where if self.enabled(Rule::FunctionCallInDefaultArgument) { flake8_bugbear::rules::function_call_argument_default(self, arguments); } - + if self.settings.rules.enabled(Rule::ImplicitOptional) { + ruff::rules::implicit_optional(self, arguments); + } if self.is_stub { if self.enabled(Rule::TypedArgumentDefaultInStub) { flake8_pyi::rules::typed_argument_simple_defaults(self, arguments); } - } - if self.is_stub { if self.enabled(Rule::ArgumentDefaultInStub) { flake8_pyi::rules::argument_simple_defaults(self, arguments); } @@ -4148,17 +4028,17 @@ where // Bind, but intentionally avoid walking default expressions, as we handle them // upstream. - for arg in &arguments.posonlyargs { - self.visit_arg(arg); + for arg_with_default in &arguments.posonlyargs { + self.visit_arg(&arg_with_default.def); } - for arg in &arguments.args { - self.visit_arg(arg); + for arg_with_default in &arguments.args { + self.visit_arg(&arg_with_default.def); } if let Some(arg) = &arguments.vararg { self.visit_arg(arg); } - for arg in &arguments.kwonlyargs { - self.visit_arg(arg); + for arg_with_default in &arguments.kwonlyargs { + self.visit_arg(&arg_with_default.def); } if let Some(arg) = &arguments.kwarg { self.visit_arg(arg); @@ -4170,7 +4050,7 @@ where // upstream. self.add_binding( &arg.arg, - arg.range(), + arg.identifier(), BindingKind::Argument, BindingFlags::empty(), ); @@ -4182,7 +4062,6 @@ where self.diagnostics.push(diagnostic); } } - if self.enabled(Rule::InvalidArgumentName) { if let Some(diagnostic) = pep8_naming::rules::invalid_argument_name( &arg.arg, @@ -4192,7 +4071,6 @@ where self.diagnostics.push(diagnostic); } } - if self.enabled(Rule::BuiltinArgumentShadowing) { flake8_builtins::rules::builtin_argument_shadowing(self, arg); } @@ -4212,7 +4090,7 @@ where { self.add_binding( name, - pattern.range(), + pattern.try_identifier().unwrap(), BindingKind::Assignment, BindingFlags::empty(), ); @@ -4226,18 +4104,18 @@ where flake8_pie::rules::no_unnecessary_pass(self, body); } - let prev_body = self.semantic_model.body; - let prev_body_index = self.semantic_model.body_index; - self.semantic_model.body = body; - self.semantic_model.body_index = 0; + let prev_body = self.semantic.body; + let prev_body_index = self.semantic.body_index; + self.semantic.body = body; + self.semantic.body_index = 0; for stmt in body { self.visit_stmt(stmt); - self.semantic_model.body_index += 1; + self.semantic.body_index += 1; } - self.semantic_model.body = prev_body; - self.semantic_model.body_index = prev_body_index; + self.semantic.body = prev_body; + self.semantic.body_index = prev_body_index; } } @@ -4288,7 +4166,7 @@ impl<'a> Checker<'a> { // while all subsequent reads and writes are evaluated in the inner scope. In particular, // `x` is local to `foo`, and the `T` in `y=T` skips the class scope when resolving. self.visit_expr(&generator.iter); - self.semantic_model.push_scope(ScopeKind::Generator); + self.semantic.push_scope(ScopeKind::Generator); self.visit_expr(&generator.target); for expr in &generator.ifs { self.visit_boolean_test(expr); @@ -4305,36 +4183,44 @@ impl<'a> Checker<'a> { /// Visit an body of [`Stmt`] nodes within a type-checking block. fn visit_type_checking_block(&mut self, body: &'a [Stmt]) { - let snapshot = self.semantic_model.flags; - self.semantic_model.flags |= SemanticModelFlags::TYPE_CHECKING_BLOCK; + let snapshot = self.semantic.flags; + self.semantic.flags |= SemanticModelFlags::TYPE_CHECKING_BLOCK; self.visit_body(body); - self.semantic_model.flags = snapshot; + self.semantic.flags = snapshot; + } + + /// Visit an [`Expr`], and treat it as a runtime-required type annotation. + fn visit_runtime_annotation(&mut self, expr: &'a Expr) { + let snapshot = self.semantic.flags; + self.semantic.flags |= SemanticModelFlags::RUNTIME_ANNOTATION; + self.visit_type_definition(expr); + self.semantic.flags = snapshot; } /// Visit an [`Expr`], and treat it as a type definition. fn visit_type_definition(&mut self, expr: &'a Expr) { - let snapshot = self.semantic_model.flags; - self.semantic_model.flags |= SemanticModelFlags::TYPE_DEFINITION; + let snapshot = self.semantic.flags; + self.semantic.flags |= SemanticModelFlags::TYPE_DEFINITION; self.visit_expr(expr); - self.semantic_model.flags = snapshot; + self.semantic.flags = snapshot; } /// Visit an [`Expr`], and treat it as _not_ a type definition. fn visit_non_type_definition(&mut self, expr: &'a Expr) { - let snapshot = self.semantic_model.flags; - self.semantic_model.flags -= SemanticModelFlags::TYPE_DEFINITION; + let snapshot = self.semantic.flags; + self.semantic.flags -= SemanticModelFlags::TYPE_DEFINITION; self.visit_expr(expr); - self.semantic_model.flags = snapshot; + self.semantic.flags = snapshot; } /// Visit an [`Expr`], and treat it as a boolean test. This is useful for detecting whether an /// expressions return value is significant, or whether the calling context only relies on /// its truthiness. fn visit_boolean_test(&mut self, expr: &'a Expr) { - let snapshot = self.semantic_model.flags; - self.semantic_model.flags |= SemanticModelFlags::BOOLEAN_TEST; + let snapshot = self.semantic.flags; + self.semantic.flags |= SemanticModelFlags::BOOLEAN_TEST; self.visit_expr(expr); - self.semantic_model.flags = snapshot; + self.semantic.flags = snapshot; } /// Add a [`Binding`] to the current scope, bound to the given name. @@ -4350,133 +4236,69 @@ impl<'a> Checker<'a> { // expressions in generators and comprehensions bind to the scope that contains the // outermost comprehension. let scope_id = if kind.is_named_expr_assignment() { - self.semantic_model + self.semantic .scopes - .ancestor_ids(self.semantic_model.scope_id) - .find_or_last(|scope_id| !self.semantic_model.scopes[*scope_id].kind.is_generator()) - .unwrap_or(self.semantic_model.scope_id) + .ancestor_ids(self.semantic.scope_id) + .find_or_last(|scope_id| !self.semantic.scopes[*scope_id].kind.is_generator()) + .unwrap_or(self.semantic.scope_id) } else { - self.semantic_model.scope_id + self.semantic.scope_id }; // Create the `Binding`. - let binding_id = self.semantic_model.push_binding(range, kind, flags); - let binding = &self.semantic_model.bindings[binding_id]; - - // Determine whether the binding shadows any existing bindings. - if let Some((stack_index, shadowed_id)) = self - .semantic_model - .scopes - .ancestors(self.semantic_model.scope_id) - .enumerate() - .find_map(|(stack_index, scope)| { - scope.get(name).map(|binding_id| (stack_index, binding_id)) - }) - { - let shadowed = &self.semantic_model.bindings[shadowed_id]; - let in_current_scope = stack_index == 0; - if !shadowed.kind.is_builtin() - && shadowed.source.map_or(true, |left| { - binding.source.map_or(true, |right| { - !branch_detection::different_forks(left, right, &self.semantic_model.stmts) - }) - }) - { - let shadows_import = matches!( - shadowed.kind, - BindingKind::Importation(..) - | BindingKind::FromImportation(..) - | BindingKind::SubmoduleImportation(..) - | BindingKind::FutureImportation - ); - if binding.kind.is_loop_var() && shadows_import { - if self.enabled(Rule::ImportShadowedByLoopVar) { - #[allow(deprecated)] - let line = self.locator.compute_line_index(shadowed.range.start()); - - self.diagnostics.push(Diagnostic::new( - pyflakes::rules::ImportShadowedByLoopVar { - name: name.to_string(), - line, - }, - binding.range, - )); - } - } else if in_current_scope { - if !shadowed.is_used() - && binding.redefines(shadowed) - && (!self.settings.dummy_variable_rgx.is_match(name) || shadows_import) - && !(shadowed.kind.is_function_definition() - && analyze::visibility::is_overload( - &self.semantic_model, - cast::decorator_list( - self.semantic_model.stmts[shadowed.source.unwrap()], - ), - )) - { - if self.enabled(Rule::RedefinedWhileUnused) { - #[allow(deprecated)] - let line = self.locator.compute_line_index( - shadowed - .trimmed_range(&self.semantic_model, self.locator) - .start(), - ); - - let mut diagnostic = Diagnostic::new( - pyflakes::rules::RedefinedWhileUnused { - name: name.to_string(), - line, - }, - binding.trimmed_range(&self.semantic_model, self.locator), - ); - if let Some(range) = binding.parent_range(&self.semantic_model) { - diagnostic.set_parent(range.start()); - } - self.diagnostics.push(diagnostic); - } - } - } else if shadows_import && binding.redefines(shadowed) { - self.semantic_model - .shadowed_bindings - .insert(binding_id, shadowed_id); - } - } - } + let binding_id = self.semantic.push_binding(range, kind, flags); // If there's an existing binding in this scope, copy its references. - if let Some(shadowed) = self.semantic_model.scopes[scope_id] - .get(name) - .map(|binding_id| &self.semantic_model.bindings[binding_id]) - { - match &shadowed.kind { - BindingKind::Builtin => { - // Avoid overriding builtins. - } - kind @ (BindingKind::Global | BindingKind::Nonlocal) => { - // If the original binding was a global or nonlocal, then the new binding is - // too. - let references = shadowed.references.clone(); - self.semantic_model.bindings[binding_id].kind = kind.clone(); - self.semantic_model.bindings[binding_id].references = references; - } - _ => { - let references = shadowed.references.clone(); - self.semantic_model.bindings[binding_id].references = references; - } + if let Some(shadowed_id) = self.semantic.scopes[scope_id].get(name) { + // If this is an annotation, and we already have an existing value in the same scope, + // don't treat it as an assignment, but track it as a delayed annotation. + if self.semantic.binding(binding_id).kind.is_annotation() { + self.semantic + .add_delayed_annotation(shadowed_id, binding_id); + return binding_id; } - // If this is an annotation, and we already have an existing value in the same scope, - // don't treat it as an assignment (i.e., avoid adding it to the scope). - if self.semantic_model.bindings[binding_id] - .kind - .is_annotation() - { - return binding_id; + // Avoid shadowing builtins. + let shadowed = &self.semantic.bindings[shadowed_id]; + if !matches!( + shadowed.kind, + BindingKind::Builtin | BindingKind::Deletion | BindingKind::UnboundException(_), + ) { + let references = shadowed.references.clone(); + let is_global = shadowed.is_global(); + let is_nonlocal = shadowed.is_nonlocal(); + + // If the shadowed binding was global, then this one is too. + if is_global { + self.semantic.bindings[binding_id].flags |= BindingFlags::GLOBAL; + } + + // If the shadowed binding was non-local, then this one is too. + if is_nonlocal { + self.semantic.bindings[binding_id].flags |= BindingFlags::NONLOCAL; + } + + self.semantic.bindings[binding_id].references = references; + } + } else if let Some(shadowed_id) = self + .semantic + .scopes + .ancestors(scope_id) + .skip(1) + .find_map(|scope| scope.get(name)) + { + // Otherwise, if there's an existing binding in a parent scope, mark it as shadowed. + let binding = self.semantic.binding(binding_id); + let shadowed = self.semantic.binding(shadowed_id); + if binding.redefines(shadowed) { + self.semantic + .shadowed_bindings + .insert(binding_id, shadowed_id); } } // Add the binding to the scope. - let scope = &mut self.semantic_model.scopes[scope_id]; + let scope = &mut self.semantic.scopes[scope_id]; scope.add(name, binding_id); binding_id @@ -4490,29 +4312,29 @@ impl<'a> Checker<'a> { .chain(self.settings.builtins.iter().map(String::as_str)) { // Add the builtin to the scope. - let binding_id = self.semantic_model.push_builtin(); - let scope = self.semantic_model.scope_mut(); + let binding_id = self.semantic.push_builtin(); + let scope = self.semantic.global_scope_mut(); scope.add(builtin, binding_id); } } fn handle_node_load(&mut self, expr: &Expr) { - let Expr::Name(ast::ExprName { id, .. } )= expr else { + let Expr::Name(ast::ExprName { id, .. }) = expr else { return; }; - match self.semantic_model.resolve_read(id, expr.range()) { - ResolvedRead::Resolved(..) | ResolvedRead::ImplicitGlobal => { + match self.semantic.resolve_read(id, expr.range()) { + ResolvedRead::Resolved(_) | ResolvedRead::ImplicitGlobal => { // Nothing to do. } - ResolvedRead::StarImport => { + ResolvedRead::WildcardImport => { // F405 if self.enabled(Rule::UndefinedLocalWithImportStarUsage) { let sources: Vec = self - .semantic_model + .semantic .scopes .iter() .flat_map(Scope::star_imports) - .map(|StarImportation { level, module }| { + .map(|StarImport { level, module }| { helpers::format_import_from(*level, *module) }) .sorted() @@ -4527,7 +4349,7 @@ impl<'a> Checker<'a> { )); } } - ResolvedRead::NotFound => { + ResolvedRead::NotFound | ResolvedRead::UnboundLocal(_) => { // F821 if self.enabled(Rule::UndefinedName) { // Allow __path__. @@ -4537,7 +4359,7 @@ impl<'a> Checker<'a> { // Avoid flagging if `NameError` is handled. if self - .semantic_model + .semantic .handled_exceptions .iter() .any(|handler_names| handler_names.contains(Exceptions::NAME_ERROR)) @@ -4557,40 +4379,30 @@ impl<'a> Checker<'a> { } fn handle_node_store(&mut self, id: &'a str, expr: &Expr) { - let parent = self.semantic_model.stmt(); + let parent = self.semantic.stmt(); if self.enabled(Rule::UndefinedLocal) { pyflakes::rules::undefined_local(self, id); } - if self.enabled(Rule::NonLowercaseVariableInFunction) { - if self.semantic_model.scope().kind.is_any_function() { + if self.semantic.scope().kind.is_any_function() { // Ignore globals. - if !self - .semantic_model - .scope() - .get(id) - .map_or(false, |binding_id| { - self.semantic_model.bindings[binding_id].kind.is_global() - }) - { + if !self.semantic.scope().get(id).map_or(false, |binding_id| { + self.semantic.binding(binding_id).is_global() + }) { pep8_naming::rules::non_lowercase_variable_in_function(self, expr, parent, id); } } } - if self.enabled(Rule::MixedCaseVariableInClassScope) { - if let ScopeKind::Class(ast::StmtClassDef { bases, .. }) = - &self.semantic_model.scope().kind - { + if let ScopeKind::Class(ast::StmtClassDef { bases, .. }) = &self.semantic.scope().kind { pep8_naming::rules::mixed_case_variable_in_class_scope( self, expr, parent, id, bases, ); } } - if self.enabled(Rule::MixedCaseVariableInGlobalScope) { - if matches!(self.semantic_model.scope().kind, ScopeKind::Module) { + if matches!(self.semantic.scope().kind, ScopeKind::Module) { pep8_naming::rules::mixed_case_variable_in_global_scope(self, expr, parent, id); } } @@ -4628,7 +4440,7 @@ impl<'a> Checker<'a> { return; } - let scope = self.semantic_model.scope(); + let scope = self.semantic.scope(); if scope.kind.is_module() && match parent { @@ -4656,8 +4468,7 @@ impl<'a> Checker<'a> { _ => false, } { - let (names, flags) = - extract_all_names(parent, |name| self.semantic_model.is_builtin(name)); + let (names, flags) = extract_all_names(parent, |name| self.semantic.is_builtin(name)); if self.enabled(Rule::InvalidAllFormat) { if matches!(flags, AllNamesFlags::INVALID_FORMAT) { @@ -4683,7 +4494,7 @@ impl<'a> Checker<'a> { } if self - .semantic_model + .semantic .expr_ancestors() .any(|expr| matches!(expr, Expr::NamedExpr(_))) { @@ -4705,15 +4516,29 @@ impl<'a> Checker<'a> { } fn handle_node_delete(&mut self, expr: &'a Expr) { - let Expr::Name(ast::ExprName { id, .. } )= expr else { + let Expr::Name(ast::ExprName { id, .. }) = expr else { return; }; - if helpers::on_conditional_branch(&mut self.semantic_model.parents()) { - return; - } - let scope = self.semantic_model.scope_mut(); - if scope.delete(id.as_str()).is_none() { + // Treat the deletion of a name as a reference to that name. + if let Some(binding_id) = self.semantic.scope().get(id) { + self.semantic + .add_local_reference(binding_id, expr.range(), ExecutionContext::Runtime); + + // If the name is unbound, then it's an error. + if self.enabled(Rule::UndefinedName) { + let binding = self.semantic.binding(binding_id); + if binding.is_unbound() { + self.diagnostics.push(Diagnostic::new( + pyflakes::rules::UndefinedName { + name: id.to_string(), + }, + expr.range(), + )); + } + } + } else { + // If the name isn't bound at all, then it's an error. if self.enabled(Rule::UndefinedName) { self.diagnostics.push(Diagnostic::new( pyflakes::rules::UndefinedName { @@ -4723,15 +4548,26 @@ impl<'a> Checker<'a> { )); } } + + if helpers::on_conditional_branch(&mut self.semantic.parents()) { + return; + } + + // Create a binding to model the deletion. + let binding_id = + self.semantic + .push_binding(expr.range(), BindingKind::Deletion, BindingFlags::empty()); + let scope = self.semantic.scope_mut(); + scope.add(id, binding_id); } fn check_deferred_future_type_definitions(&mut self) { while !self.deferred.future_type_definitions.is_empty() { let type_definitions = std::mem::take(&mut self.deferred.future_type_definitions); for (expr, snapshot) in type_definitions { - self.semantic_model.restore(snapshot); + self.semantic.restore(snapshot); - self.semantic_model.flags |= SemanticModelFlags::TYPE_DEFINITION + self.semantic.flags |= SemanticModelFlags::TYPE_DEFINITION | SemanticModelFlags::FUTURE_TYPE_DEFINITION; self.visit_expr(expr); } @@ -4745,11 +4581,9 @@ impl<'a> Checker<'a> { if let Ok((expr, kind)) = parse_type_annotation(value, range, self.locator) { let expr = allocator.alloc(expr); - self.semantic_model.restore(snapshot); + self.semantic.restore(snapshot); - if self.semantic_model.in_annotation() - && self.semantic_model.future_annotations() - { + if self.semantic.in_annotation() && self.semantic.future_annotations() { if self.enabled(Rule::QuotedAnnotation) { pyupgrade::rules::quoted_annotation(self, value, range); } @@ -4767,7 +4601,7 @@ impl<'a> Checker<'a> { } }; - self.semantic_model.flags |= + self.semantic.flags |= SemanticModelFlags::TYPE_DEFINITION | type_definition_flag; self.visit_expr(expr); } else { @@ -4788,9 +4622,9 @@ impl<'a> Checker<'a> { while !self.deferred.functions.is_empty() { let deferred_functions = std::mem::take(&mut self.deferred.functions); for snapshot in deferred_functions { - self.semantic_model.restore(snapshot); + self.semantic.restore(snapshot); - match &self.semantic_model.stmt() { + match &self.semantic.stmt() { Stmt::FunctionDef(ast::StmtFunctionDef { body, args, .. }) | Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { body, args, .. }) => { self.visit_arguments(args); @@ -4810,7 +4644,7 @@ impl<'a> Checker<'a> { while !self.deferred.lambdas.is_empty() { let lambdas = std::mem::take(&mut self.deferred.lambdas); for (expr, snapshot) in lambdas { - self.semantic_model.restore(snapshot); + self.semantic.restore(snapshot); if let Expr::Lambda(ast::ExprLambda { args, @@ -4833,18 +4667,15 @@ impl<'a> Checker<'a> { while !self.deferred.assignments.is_empty() { let assignments = std::mem::take(&mut self.deferred.assignments); for snapshot in assignments { - self.semantic_model.restore(snapshot); + self.semantic.restore(snapshot); - // pyflakes if self.enabled(Rule::UnusedVariable) { - pyflakes::rules::unused_variable(self, self.semantic_model.scope_id); + pyflakes::rules::unused_variable(self, self.semantic.scope_id); } if self.enabled(Rule::UnusedAnnotation) { - pyflakes::rules::unused_annotation(self, self.semantic_model.scope_id); + pyflakes::rules::unused_annotation(self, self.semantic.scope_id); } - if !self.is_stub { - // flake8-unused-arguments if self.any_enabled(&[ Rule::UnusedFunctionArgument, Rule::UnusedMethodArgument, @@ -4852,14 +4683,11 @@ impl<'a> Checker<'a> { Rule::UnusedStaticMethodArgument, Rule::UnusedLambdaArgument, ]) { - let scope = &self.semantic_model.scopes[self.semantic_model.scope_id]; - let parent = &self.semantic_model.scopes[scope.parent.unwrap()]; + let scope = &self.semantic.scopes[self.semantic.scope_id]; + let parent = &self.semantic.scopes[scope.parent.unwrap()]; self.diagnostics .extend(flake8_unused_arguments::rules::unused_arguments( - self, - parent, - scope, - &self.semantic_model.bindings, + self, parent, scope, )); } } @@ -4872,11 +4700,10 @@ impl<'a> Checker<'a> { let for_loops = std::mem::take(&mut self.deferred.for_loops); for snapshot in for_loops { - self.semantic_model.restore(snapshot); + self.semantic.restore(snapshot); if let Stmt::For(ast::StmtFor { target, body, .. }) - | Stmt::AsyncFor(ast::StmtAsyncFor { target, body, .. }) = - &self.semantic_model.stmt() + | Stmt::AsyncFor(ast::StmtAsyncFor { target, body, .. }) = &self.semantic.stmt() { if self.enabled(Rule::UnusedLoopControlVariable) { flake8_bugbear::rules::unused_loop_control_variable(self, target, body); @@ -4888,33 +4715,31 @@ impl<'a> Checker<'a> { } } - fn check_dead_scopes(&mut self) { - let enforce_typing_imports = !self.is_stub - && self.any_enabled(&[ - Rule::GlobalVariableNotAssigned, - Rule::RuntimeImportInTypeCheckingBlock, - Rule::TypingOnlyFirstPartyImport, - Rule::TypingOnlyThirdPartyImport, - Rule::TypingOnlyStandardLibraryImport, - ]); - - if !(enforce_typing_imports - || self.any_enabled(&[ - Rule::UnusedImport, - Rule::UndefinedLocalWithImportStarUsage, - Rule::RedefinedWhileUnused, - Rule::UndefinedExport, - ])) - { + fn check_deferred_scopes(&mut self) { + if !self.any_enabled(&[ + Rule::GlobalVariableNotAssigned, + Rule::ImportShadowedByLoopVar, + Rule::RedefinedWhileUnused, + Rule::RuntimeImportInTypeCheckingBlock, + Rule::TypingOnlyFirstPartyImport, + Rule::TypingOnlyStandardLibraryImport, + Rule::TypingOnlyThirdPartyImport, + Rule::UnaliasedCollectionsAbcSetImport, + Rule::UnconventionalImportAlias, + Rule::UndefinedExport, + Rule::UndefinedLocalWithImportStarUsage, + Rule::UndefinedLocalWithImportStarUsage, + Rule::UnusedImport, + ]) { return; } // Mark anything referenced in `__all__` as used. let exports: Vec<(&str, TextRange)> = { - let global_scope = self.semantic_model.global_scope(); - global_scope - .bindings_for_name("__all__") - .map(|binding_id| &self.semantic_model.bindings[binding_id]) + self.semantic + .global_scope() + .get_all("__all__") + .map(|binding_id| &self.semantic.bindings[binding_id]) .filter_map(|binding| match &binding.kind { BindingKind::Export(Export { names }) => { Some(names.iter().map(|name| (*name, binding.range))) @@ -4926,33 +4751,40 @@ impl<'a> Checker<'a> { }; for (name, range) in &exports { - if let Some(binding_id) = self.semantic_model.global_scope().get(name) { - self.semantic_model.add_global_reference( - binding_id, - *range, - ExecutionContext::Runtime, - ); + if let Some(binding_id) = self.semantic.global_scope().get(name) { + self.semantic + .add_global_reference(binding_id, *range, ExecutionContext::Runtime); } } // Identify any valid runtime imports. If a module is imported at runtime, and // used at runtime, then by default, we avoid flagging any other // imports from that model as typing-only. + let enforce_typing_imports = if self.is_stub { + false + } else { + self.any_enabled(&[ + Rule::RuntimeImportInTypeCheckingBlock, + Rule::TypingOnlyFirstPartyImport, + Rule::TypingOnlyThirdPartyImport, + Rule::TypingOnlyStandardLibraryImport, + ]) + }; let runtime_imports: Vec> = if enforce_typing_imports { if self.settings.flake8_type_checking.strict { vec![] } else { - self.semantic_model + self.semantic .scopes .iter() .map(|scope| { scope .binding_ids() - .map(|binding_id| &self.semantic_model.bindings[binding_id]) + .map(|binding_id| self.semantic.binding(binding_id)) .filter(|binding| { flake8_type_checking::helpers::is_valid_runtime_import( - &self.semantic_model, binding, + &self.semantic, ) }) .collect() @@ -4964,8 +4796,8 @@ impl<'a> Checker<'a> { }; let mut diagnostics: Vec = vec![]; - for scope_id in self.semantic_model.dead_scopes.iter().rev() { - let scope = &self.semantic_model.scopes[*scope_id]; + for scope_id in self.deferred.scopes.iter().rev().copied() { + let scope = &self.semantic.scopes[scope_id]; if scope.kind.is_module() { // F822 @@ -4982,7 +4814,7 @@ impl<'a> Checker<'a> { if self.enabled(Rule::UndefinedLocalWithImportStarUsage) { let sources: Vec = scope .star_imports() - .map(|StarImportation { level, module }| { + .map(|StarImport { level, module }| { helpers::format_import_from(*level, *module) }) .sorted() @@ -5007,19 +4839,14 @@ impl<'a> Checker<'a> { // PLW0602 if self.enabled(Rule::GlobalVariableNotAssigned) { for (name, binding_id) in scope.bindings() { - let binding = &self.semantic_model.bindings[binding_id]; + let binding = self.semantic.binding(binding_id); if binding.kind.is_global() { - if let Some(source) = binding.source { - let stmt = &self.semantic_model.stmts[source]; - if stmt.is_global_stmt() { - diagnostics.push(Diagnostic::new( - pylint::rules::GlobalVariableNotAssigned { - name: (*name).to_string(), - }, - binding.range, - )); - } - } + diagnostics.push(Diagnostic::new( + pylint::rules::GlobalVariableNotAssigned { + name: (*name).to_string(), + }, + binding.range, + )); } } } @@ -5029,33 +4856,131 @@ impl<'a> Checker<'a> { continue; } - // Look for any bindings that were redefined in another scope, and remain - // unused. Note that we only store references in `shadowed_bindings` if - // the bindings are in different scopes. - if self.enabled(Rule::RedefinedWhileUnused) { + // F402 + if self.enabled(Rule::ImportShadowedByLoopVar) { for (name, binding_id) in scope.bindings() { - if let Some(shadowed_id) = self.semantic_model.shadowed_binding(binding_id) { - let shadowed = &self.semantic_model.bindings[shadowed_id]; - if shadowed.is_used() { + for shadow in self.semantic.shadowed_bindings(scope_id, binding_id) { + // If the shadowing binding isn't a loop variable, abort. + let binding = &self.semantic.bindings[shadow.binding_id()]; + if !binding.kind.is_loop_var() { + continue; + } + + // If the shadowed binding isn't an import, abort. + let shadowed = &self.semantic.bindings[shadow.shadowed_id()]; + if !matches!( + shadowed.kind, + BindingKind::Import(..) + | BindingKind::FromImport(..) + | BindingKind::SubmoduleImport(..) + | BindingKind::FutureImport + ) { + continue; + } + + // If the bindings are in different forks, abort. + if shadowed.source.map_or(true, |left| { + binding.source.map_or(true, |right| { + branch_detection::different_forks(left, right, &self.semantic.stmts) + }) + }) { continue; } #[allow(deprecated)] - let line = self.locator.compute_line_index( - shadowed - .trimmed_range(&self.semantic_model, self.locator) - .start(), - ); + let line = self.locator.compute_line_index(shadowed.range.start()); - let binding = &self.semantic_model.bindings[binding_id]; + self.diagnostics.push(Diagnostic::new( + pyflakes::rules::ImportShadowedByLoopVar { + name: name.to_string(), + line, + }, + binding.range, + )); + } + } + } + + // F811 + if self.enabled(Rule::RedefinedWhileUnused) { + for (name, binding_id) in scope.bindings() { + for shadow in self.semantic.shadowed_bindings(scope_id, binding_id) { + // If the shadowing binding is a loop variable, abort, to avoid overlap + // with F402. + let binding = &self.semantic.bindings[shadow.binding_id()]; + if binding.kind.is_loop_var() { + continue; + } + + // If the shadowed binding is used, abort. + let shadowed = &self.semantic.bindings[shadow.shadowed_id()]; + if shadowed.is_used() { + continue; + } + + // If the shadowing binding isn't considered a "redefinition" of the + // shadowed binding, abort. + if !binding.redefines(shadowed) { + continue; + } + + if shadow.same_scope() { + // If the symbol is a dummy variable, abort, unless the shadowed + // binding is an import. + if !matches!( + shadowed.kind, + BindingKind::Import(..) + | BindingKind::FromImport(..) + | BindingKind::SubmoduleImport(..) + | BindingKind::FutureImport + ) && self.settings.dummy_variable_rgx.is_match(name) + { + continue; + } + + // If this is an overloaded function, abort. + if shadowed.kind.is_function_definition() + && visibility::is_overload( + cast::decorator_list( + self.semantic.stmts[shadowed.source.unwrap()], + ), + &self.semantic, + ) + { + continue; + } + } else { + // Only enforce cross-scope shadowing for imports. + if !matches!( + shadowed.kind, + BindingKind::Import(..) + | BindingKind::FromImport(..) + | BindingKind::SubmoduleImport(..) + | BindingKind::FutureImport + ) { + continue; + } + } + + // If the bindings are in different forks, abort. + if shadowed.source.map_or(true, |left| { + binding.source.map_or(true, |right| { + branch_detection::different_forks(left, right, &self.semantic.stmts) + }) + }) { + continue; + } + + #[allow(deprecated)] + let line = self.locator.compute_line_index(shadowed.range.start()); let mut diagnostic = Diagnostic::new( pyflakes::rules::RedefinedWhileUnused { name: (*name).to_string(), line, }, - binding.trimmed_range(&self.semantic_model, self.locator), + binding.range, ); - if let Some(range) = binding.parent_range(&self.semantic_model) { + if let Some(range) = binding.parent_range(&self.semantic) { diagnostic.set_parent(range.start()); } diagnostics.push(diagnostic); @@ -5067,9 +4992,9 @@ impl<'a> Checker<'a> { let runtime_imports: Vec<&Binding> = if self.settings.flake8_type_checking.strict { vec![] } else { - self.semantic_model + self.semantic .scopes - .ancestor_ids(*scope_id) + .ancestor_ids(scope_id) .flat_map(|scope_id| runtime_imports[scope_id.as_usize()].iter()) .copied() .collect() @@ -5092,6 +5017,23 @@ impl<'a> Checker<'a> { if self.enabled(Rule::UnusedImport) { pyflakes::rules::unused_import(self, scope, &mut diagnostics); } + if self.enabled(Rule::UnconventionalImportAlias) { + flake8_import_conventions::rules::unconventional_import_alias( + self, + scope, + &mut diagnostics, + &self.settings.flake8_import_conventions.aliases, + ); + } + if self.is_stub { + if self.enabled(Rule::UnaliasedCollectionsAbcSetImport) { + flake8_pyi::rules::unaliased_collections_abc_set_import( + self, + scope, + &mut diagnostics, + ); + } + } } self.diagnostics.extend(diagnostics); } @@ -5171,21 +5113,26 @@ impl<'a> Checker<'a> { } // Compute visibility of all definitions. - let global_scope = self.semantic_model.global_scope(); - let exports: Option<&[&str]> = global_scope - .get("__all__") - .map(|binding_id| &self.semantic_model.bindings[binding_id]) - .and_then(|binding| match &binding.kind { - BindingKind::Export(Export { names }) => Some(names.as_slice()), - _ => None, - }); - let definitions = std::mem::take(&mut self.semantic_model.definitions); + let exports: Option> = { + let global_scope = self.semantic.global_scope(); + global_scope + .get_all("__all__") + .map(|binding_id| &self.semantic.bindings[binding_id]) + .filter_map(|binding| match &binding.kind { + BindingKind::Export(Export { names }) => Some(names.iter().copied()), + _ => None, + }) + .fold(None, |acc, names| { + Some(acc.into_iter().flatten().chain(names).collect()) + }) + }; + let definitions = std::mem::take(&mut self.semantic.definitions); let mut overloaded_name: Option = None; for ContextualizedDefinition { definition, visibility, - } in definitions.resolve(exports).iter() + } in definitions.resolve(exports.as_deref()).iter() { let docstring = docstrings::extraction::extract_docstring(definition); @@ -5198,9 +5145,9 @@ impl<'a> Checker<'a> { // classes, etc.). if !overloaded_name.map_or(false, |overloaded_name| { flake8_annotations::helpers::is_overload_impl( - &self.semantic_model, definition, &overloaded_name, + &self.semantic, ) }) { self.diagnostics @@ -5211,7 +5158,7 @@ impl<'a> Checker<'a> { )); } overloaded_name = - flake8_annotations::helpers::overloaded_name(&self.semantic_model, definition); + flake8_annotations::helpers::overloaded_name(definition, &self.semantic); } // flake8-pyi @@ -5229,9 +5176,9 @@ impl<'a> Checker<'a> { // pydocstyle if enforce_docstrings { if pydocstyle::helpers::should_ignore_definition( - &self.semantic_model, definition, &self.settings.pydocstyle.ignore_decorators, + &self.semantic, ) { continue; } @@ -5274,7 +5221,6 @@ impl<'a> Checker<'a> { if !pydocstyle::rules::not_empty(self, &docstring) { continue; } - if self.enabled(Rule::FitsOnOneLine) { pydocstyle::rules::one_liner(self, &docstring); } @@ -5392,9 +5338,9 @@ pub(crate) fn check_ast( ModuleKind::Module }, source: if let Some(module_path) = module_path.as_ref() { - ModuleSource::Path(module_path) + visibility::ModuleSource::Path(module_path) } else { - ModuleSource::File(path) + visibility::ModuleSource::File(path) }, python_ast, }; @@ -5436,9 +5382,9 @@ pub(crate) fn check_ast( checker.check_definitions(); // Reset the scope to module-level, and check all consumed scopes. - checker.semantic_model.scope_id = ScopeId::global(); - checker.semantic_model.dead_scopes.push(ScopeId::global()); - checker.check_dead_scopes(); + checker.semantic.scope_id = ScopeId::global(); + checker.deferred.scopes.push(ScopeId::global()); + checker.check_deferred_scopes(); checker.diagnostics } diff --git a/crates/ruff/src/checkers/filesystem.rs b/crates/ruff/src/checkers/filesystem.rs index 83df0ba87b..ad8a28dab9 100644 --- a/crates/ruff/src/checkers/filesystem.rs +++ b/crates/ruff/src/checkers/filesystem.rs @@ -25,7 +25,9 @@ pub(crate) fn check_file_path( // pep8-naming if settings.rules.enabled(Rule::InvalidModuleName) { - if let Some(diagnostic) = invalid_module_name(path, package) { + if let Some(diagnostic) = + invalid_module_name(path, package, &settings.pep8_naming.ignore_names) + { diagnostics.push(diagnostic); } } diff --git a/crates/ruff/src/checkers/imports.rs b/crates/ruff/src/checkers/imports.rs index a985c0daaf..0ee66eddaf 100644 --- a/crates/ruff/src/checkers/imports.rs +++ b/crates/ruff/src/checkers/imports.rs @@ -16,6 +16,7 @@ use crate::registry::Rule; use crate::rules::isort; use crate::rules::isort::block::{Block, BlockBuilder}; use crate::settings::Settings; +use crate::source_kind::SourceKind; fn extract_import_map(path: &Path, package: Option<&Path>, blocks: &[&Block]) -> Option { let Some(package) = package else { @@ -83,12 +84,13 @@ pub(crate) fn check_imports( stylist: &Stylist, path: &Path, package: Option<&Path>, + source_kind: Option<&SourceKind>, ) -> (Vec, Option) { let is_stub = is_python_stub_file(path); // Extract all import blocks from the AST. let tracker = { - let mut tracker = BlockBuilder::new(locator, directives, is_stub); + let mut tracker = BlockBuilder::new(locator, directives, is_stub, source_kind); tracker.visit_body(python_ast); tracker }; diff --git a/crates/ruff/src/checkers/noqa.rs b/crates/ruff/src/checkers/noqa.rs index 52d47182b5..16cea1d7cb 100644 --- a/crates/ruff/src/checkers/noqa.rs +++ b/crates/ruff/src/checkers/noqa.rs @@ -2,6 +2,7 @@ use itertools::Itertools; use ruff_text_size::{TextLen, TextRange, TextSize}; +use rustpython_parser::ast::Ranged; use ruff_diagnostics::{Diagnostic, Edit, Fix}; use ruff_python_ast::source_code::Locator; @@ -22,7 +23,7 @@ pub(crate) fn check_noqa( settings: &Settings, ) -> Vec { // Identify any codes that are globally exempted (within the current file). - let exemption = noqa::file_exemption(locator.contents(), comment_ranges); + let exemption = FileExemption::try_extract(locator.contents(), comment_ranges, locator); // Extract all `noqa` directives. let mut noqa_directives = NoqaDirectives::from_commented_ranges(comment_ranges, locator); @@ -37,19 +38,19 @@ pub(crate) fn check_noqa( } match &exemption { - FileExemption::All => { + Some(FileExemption::All) => { // If the file is exempted, ignore all diagnostics. ignored_diagnostics.push(index); continue; } - FileExemption::Codes(codes) => { + Some(FileExemption::Codes(codes)) => { // If the diagnostic is ignored by a global exemption, ignore it. if codes.contains(&diagnostic.kind.rule().noqa_code()) { ignored_diagnostics.push(index); continue; } } - FileExemption::None => {} + None => {} } let noqa_offsets = diagnostic @@ -63,15 +64,15 @@ pub(crate) fn check_noqa( if let Some(directive_line) = noqa_directives.find_line_with_directive_mut(noqa_offset) { let suppressed = match &directive_line.directive { - Directive::All(..) => { + Directive::All(_) => { directive_line .matches .push(diagnostic.kind.rule().noqa_code()); ignored_diagnostics.push(index); true } - Directive::Codes(.., codes, _) => { - if noqa::includes(diagnostic.kind.rule(), codes) { + Directive::Codes(directive) => { + if noqa::includes(diagnostic.kind.rule(), directive.codes()) { directive_line .matches .push(diagnostic.kind.rule().noqa_code()); @@ -81,7 +82,6 @@ pub(crate) fn check_noqa( false } } - Directive::None => unreachable!(), }; if suppressed { @@ -95,36 +95,31 @@ pub(crate) fn check_noqa( if analyze_directives && settings.rules.enabled(Rule::UnusedNOQA) { for line in noqa_directives.lines() { match &line.directive { - Directive::All(leading_spaces, noqa_range, trailing_spaces) => { + Directive::All(directive) => { if line.matches.is_empty() { let mut diagnostic = - Diagnostic::new(UnusedNOQA { codes: None }, *noqa_range); + Diagnostic::new(UnusedNOQA { codes: None }, directive.range()); if settings.rules.should_fix(diagnostic.kind.rule()) { #[allow(deprecated)] - diagnostic.set_fix_from_edit(delete_noqa( - *leading_spaces, - *noqa_range, - *trailing_spaces, - locator, - )); + diagnostic.set_fix_from_edit(delete_noqa(directive.range(), locator)); } diagnostics.push(diagnostic); } } - Directive::Codes(leading_spaces, range, codes, trailing_spaces) => { + Directive::Codes(directive) => { let mut disabled_codes = vec![]; let mut unknown_codes = vec![]; let mut unmatched_codes = vec![]; let mut valid_codes = vec![]; let mut self_ignore = false; - for code in codes { + for code in directive.codes() { let code = get_redirect_target(code).unwrap_or(code); if Rule::UnusedNOQA.noqa_code() == code { self_ignore = true; break; } - if line.matches.iter().any(|m| *m == code) + if line.matches.iter().any(|match_| *match_ == code) || settings.external.contains(code) { valid_codes.push(code); @@ -166,29 +161,24 @@ pub(crate) fn check_noqa( .collect(), }), }, - *range, + directive.range(), ); if settings.rules.should_fix(diagnostic.kind.rule()) { if valid_codes.is_empty() { #[allow(deprecated)] - diagnostic.set_fix_from_edit(delete_noqa( - *leading_spaces, - *range, - *trailing_spaces, - locator, - )); + diagnostic + .set_fix_from_edit(delete_noqa(directive.range(), locator)); } else { #[allow(deprecated)] diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( format!("# noqa: {}", valid_codes.join(", ")), - *range, + directive.range(), ))); } } diagnostics.push(diagnostic); } } - Directive::None => {} } } } @@ -198,38 +188,46 @@ pub(crate) fn check_noqa( } /// Generate a [`Edit`] to delete a `noqa` directive. -fn delete_noqa( - leading_spaces: TextSize, - noqa_range: TextRange, - trailing_spaces: TextSize, - locator: &Locator, -) -> Edit { - let line_range = locator.line_range(noqa_range.start()); +fn delete_noqa(range: TextRange, locator: &Locator) -> Edit { + let line_range = locator.line_range(range.start()); + + // Compute the leading space. + let prefix = locator.slice(TextRange::new(line_range.start(), range.start())); + let leading_space = prefix + .rfind(|c: char| !c.is_whitespace()) + .map_or(prefix.len(), |i| prefix.len() - i - 1); + let leading_space_len = TextSize::try_from(leading_space).unwrap(); + + // Compute the trailing space. + let suffix = locator.slice(TextRange::new(range.end(), line_range.end())); + let trailing_space = suffix + .find(|c: char| !c.is_whitespace()) + .map_or(suffix.len(), |i| i); + let trailing_space_len = TextSize::try_from(trailing_space).unwrap(); // Ex) `# noqa` if line_range == TextRange::new( - noqa_range.start() - leading_spaces, - noqa_range.end() + trailing_spaces, + range.start() - leading_space_len, + range.end() + trailing_space_len, ) { let full_line_end = locator.full_line_end(line_range.end()); Edit::deletion(line_range.start(), full_line_end) } // Ex) `x = 1 # noqa` - else if noqa_range.end() + trailing_spaces == line_range.end() { - Edit::deletion(noqa_range.start() - leading_spaces, line_range.end()) + else if range.end() + trailing_space_len == line_range.end() { + Edit::deletion(range.start() - leading_space_len, line_range.end()) } // Ex) `x = 1 # noqa # type: ignore` - else if locator.contents()[usize::from(noqa_range.end() + trailing_spaces)..].starts_with('#') - { - Edit::deletion(noqa_range.start(), noqa_range.end() + trailing_spaces) + else if locator.contents()[usize::from(range.end() + trailing_space_len)..].starts_with('#') { + Edit::deletion(range.start(), range.end() + trailing_space_len) } // Ex) `x = 1 # noqa here` else { Edit::deletion( - noqa_range.start() + "# ".text_len(), - noqa_range.end() + trailing_spaces, + range.start() + "# ".text_len(), + range.end() + trailing_space_len, ) } } diff --git a/crates/ruff/src/checkers/physical_lines.rs b/crates/ruff/src/checkers/physical_lines.rs index 3a5345ae58..92524ef2f0 100644 --- a/crates/ruff/src/checkers/physical_lines.rs +++ b/crates/ruff/src/checkers/physical_lines.rs @@ -1,14 +1,15 @@ //! Lint rules based on checking physical lines. +use std::path::Path; use ruff_text_size::TextSize; -use std::path::Path; use ruff_diagnostics::Diagnostic; use ruff_python_ast::source_code::{Indexer, Locator, Stylist}; use ruff_python_whitespace::UniversalNewlines; +use crate::comments::shebang::ShebangDirective; use crate::registry::Rule; -use crate::rules::flake8_executable::helpers::{extract_shebang, ShebangDirective}; +use crate::rules::flake8_copyright::rules::missing_copyright_notice; use crate::rules::flake8_executable::rules::{ shebang_missing, shebang_newline, shebang_not_executable, shebang_python, shebang_whitespace, }; @@ -49,6 +50,7 @@ pub(crate) fn check_physical_lines( let enforce_blank_line_contains_whitespace = settings.rules.enabled(Rule::BlankLineWithWhitespace); let enforce_tab_indentation = settings.rules.enabled(Rule::TabIndentation); + let enforce_copyright_notice = settings.rules.enabled(Rule::MissingCopyrightNotice); let fix_unnecessary_coding_comment = settings.rules.should_fix(Rule::UTF8EncodingDeclaration); let fix_shebang_whitespace = settings.rules.should_fix(Rule::ShebangLeadingWhitespace); @@ -85,33 +87,35 @@ pub(crate) fn check_physical_lines( || enforce_shebang_newline || enforce_shebang_python { - let shebang = extract_shebang(&line); - if enforce_shebang_not_executable { - if let Some(diagnostic) = shebang_not_executable(path, line.range(), &shebang) { - diagnostics.push(diagnostic); + if let Some(shebang) = ShebangDirective::try_extract(&line) { + has_any_shebang = true; + if enforce_shebang_not_executable { + if let Some(diagnostic) = + shebang_not_executable(path, line.range(), &shebang) + { + diagnostics.push(diagnostic); + } } - } - if enforce_shebang_missing { - if !has_any_shebang && matches!(shebang, ShebangDirective::Match(..)) { - has_any_shebang = true; + if enforce_shebang_whitespace { + if let Some(diagnostic) = + shebang_whitespace(line.range(), &shebang, fix_shebang_whitespace) + { + diagnostics.push(diagnostic); + } } - } - if enforce_shebang_whitespace { - if let Some(diagnostic) = - shebang_whitespace(line.range(), &shebang, fix_shebang_whitespace) - { - diagnostics.push(diagnostic); + if enforce_shebang_newline { + if let Some(diagnostic) = + shebang_newline(line.range(), &shebang, index == 0) + { + diagnostics.push(diagnostic); + } } - } - if enforce_shebang_newline { - if let Some(diagnostic) = shebang_newline(line.range(), &shebang, index == 0) { - diagnostics.push(diagnostic); - } - } - if enforce_shebang_python { - if let Some(diagnostic) = shebang_python(line.range(), &shebang) { - diagnostics.push(diagnostic); + if enforce_shebang_python { + if let Some(diagnostic) = shebang_python(line.range(), &shebang) { + diagnostics.push(diagnostic); + } } + } else { } } } @@ -144,7 +148,7 @@ pub(crate) fn check_physical_lines( } if enforce_trailing_whitespace || enforce_blank_line_contains_whitespace { - if let Some(diagnostic) = trailing_whitespace(&line, settings) { + if let Some(diagnostic) = trailing_whitespace(&line, locator, indexer, settings) { diagnostics.push(diagnostic); } } @@ -172,14 +176,21 @@ pub(crate) fn check_physical_lines( } } + if enforce_copyright_notice { + if let Some(diagnostic) = missing_copyright_notice(locator, settings) { + diagnostics.push(diagnostic); + } + } + diagnostics } #[cfg(test)] mod tests { + use std::path::Path; + use rustpython_parser::lexer::lex; use rustpython_parser::Mode; - use std::path::Path; use ruff_python_ast::source_code::{Indexer, Locator, Stylist}; diff --git a/crates/ruff/src/checkers/tokens.rs b/crates/ruff/src/checkers/tokens.rs index 4f71610a7c..b7883b4045 100644 --- a/crates/ruff/src/checkers/tokens.rs +++ b/crates/ruff/src/checkers/tokens.rs @@ -3,6 +3,9 @@ use rustpython_parser::lexer::LexResult; use rustpython_parser::Tok; +use ruff_diagnostics::Diagnostic; +use ruff_python_ast::source_code::{Indexer, Locator}; + use crate::directives::TodoComment; use crate::lex::docstring_detection::StateMachine; use crate::registry::{AsRule, Rule}; @@ -12,8 +15,6 @@ use crate::rules::{ flake8_todos, pycodestyle, pylint, pyupgrade, ruff, }; use crate::settings::Settings; -use ruff_diagnostics::Diagnostic; -use ruff_python_ast::source_code::{Indexer, Locator}; pub(crate) fn check_tokens( locator: &Locator, @@ -88,10 +89,11 @@ pub(crate) fn check_tokens( }; if matches!(tok, Tok::String { .. } | Tok::Comment(_)) { - diagnostics.extend(ruff::rules::ambiguous_unicode_character( + ruff::rules::ambiguous_unicode_character( + &mut diagnostics, locator, range, - if matches!(tok, Tok::String { .. }) { + if tok.is_string() { if is_docstring { Context::Docstring } else { @@ -101,93 +103,77 @@ pub(crate) fn check_tokens( Context::Comment }, settings, - )); + ); } } } // ERA001 if enforce_commented_out_code { - diagnostics.extend(eradicate::rules::commented_out_code( - indexer, locator, settings, - )); + eradicate::rules::commented_out_code(&mut diagnostics, locator, indexer, settings); } // W605 if enforce_invalid_escape_sequence { for (tok, range) in tokens.iter().flatten() { - if matches!(tok, Tok::String { .. }) { - diagnostics.extend(pycodestyle::rules::invalid_escape_sequence( + if tok.is_string() { + pycodestyle::rules::invalid_escape_sequence( + &mut diagnostics, locator, *range, settings.rules.should_fix(Rule::InvalidEscapeSequence), - )); + ); } } } // PLE2510, PLE2512, PLE2513 if enforce_invalid_string_character { for (tok, range) in tokens.iter().flatten() { - if matches!(tok, Tok::String { .. }) { - diagnostics.extend( - pylint::rules::invalid_string_characters(locator, *range) - .into_iter() - .filter(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())), - ); + if tok.is_string() { + pylint::rules::invalid_string_characters(&mut diagnostics, *range, locator); } } } // E701, E702, E703 if enforce_compound_statements { - diagnostics.extend( - pycodestyle::rules::compound_statements(tokens, settings) - .into_iter() - .filter(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())), + pycodestyle::rules::compound_statements( + &mut diagnostics, + tokens, + locator, + indexer, + settings, ); } // Q001, Q002, Q003 if enforce_quotes { - diagnostics.extend( - flake8_quotes::rules::from_tokens(tokens, locator, settings) - .into_iter() - .filter(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())), - ); + flake8_quotes::rules::from_tokens(&mut diagnostics, tokens, locator, settings); } // ISC001, ISC002 if enforce_implicit_string_concatenation { - diagnostics.extend( - flake8_implicit_str_concat::rules::implicit( - tokens, - &settings.flake8_implicit_str_concat, - locator, - ) - .into_iter() - .filter(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())), + flake8_implicit_str_concat::rules::implicit( + &mut diagnostics, + tokens, + &settings.flake8_implicit_str_concat, + locator, ); } // COM812, COM818, COM819 if enforce_trailing_comma { - diagnostics.extend( - flake8_commas::rules::trailing_commas(tokens, locator, settings) - .into_iter() - .filter(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())), - ); + flake8_commas::rules::trailing_commas(&mut diagnostics, tokens, locator, settings); } // UP034 if enforce_extraneous_parenthesis { - diagnostics.extend( - pyupgrade::rules::extraneous_parentheses(tokens, locator, settings).into_iter(), - ); + pyupgrade::rules::extraneous_parentheses(&mut diagnostics, tokens, locator, settings); } // PYI033 if enforce_type_comment_in_stub && is_stub { - diagnostics.extend(flake8_pyi::rules::type_comment_in_stub(indexer, locator)); + flake8_pyi::rules::type_comment_in_stub(&mut diagnostics, locator, indexer); } // TD001, TD002, TD003, TD004, TD005, TD006, TD007 @@ -203,18 +189,12 @@ pub(crate) fn check_tokens( }) .collect(); - diagnostics.extend( - flake8_todos::rules::todos(&todo_comments, indexer, locator, settings) - .into_iter() - .filter(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())), - ); + flake8_todos::rules::todos(&mut diagnostics, &todo_comments, locator, indexer, settings); - diagnostics.extend( - flake8_fixme::rules::todos(&todo_comments) - .into_iter() - .filter(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())), - ); + flake8_fixme::rules::todos(&mut diagnostics, &todo_comments); } + diagnostics.retain(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())); + diagnostics } diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 9e4f05bd97..b8b95250ad 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -14,6 +14,18 @@ use crate::rules; #[derive(PartialEq, Eq, PartialOrd, Ord)] pub struct NoqaCode(&'static str, &'static str); +impl NoqaCode { + /// Return the prefix for the [`NoqaCode`], e.g., `SIM` for `SIM101`. + pub fn prefix(&self) -> &str { + self.0 + } + + /// Return the suffix for the [`NoqaCode`], e.g., `101` for `SIM101`. + pub fn suffix(&self) -> &str { + self.1 + } +} + impl std::fmt::Debug for NoqaCode { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { std::fmt::Display::fmt(self, f) @@ -156,8 +168,12 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Pyflakes, "901") => (RuleGroup::Unspecified, rules::pyflakes::rules::RaiseNotImplemented), // pylint + (Pylint, "C0105") => (RuleGroup::Unspecified, rules::pylint::rules::TypeNameIncorrectVariance), + (Pylint, "C0131") => (RuleGroup::Unspecified, rules::pylint::rules::TypeBivariance), + (Pylint, "C0132") => (RuleGroup::Unspecified, rules::pylint::rules::TypeParamNameMismatch), + (Pylint, "C0205") => (RuleGroup::Unspecified, rules::pylint::rules::SingleStringSlots), (Pylint, "C0414") => (RuleGroup::Unspecified, rules::pylint::rules::UselessImportAlias), - (Pylint, "C1901") => (RuleGroup::Unspecified, rules::pylint::rules::CompareToEmptyString), + (Pylint, "C1901") => (RuleGroup::Nursery, rules::pylint::rules::CompareToEmptyString), (Pylint, "C3002") => (RuleGroup::Unspecified, rules::pylint::rules::UnnecessaryDirectLambdaCall), (Pylint, "C0208") => (RuleGroup::Unspecified, rules::pylint::rules::IterationOverSet), (Pylint, "E0100") => (RuleGroup::Unspecified, rules::pylint::rules::YieldInInit), @@ -193,6 +209,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Pylint, "R0915") => (RuleGroup::Unspecified, rules::pylint::rules::TooManyStatements), (Pylint, "R1701") => (RuleGroup::Unspecified, rules::pylint::rules::RepeatedIsinstanceCalls), (Pylint, "R1711") => (RuleGroup::Unspecified, rules::pylint::rules::UselessReturn), + (Pylint, "R1714") => (RuleGroup::Unspecified, rules::pylint::rules::RepeatedEqualityComparisonTarget), (Pylint, "R1722") => (RuleGroup::Unspecified, rules::pylint::rules::SysExitAlias), (Pylint, "R2004") => (RuleGroup::Unspecified, rules::pylint::rules::MagicValueComparison), (Pylint, "R5501") => (RuleGroup::Unspecified, rules::pylint::rules::CollapsibleElseIf), @@ -232,7 +249,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Flake8Bugbear, "013") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::RedundantTupleInExceptionHandler), (Flake8Bugbear, "014") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::DuplicateHandlerException), (Flake8Bugbear, "015") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::UselessComparison), - (Flake8Bugbear, "016") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::CannotRaiseLiteral), + (Flake8Bugbear, "016") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::RaiseLiteral), (Flake8Bugbear, "017") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::AssertRaisesException), (Flake8Bugbear, "018") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::UselessExpression), (Flake8Bugbear, "019") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::CachedInstanceMethod), @@ -250,6 +267,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Flake8Bugbear, "031") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::ReuseOfGroupbyGenerator), (Flake8Bugbear, "032") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::UnintentionalTypeAnnotation), (Flake8Bugbear, "033") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::DuplicateValue), + (Flake8Bugbear, "034") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::ReSubPositionalArgs), (Flake8Bugbear, "904") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::RaiseWithoutFromInsideExcept), (Flake8Bugbear, "905") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::ZipWithoutExplicitStrict), @@ -374,6 +392,9 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Flake8Simplify, "401") => (RuleGroup::Unspecified, rules::flake8_simplify::rules::IfElseBlockInsteadOfDictGet), (Flake8Simplify, "910") => (RuleGroup::Unspecified, rules::flake8_simplify::rules::DictGetWithNoneDefault), + // flake8-copyright + (Flake8Copyright, "001") => (RuleGroup::Nursery, rules::flake8_copyright::rules::MissingCopyrightNotice), + // pyupgrade (Pyupgrade, "001") => (RuleGroup::Unspecified, rules::pyupgrade::rules::UselessMetaclassType), (Pyupgrade, "003") => (RuleGroup::Unspecified, rules::pyupgrade::rules::TypeOfPrimitive), @@ -411,6 +432,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Pyupgrade, "036") => (RuleGroup::Unspecified, rules::pyupgrade::rules::OutdatedVersionBlock), (Pyupgrade, "037") => (RuleGroup::Unspecified, rules::pyupgrade::rules::QuotedAnnotation), (Pyupgrade, "038") => (RuleGroup::Unspecified, rules::pyupgrade::rules::NonPEP604Isinstance), + (Pyupgrade, "039") => (RuleGroup::Unspecified, rules::pyupgrade::rules::UnnecessaryClassParentheses), // pydocstyle (Pydocstyle, "100") => (RuleGroup::Unspecified, rules::pydocstyle::rules::UndocumentedPublicModule), @@ -591,6 +613,10 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { // flake8-pyi (Flake8Pyi, "001") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnprefixedTypeParam), + (Flake8Pyi, "002") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::ComplexIfStatementInStub), + (Flake8Pyi, "003") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnrecognizedVersionInfoCheck), + (Flake8Pyi, "004") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::PatchVersionComparison), + (Flake8Pyi, "005") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::WrongTupleLengthVersionComparison), (Flake8Pyi, "006") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::BadVersionInfoComparison), (Flake8Pyi, "007") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnrecognizedPlatformCheck), (Flake8Pyi, "008") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnrecognizedPlatformName), @@ -607,12 +633,15 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Flake8Pyi, "024") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::CollectionsNamedTuple), (Flake8Pyi, "025") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnaliasedCollectionsAbcSetImport), (Flake8Pyi, "029") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::StrOrReprDefinedInStub), + (Flake8Pyi, "030") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnnecessaryLiteralUnion), (Flake8Pyi, "032") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::AnyEqNeAnnotation), (Flake8Pyi, "033") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::TypeCommentInStub), (Flake8Pyi, "034") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::NonSelfReturnType), (Flake8Pyi, "035") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnassignedSpecialVariableInStub), + (Flake8Pyi, "036") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::BadExitAnnotation), (Flake8Pyi, "042") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::SnakeCaseTypeAlias), (Flake8Pyi, "043") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::TSuffixedTypeAlias), + (Flake8Pyi, "044") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::FutureAnnotationsInStub), (Flake8Pyi, "045") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::IterMethodReturnIterable), (Flake8Pyi, "048") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::StubBodyMultipleStatements), (Flake8Pyi, "050") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::NoReturnArgumentAnnotationInStub), @@ -736,6 +765,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { // numpy (Numpy, "001") => (RuleGroup::Unspecified, rules::numpy::rules::NumpyDeprecatedTypeAlias), (Numpy, "002") => (RuleGroup::Unspecified, rules::numpy::rules::NumpyLegacyRandom), + (Numpy, "003") => (RuleGroup::Unspecified, rules::numpy::rules::NumpyDeprecatedFunction), // ruff (Ruff, "001") => (RuleGroup::Unspecified, rules::ruff::rules::AmbiguousUnicodeCharacterString), @@ -748,6 +778,12 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Ruff, "009") => (RuleGroup::Unspecified, rules::ruff::rules::FunctionCallInDataclassDefaultArgument), (Ruff, "010") => (RuleGroup::Unspecified, rules::ruff::rules::ExplicitFStringTypeConversion), (Ruff, "011") => (RuleGroup::Unspecified, rules::ruff::rules::StaticKeyDictComprehension), + (Ruff, "012") => (RuleGroup::Unspecified, rules::ruff::rules::MutableClassDefault), + (Ruff, "013") => (RuleGroup::Unspecified, rules::ruff::rules::ImplicitOptional), + #[cfg(feature = "unreachable-code")] + (Ruff, "014") => (RuleGroup::Nursery, rules::ruff::rules::UnreachableCode), + (Ruff, "015") => (RuleGroup::Unspecified, rules::ruff::rules::UnnecessaryIterableAllocationForFirstElement), + (Ruff, "016") => (RuleGroup::Unspecified, rules::ruff::rules::InvalidIndexType), (Ruff, "100") => (RuleGroup::Unspecified, rules::ruff::rules::UnusedNOQA), (Ruff, "200") => (RuleGroup::Unspecified, rules::ruff::rules::InvalidPyprojectToml), @@ -776,6 +812,13 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { // airflow (Airflow, "001") => (RuleGroup::Unspecified, rules::airflow::rules::AirflowVariableNameTaskIdMismatch), + // perflint + (Perflint, "101") => (RuleGroup::Unspecified, rules::perflint::rules::UnnecessaryListCast), + (Perflint, "102") => (RuleGroup::Unspecified, rules::perflint::rules::IncorrectDictIterator), + (Perflint, "203") => (RuleGroup::Unspecified, rules::perflint::rules::TryExceptInLoop), + (Perflint, "401") => (RuleGroup::Unspecified, rules::perflint::rules::ManualListComprehension), + (Perflint, "402") => (RuleGroup::Unspecified, rules::perflint::rules::ManualListCopy), + // flake8-fixme (Flake8Fixme, "001") => (RuleGroup::Unspecified, rules::flake8_fixme::rules::LineContainsFixme), (Flake8Fixme, "002") => (RuleGroup::Unspecified, rules::flake8_fixme::rules::LineContainsTodo), diff --git a/crates/ruff/src/comments/mod.rs b/crates/ruff/src/comments/mod.rs new file mode 100644 index 0000000000..4b88bac2ed --- /dev/null +++ b/crates/ruff/src/comments/mod.rs @@ -0,0 +1 @@ +pub(crate) mod shebang; diff --git a/crates/ruff/src/comments/shebang.rs b/crates/ruff/src/comments/shebang.rs new file mode 100644 index 0000000000..a9a6bb13b1 --- /dev/null +++ b/crates/ruff/src/comments/shebang.rs @@ -0,0 +1,67 @@ +use ruff_python_whitespace::{is_python_whitespace, Cursor}; +use ruff_text_size::{TextLen, TextSize}; + +/// A shebang directive (e.g., `#!/usr/bin/env python3`). +#[derive(Debug, PartialEq, Eq)] +pub(crate) struct ShebangDirective<'a> { + /// The offset of the directive contents (e.g., `/usr/bin/env python3`) from the start of the + /// line. + pub(crate) offset: TextSize, + /// The contents of the directive (e.g., `"/usr/bin/env python3"`). + pub(crate) contents: &'a str, +} + +impl<'a> ShebangDirective<'a> { + /// Parse a shebang directive from a line, or return `None` if the line does not contain a + /// shebang directive. + pub(crate) fn try_extract(line: &'a str) -> Option { + let mut cursor = Cursor::new(line); + + // Trim whitespace. + cursor.eat_while(is_python_whitespace); + + // Trim the `#!` prefix. + if !cursor.eat_char('#') { + return None; + } + if !cursor.eat_char('!') { + return None; + } + + Some(Self { + offset: line.text_len() - cursor.text_len(), + contents: cursor.chars().as_str(), + }) + } +} + +#[cfg(test)] +mod tests { + use insta::assert_debug_snapshot; + + use super::ShebangDirective; + + #[test] + fn shebang_non_match() { + let source = "not a match"; + assert_debug_snapshot!(ShebangDirective::try_extract(source)); + } + + #[test] + fn shebang_end_of_line() { + let source = "print('test') #!/usr/bin/python"; + assert_debug_snapshot!(ShebangDirective::try_extract(source)); + } + + #[test] + fn shebang_match() { + let source = "#!/usr/bin/env python"; + assert_debug_snapshot!(ShebangDirective::try_extract(source)); + } + + #[test] + fn shebang_leading_space() { + let source = " #!/usr/bin/env python"; + assert_debug_snapshot!(ShebangDirective::try_extract(source)); + } +} diff --git a/crates/ruff/src/comments/snapshots/ruff__comments__shebang__tests__shebang_end_of_line.snap b/crates/ruff/src/comments/snapshots/ruff__comments__shebang__tests__shebang_end_of_line.snap new file mode 100644 index 0000000000..87d72c9d88 --- /dev/null +++ b/crates/ruff/src/comments/snapshots/ruff__comments__shebang__tests__shebang_end_of_line.snap @@ -0,0 +1,5 @@ +--- +source: crates/ruff/src/comments/shebang.rs +expression: "ShebangDirective::try_extract(source)" +--- +None diff --git a/crates/ruff/src/comments/snapshots/ruff__comments__shebang__tests__shebang_leading_space.snap b/crates/ruff/src/comments/snapshots/ruff__comments__shebang__tests__shebang_leading_space.snap new file mode 100644 index 0000000000..8ea8bfcfca --- /dev/null +++ b/crates/ruff/src/comments/snapshots/ruff__comments__shebang__tests__shebang_leading_space.snap @@ -0,0 +1,10 @@ +--- +source: crates/ruff/src/comments/shebang.rs +expression: "ShebangDirective::try_extract(source)" +--- +Some( + ShebangDirective { + offset: 4, + contents: "/usr/bin/env python", + }, +) diff --git a/crates/ruff/src/comments/snapshots/ruff__comments__shebang__tests__shebang_match.snap b/crates/ruff/src/comments/snapshots/ruff__comments__shebang__tests__shebang_match.snap new file mode 100644 index 0000000000..c0ec6ca308 --- /dev/null +++ b/crates/ruff/src/comments/snapshots/ruff__comments__shebang__tests__shebang_match.snap @@ -0,0 +1,10 @@ +--- +source: crates/ruff/src/comments/shebang.rs +expression: "ShebangDirective::try_extract(source)" +--- +Some( + ShebangDirective { + offset: 2, + contents: "/usr/bin/env python", + }, +) diff --git a/crates/ruff/src/comments/snapshots/ruff__comments__shebang__tests__shebang_non_match.snap b/crates/ruff/src/comments/snapshots/ruff__comments__shebang__tests__shebang_non_match.snap new file mode 100644 index 0000000000..87d72c9d88 --- /dev/null +++ b/crates/ruff/src/comments/snapshots/ruff__comments__shebang__tests__shebang_non_match.snap @@ -0,0 +1,5 @@ +--- +source: crates/ruff/src/comments/shebang.rs +expression: "ShebangDirective::try_extract(source)" +--- +None diff --git a/crates/ruff/src/directives.rs b/crates/ruff/src/directives.rs index 2f3c0187ac..4fc919a91c 100644 --- a/crates/ruff/src/directives.rs +++ b/crates/ruff/src/directives.rs @@ -427,22 +427,22 @@ ghi NoqaMapping::from_iter([TextRange::new(TextSize::from(6), TextSize::from(28))]) ); - let contents = r#"x = \ - 1"#; + let contents = r"x = \ + 1"; assert_eq!( noqa_mappings(contents), NoqaMapping::from_iter([TextRange::new(TextSize::from(0), TextSize::from(6))]) ); - let contents = r#"from foo import \ + let contents = r"from foo import \ bar as baz, \ - qux as quux"#; + qux as quux"; assert_eq!( noqa_mappings(contents), NoqaMapping::from_iter([TextRange::new(TextSize::from(0), TextSize::from(36))]) ); - let contents = r#" + let contents = r" # Foo from foo import \ bar as baz, \ @@ -450,7 +450,7 @@ from foo import \ x = \ 1 y = \ - 2"#; + 2"; assert_eq!( noqa_mappings(contents), NoqaMapping::from_iter([ diff --git a/crates/ruff/src/docstrings/extraction.rs b/crates/ruff/src/docstrings/extraction.rs index aa64249425..d4ae103b2f 100644 --- a/crates/ruff/src/docstrings/extraction.rs +++ b/crates/ruff/src/docstrings/extraction.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::{self, Constant, Expr, Stmt}; -use ruff_python_semantic::definition::{Definition, DefinitionId, Definitions, Member, MemberKind}; +use ruff_python_semantic::{Definition, DefinitionId, Definitions, Member, MemberKind}; /// Extract a docstring from a function or class body. pub(crate) fn docstring_from(suite: &[Stmt]) -> Option<&Expr> { diff --git a/crates/ruff/src/docstrings/mod.rs b/crates/ruff/src/docstrings/mod.rs index b9df42d415..97b44784b5 100644 --- a/crates/ruff/src/docstrings/mod.rs +++ b/crates/ruff/src/docstrings/mod.rs @@ -4,7 +4,7 @@ use std::ops::Deref; use ruff_text_size::{TextRange, TextSize}; use rustpython_parser::ast::{Expr, Ranged}; -use ruff_python_semantic::definition::Definition; +use ruff_python_semantic::Definition; pub(crate) mod extraction; pub(crate) mod google; @@ -18,7 +18,6 @@ pub(crate) struct Docstring<'a> { pub(crate) expr: &'a Expr, /// The content of the docstring, including the leading and trailing quotes. pub(crate) contents: &'a str, - /// The range of the docstring body (without the quotes). The range is relative to [`Self::contents`]. pub(crate) body_range: TextRange, pub(crate) indentation: &'a str, diff --git a/crates/ruff/src/docstrings/sections.rs b/crates/ruff/src/docstrings/sections.rs index d61697a1c5..6bdca985f4 100644 --- a/crates/ruff/src/docstrings/sections.rs +++ b/crates/ruff/src/docstrings/sections.rs @@ -5,7 +5,7 @@ use ruff_python_ast::docstrings::{leading_space, leading_words}; use ruff_text_size::{TextLen, TextRange, TextSize}; use strum_macros::EnumIter; -use ruff_python_whitespace::{UniversalNewlineIterator, UniversalNewlines}; +use ruff_python_whitespace::{Line, UniversalNewlineIterator, UniversalNewlines}; use crate::docstrings::styles::SectionStyle; use crate::docstrings::{Docstring, DocstringBody}; @@ -144,15 +144,13 @@ impl<'a> SectionContexts<'a> { let mut contexts = Vec::new(); let mut last: Option = None; - let mut previous_line = None; - for line in contents.universal_newlines() { - if previous_line.is_none() { - // skip the first line - previous_line = Some(line.as_str()); - continue; - } + let mut lines = contents.universal_newlines().peekable(); + // Skip the first line, which is the summary. + let mut previous_line = lines.next(); + + while let Some(line) = lines.next() { if let Some(section_kind) = suspected_as_section(&line, style) { let indent = leading_space(&line); let section_name = leading_words(&line); @@ -162,7 +160,8 @@ impl<'a> SectionContexts<'a> { if is_docstring_section( &line, section_name_range, - previous_line.unwrap_or_default(), + previous_line.as_ref(), + lines.peek(), ) { if let Some(mut last) = last.take() { last.range = TextRange::new(last.range.start(), line.start()); @@ -178,7 +177,7 @@ impl<'a> SectionContexts<'a> { } } - previous_line = Some(line.as_str()); + previous_line = Some(line); } if let Some(mut last) = last.take() { @@ -205,8 +204,8 @@ impl<'a> SectionContexts<'a> { } impl<'a> IntoIterator for &'a SectionContexts<'a> { - type Item = SectionContext<'a>; type IntoIter = SectionContextsIter<'a>; + type Item = SectionContext<'a>; fn into_iter(self) -> Self::IntoIter { self.iter() @@ -388,7 +387,13 @@ fn suspected_as_section(line: &str, style: SectionStyle) -> Option } /// Check if the suspected context is really a section header. -fn is_docstring_section(line: &str, section_name_range: TextRange, previous_lines: &str) -> bool { +fn is_docstring_section( + line: &Line, + section_name_range: TextRange, + previous_line: Option<&Line>, + next_line: Option<&Line>, +) -> bool { + // Determine whether the current line looks like a section header, e.g., "Args:". let section_name_suffix = line[usize::from(section_name_range.end())..].trim(); let this_looks_like_a_section_name = section_name_suffix == ":" || section_name_suffix.is_empty(); @@ -396,13 +401,29 @@ fn is_docstring_section(line: &str, section_name_range: TextRange, previous_line return false; } - let prev_line = previous_lines.trim(); - let prev_line_ends_with_punctuation = [',', ';', '.', '-', '\\', '/', ']', '}', ')'] - .into_iter() - .any(|char| prev_line.ends_with(char)); - let prev_line_looks_like_end_of_paragraph = - prev_line_ends_with_punctuation || prev_line.is_empty(); - if !prev_line_looks_like_end_of_paragraph { + // Determine whether the next line is an underline, e.g., "-----". + let next_line_is_underline = next_line.map_or(false, |next_line| { + let next_line = next_line.trim(); + if next_line.is_empty() { + false + } else { + let next_line_is_underline = next_line.chars().all(|char| matches!(char, '-' | '=')); + next_line_is_underline + } + }); + if next_line_is_underline { + return true; + } + + // Determine whether the previous line looks like the end of a paragraph. + let previous_line_looks_like_end_of_paragraph = previous_line.map_or(true, |previous_line| { + let previous_line = previous_line.trim(); + let previous_line_ends_with_punctuation = [',', ';', '.', '-', '\\', '/', ']', '}', ')'] + .into_iter() + .any(|char| previous_line.ends_with(char)); + previous_line_ends_with_punctuation || previous_line.is_empty() + }); + if !previous_line_looks_like_end_of_paragraph { return false; } diff --git a/crates/ruff/src/flake8_to_ruff/plugin.rs b/crates/ruff/src/flake8_to_ruff/plugin.rs index 77f6645d29..c234556c8a 100644 --- a/crates/ruff/src/flake8_to_ruff/plugin.rs +++ b/crates/ruff/src/flake8_to_ruff/plugin.rs @@ -333,7 +333,7 @@ pub(crate) fn infer_plugins_from_codes(selectors: &HashSet) -> Vec for selector in selectors { if selector .into_iter() - .any(|rule| Linter::from(plugin).into_iter().any(|r| r == rule)) + .any(|rule| Linter::from(plugin).rules().any(|r| r == rule)) { return true; } diff --git a/crates/ruff/src/importer/mod.rs b/crates/ruff/src/importer/mod.rs index f57fcfafe5..6acab6e471 100644 --- a/crates/ruff/src/importer/mod.rs +++ b/crates/ruff/src/importer/mod.rs @@ -1,4 +1,7 @@ -//! Add and modify import statements to make module members available during fix execution. +//! Code modification struct to add and modify import statements. +//! +//! Enables rules to make module members available (that may be not yet be imported) during fix +//! execution. use std::error::Error; @@ -10,7 +13,7 @@ use rustpython_parser::ast::{self, Ranged, Stmt, Suite}; use ruff_diagnostics::Edit; use ruff_python_ast::imports::{AnyImport, Import, ImportFrom}; use ruff_python_ast::source_code::{Locator, Stylist}; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; use ruff_textwrap::indent; use crate::autofix; @@ -116,7 +119,7 @@ impl<'a> Importer<'a> { &self, import: &StmtImports, at: TextSize, - semantic_model: &SemanticModel, + semantic: &SemanticModel, ) -> Result { // Generate the modified import statement. let content = autofix::codemods::retain_imports( @@ -130,7 +133,7 @@ impl<'a> Importer<'a> { let (type_checking_edit, type_checking) = self.get_or_import_symbol( &ImportRequest::import_from("typing", "TYPE_CHECKING"), at, - semantic_model, + semantic, )?; // Add the import to a `TYPE_CHECKING` block. @@ -164,11 +167,11 @@ impl<'a> Importer<'a> { &self, symbol: &ImportRequest, at: TextSize, - semantic_model: &SemanticModel, + semantic: &SemanticModel, ) -> Result<(Edit, String), ResolutionError> { - match self.get_symbol(symbol, at, semantic_model) { + match self.get_symbol(symbol, at, semantic) { Some(result) => result, - None => self.import_symbol(symbol, at, semantic_model), + None => self.import_symbol(symbol, at, semantic), } } @@ -177,11 +180,10 @@ impl<'a> Importer<'a> { &self, symbol: &ImportRequest, at: TextSize, - semantic_model: &SemanticModel, + semantic: &SemanticModel, ) -> Option> { // If the symbol is already available in the current scope, use it. - let imported_name = - semantic_model.resolve_qualified_import_name(symbol.module, symbol.member)?; + let imported_name = semantic.resolve_qualified_import_name(symbol.module, symbol.member)?; // If the symbol source (i.e., the import statement) comes after the current location, // abort. For example, we could be generating an edit within a function, and the import @@ -196,7 +198,7 @@ impl<'a> Importer<'a> { // If the symbol source (i.e., the import statement) is in a typing-only context, but we're // in a runtime context, abort. - if imported_name.context().is_typing() && semantic_model.execution_context().is_runtime() { + if imported_name.context().is_typing() && semantic.execution_context().is_runtime() { return Some(Err(ResolutionError::IncompatibleContext)); } @@ -233,13 +235,13 @@ impl<'a> Importer<'a> { &self, symbol: &ImportRequest, at: TextSize, - semantic_model: &SemanticModel, + semantic: &SemanticModel, ) -> Result<(Edit, String), ResolutionError> { if let Some(stmt) = self.find_import_from(symbol.module, at) { // Case 1: `from functools import lru_cache` is in scope, and we're trying to reference // `functools.cache`; thus, we add `cache` to the import, and return `"cache"` as the // bound name. - if semantic_model.is_unbound(symbol.member) { + if semantic.is_available(symbol.member) { let Ok(import_edit) = self.add_member(stmt, symbol.member) else { return Err(ResolutionError::InvalidEdit); }; @@ -252,7 +254,7 @@ impl<'a> Importer<'a> { ImportStyle::Import => { // Case 2a: No `functools` import is in scope; thus, we add `import functools`, // and return `"functools.cache"` as the bound name. - if semantic_model.is_unbound(symbol.module) { + if semantic.is_available(symbol.module) { let import_edit = self.add_import(&AnyImport::Import(Import::module(symbol.module)), at); Ok(( @@ -270,7 +272,7 @@ impl<'a> Importer<'a> { ImportStyle::ImportFrom => { // Case 2b: No `functools` import is in scope; thus, we add // `from functools import cache`, and return `"cache"` as the bound name. - if semantic_model.is_unbound(symbol.member) { + if semantic.is_available(symbol.member) { let import_edit = self.add_import( &AnyImport::ImportFrom(ImportFrom::member( symbol.module, diff --git a/crates/ruff/src/jupyter/index.rs b/crates/ruff/src/jupyter/index.rs new file mode 100644 index 0000000000..6a46d4da31 --- /dev/null +++ b/crates/ruff/src/jupyter/index.rs @@ -0,0 +1,24 @@ +/// Jupyter Notebook indexing table +/// +/// When we lint a jupyter notebook, we have to translate the row/column based on +/// [`ruff_text_size::TextSize`] to jupyter notebook cell/row/column. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct JupyterIndex { + /// Enter a row (1-based), get back the cell (1-based) + pub(super) row_to_cell: Vec, + /// Enter a row (1-based), get back the row in cell (1-based) + pub(super) row_to_row_in_cell: Vec, +} + +impl JupyterIndex { + /// Returns the cell number (1-based) for the given row (1-based). + pub fn cell(&self, row: usize) -> Option { + self.row_to_cell.get(row).copied() + } + + /// Returns the row number (1-based) in the cell (1-based) for the + /// given row (1-based). + pub fn cell_row(&self, row: usize) -> Option { + self.row_to_row_in_cell.get(row).copied() + } +} diff --git a/crates/ruff/src/jupyter/mod.rs b/crates/ruff/src/jupyter/mod.rs index ce6b9ef3bc..0d0bb5dc0a 100644 --- a/crates/ruff/src/jupyter/mod.rs +++ b/crates/ruff/src/jupyter/mod.rs @@ -1,7 +1,9 @@ //! Utils for reading and writing jupyter notebooks +pub use index::*; pub use notebook::*; pub use schema::*; +mod index; mod notebook; mod schema; diff --git a/crates/ruff/src/jupyter/notebook.rs b/crates/ruff/src/jupyter/notebook.rs index 5f2c545cac..3c3c0154b3 100644 --- a/crates/ruff/src/jupyter/notebook.rs +++ b/crates/ruff/src/jupyter/notebook.rs @@ -1,47 +1,120 @@ +use std::cmp::Ordering; use std::fs::File; -use std::io::{BufReader, BufWriter}; +use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write}; use std::iter; use std::path::Path; -use ruff_text_size::TextRange; +use itertools::Itertools; +use once_cell::sync::OnceCell; use serde::Serialize; use serde_json::error::Category; use ruff_diagnostics::Diagnostic; +use ruff_python_whitespace::{NewlineWithTrailingNewline, UniversalNewlineIterator}; +use ruff_text_size::{TextRange, TextSize}; -use crate::jupyter::{CellType, JupyterNotebook, SourceValue}; +use crate::autofix::source_map::{SourceMap, SourceMarker}; +use crate::jupyter::index::JupyterIndex; +use crate::jupyter::schema::{Cell, RawNotebook, SortAlphabetically, SourceValue}; use crate::rules::pycodestyle::rules::SyntaxError; use crate::IOError; pub const JUPYTER_NOTEBOOK_EXT: &str = "ipynb"; -/// Jupyter Notebook indexing table -/// -/// When we lint a jupyter notebook, we have to translate the row/column based on -/// [`ruff_text_size::TextSize`] -/// to jupyter notebook cell/row/column. -#[derive(Debug, Eq, PartialEq)] -pub struct JupyterIndex { - /// Enter a row (1-based), get back the cell (1-based) - pub row_to_cell: Vec, - /// Enter a row (1-based), get back the cell (1-based) - pub row_to_row_in_cell: Vec, +const MAGIC_PREFIX: [&str; 3] = ["%", "!", "?"]; + +/// Run round-trip source code generation on a given Jupyter notebook file path. +pub fn round_trip(path: &Path) -> anyhow::Result { + let mut notebook = Notebook::read(path).map_err(|err| { + anyhow::anyhow!( + "Failed to read notebook file `{}`: {:?}", + path.display(), + err + ) + })?; + let code = notebook.content().to_string(); + notebook.update_cell_content(&code); + let mut writer = Vec::new(); + notebook.write_inner(&mut writer)?; + Ok(String::from_utf8(writer)?) } -/// Return `true` if the [`Path`] appears to be that of a jupyter notebook file (`.ipynb`). -pub fn is_jupyter_notebook(path: &Path) -> bool { - path.extension() - .map_or(false, |ext| ext == JUPYTER_NOTEBOOK_EXT) - // For now this is feature gated here, the long term solution depends on - // https://github.com/astral-sh/ruff/issues/3410 - && cfg!(feature = "jupyter_notebook") +impl Cell { + /// Return the [`SourceValue`] of the cell. + fn source(&self) -> &SourceValue { + match self { + Cell::Code(cell) => &cell.source, + Cell::Markdown(cell) => &cell.source, + Cell::Raw(cell) => &cell.source, + } + } + + /// Update the [`SourceValue`] of the cell. + fn set_source(&mut self, source: SourceValue) { + match self { + Cell::Code(cell) => cell.source = source, + Cell::Markdown(cell) => cell.source = source, + Cell::Raw(cell) => cell.source = source, + } + } + + /// Return `true` if it's a valid code cell. + /// + /// A valid code cell is a cell where the cell type is [`Cell::Code`] and the + /// source doesn't contain a magic, shell or help command. + fn is_valid_code_cell(&self) -> bool { + let source = match self { + Cell::Code(cell) => &cell.source, + _ => return false, + }; + // Ignore a cell if it contains a magic command. There could be valid + // Python code as well, but we'll ignore that for now. + // TODO(dhruvmanila): https://github.com/psf/black/blob/main/src/black/handle_ipynb_magics.py + !match source { + SourceValue::String(string) => string.lines().any(|line| { + MAGIC_PREFIX + .iter() + .any(|prefix| line.trim_start().starts_with(prefix)) + }), + SourceValue::StringArray(string_array) => string_array.iter().any(|line| { + MAGIC_PREFIX + .iter() + .any(|prefix| line.trim_start().starts_with(prefix)) + }), + } + } } -impl JupyterNotebook { +#[derive(Clone, Debug, PartialEq)] +pub struct Notebook { + /// Python source code of the notebook. + /// + /// This is the concatenation of all valid code cells in the notebook + /// separated by a newline and a trailing newline. The trailing newline + /// is added to make sure that each cell ends with a newline which will + /// be removed when updating the cell content. + content: String, + /// The index of the notebook. This is used to map between the concatenated + /// source code and the original notebook. + index: OnceCell, + /// The raw notebook i.e., the deserialized version of JSON string. + raw: RawNotebook, + /// The offsets of each cell in the concatenated source code. This includes + /// the first and last character offsets as well. + cell_offsets: Vec, + /// The cell index of all valid code cells in the notebook. + valid_code_cells: Vec, + /// Flag to indicate if the JSON string of the notebook has a trailing newline. + trailing_newline: bool, +} + +impl Notebook { + /// Read the Jupyter Notebook from the given [`Path`]. + /// /// See also the black implementation /// pub fn read(path: &Path) -> Result> { - let reader = BufReader::new(File::open(path).map_err(|err| { + let mut reader = BufReader::new(File::open(path).map_err(|err| { Diagnostic::new( IOError { message: format!("{err}"), @@ -49,7 +122,19 @@ impl JupyterNotebook { TextRange::default(), ) })?); - let notebook: JupyterNotebook = match serde_json::from_reader(reader) { + let trailing_newline = reader.seek(SeekFrom::End(-1)).is_ok_and(|_| { + let mut buf = [0; 1]; + reader.read_exact(&mut buf).is_ok_and(|_| buf[0] == b'\n') + }); + reader.rewind().map_err(|err| { + Diagnostic::new( + IOError { + message: format!("{err}"), + }, + TextRange::default(), + ) + })?; + let raw_notebook: RawNotebook = match serde_json::from_reader(reader) { Ok(notebook) => notebook, Err(err) => { // Translate the error into a diagnostic @@ -117,112 +202,277 @@ impl JupyterNotebook { }; // v4 is what everybody uses - if notebook.nbformat != 4 { + if raw_notebook.nbformat != 4 { // bail because we should have already failed at the json schema stage return Err(Box::new(Diagnostic::new( SyntaxError { message: format!( "Expected Jupyter Notebook format 4, found {}", - notebook.nbformat + raw_notebook.nbformat ), }, TextRange::default(), ))); } - Ok(notebook) - } - - /// Concatenates all cells into a single virtual file and builds an index that maps the content - /// to notebook cell locations - pub fn index(&self) -> (String, JupyterIndex) { - let mut jupyter_index = JupyterIndex { - // Enter a line number (1-based), get back the cell (1-based) - // 0 index is just padding - row_to_cell: vec![0], - // Enter a line number (1-based), get back the row number in the cell (1-based) - // 0 index is just padding - row_to_row_in_cell: vec![0], - }; - let size_hint = self - .cells - .iter() - .filter(|cell| cell.cell_type == CellType::Code) - .count(); - - let mut contents = Vec::with_capacity(size_hint); - - for (pos, cell) in self + let valid_code_cells = raw_notebook .cells .iter() .enumerate() - .filter(|(_pos, cell)| cell.cell_type == CellType::Code) - { - let cell_contents = match &cell.source { - SourceValue::String(string) => { - // TODO(konstin): is or isn't there a trailing newline per cell? - // i've only seen these as array and never as string - let line_count = u32::try_from(string.lines().count()).unwrap(); - jupyter_index.row_to_cell.extend( - iter::repeat(u32::try_from(pos + 1).unwrap()).take(line_count as usize), - ); - jupyter_index.row_to_row_in_cell.extend(1..=line_count); - string.clone() - } - SourceValue::StringArray(string_array) => { - jupyter_index.row_to_cell.extend( - iter::repeat(u32::try_from(pos + 1).unwrap()).take(string_array.len()), - ); - jupyter_index - .row_to_row_in_cell - .extend(1..=u32::try_from(string_array.len()).unwrap()); - // lines already end in a newline character - string_array.join("") + .filter(|(_, cell)| cell.is_valid_code_cell()) + .map(|(idx, _)| u32::try_from(idx).unwrap()) + .collect::>(); + + let mut contents = Vec::with_capacity(valid_code_cells.len()); + let mut current_offset = TextSize::from(0); + let mut cell_offsets = Vec::with_capacity(valid_code_cells.len()); + cell_offsets.push(TextSize::from(0)); + + for &idx in &valid_code_cells { + let cell_contents = match &raw_notebook.cells[idx as usize].source() { + SourceValue::String(string) => string.clone(), + SourceValue::StringArray(string_array) => string_array.join(""), + }; + current_offset += TextSize::of(&cell_contents) + TextSize::new(1); + contents.push(cell_contents); + cell_offsets.push(current_offset); + } + + Ok(Self { + raw: raw_notebook, + index: OnceCell::new(), + // The additional newline at the end is to maintain consistency for + // all cells. These newlines will be removed before updating the + // source code with the transformed content. Refer `update_cell_content`. + content: contents.join("\n") + "\n", + cell_offsets, + valid_code_cells, + trailing_newline, + }) + } + + /// Update the cell offsets as per the given [`SourceMap`]. + fn update_cell_offsets(&mut self, source_map: &SourceMap) { + // When there are multiple cells without any edits, the offsets of those + // cells will be updated using the same marker. So, we can keep track of + // the last marker used to update the offsets and check if it's still + // the closest marker to the current offset. + let mut last_marker: Option<&SourceMarker> = None; + + // The first offset is always going to be at 0, so skip it. + for offset in self.cell_offsets.iter_mut().skip(1).rev() { + let closest_marker = match last_marker { + Some(marker) if marker.source <= *offset => marker, + _ => { + let Some(marker) = source_map + .markers() + .iter() + .rev() + .find(|m| m.source <= *offset) + else { + // There are no markers above the current offset, so we can + // stop here. + break; + }; + last_marker = Some(marker); + marker } }; - contents.push(cell_contents); + + match closest_marker.source.cmp(&closest_marker.dest) { + Ordering::Less => *offset += closest_marker.dest - closest_marker.source, + Ordering::Greater => *offset -= closest_marker.source - closest_marker.dest, + Ordering::Equal => (), + } } - // The last line doesn't end in a newline character - (contents.join("\n"), jupyter_index) + } + + /// Update the cell contents with the transformed content. + /// + /// ## Panics + /// + /// Panics if the transformed content is out of bounds for any cell. This + /// can happen only if the cell offsets were not updated before calling + /// this method or the offsets were updated incorrectly. + fn update_cell_content(&mut self, transformed: &str) { + for (&idx, (start, end)) in self + .valid_code_cells + .iter() + .zip(self.cell_offsets.iter().tuple_windows::<(_, _)>()) + { + let cell_content = transformed + .get(start.to_usize()..end.to_usize()) + .unwrap_or_else(|| { + panic!( + "Transformed content out of bounds ({start:?}..{end:?}) for cell at {idx:?}" + ); + }); + self.raw.cells[idx as usize].set_source(SourceValue::StringArray( + UniversalNewlineIterator::from( + // We only need to strip the trailing newline which we added + // while concatenating the cell contents. + cell_content.strip_suffix('\n').unwrap_or(cell_content), + ) + .map(|line| line.as_full_str().to_string()) + .collect::>(), + )); + } + } + + /// Build and return the [`JupyterIndex`]. + /// + /// ## Notes + /// + /// Empty cells don't have any newlines, but there's a single visible line + /// in the UI. That single line needs to be accounted for. + /// + /// In case of [`SourceValue::StringArray`], newlines are part of the strings. + /// So, to get the actual count of lines, we need to check for any trailing + /// newline for the last line. + /// + /// For example, consider the following cell: + /// ```python + /// [ + /// "import os\n", + /// "import sys\n", + /// ] + /// ``` + /// + /// Here, the array suggests that there are two lines, but the actual number + /// of lines visible in the UI is three. The same goes for [`SourceValue::String`] + /// where we need to check for the trailing newline. + /// + /// The index building is expensive as it needs to go through the content of + /// every valid code cell. + fn build_index(&self) -> JupyterIndex { + let mut row_to_cell = vec![0]; + let mut row_to_row_in_cell = vec![0]; + + for &idx in &self.valid_code_cells { + let line_count = match &self.raw.cells[idx as usize].source() { + SourceValue::String(string) => { + if string.is_empty() { + 1 + } else { + u32::try_from(NewlineWithTrailingNewline::from(string).count()).unwrap() + } + } + SourceValue::StringArray(string_array) => { + if string_array.is_empty() { + 1 + } else { + let trailing_newline = + usize::from(string_array.last().map_or(false, |s| s.ends_with('\n'))); + u32::try_from(string_array.len() + trailing_newline).unwrap() + } + } + }; + row_to_cell.extend(iter::repeat(idx + 1).take(line_count as usize)); + row_to_row_in_cell.extend(1..=line_count); + } + + JupyterIndex { + row_to_cell, + row_to_row_in_cell, + } + } + + /// Return the notebook content. + /// + /// This is the concatenation of all Python code cells. + pub(crate) fn content(&self) -> &str { + &self.content + } + + /// Return the Jupyter notebook index. + /// + /// The index is built only once when required. This is only used to + /// report diagnostics, so by that time all of the autofixes must have + /// been applied if `--fix` was passed. + pub(crate) fn index(&self) -> &JupyterIndex { + self.index.get_or_init(|| self.build_index()) + } + + /// Return the cell offsets for the concatenated source code corresponding + /// the Jupyter notebook. + pub(crate) fn cell_offsets(&self) -> &[TextSize] { + &self.cell_offsets + } + + /// Update the notebook with the given sourcemap and transformed content. + pub(crate) fn update(&mut self, source_map: &SourceMap, transformed: &str) { + // Cell offsets must be updated before updating the cell content as + // it depends on the offsets to extract the cell content. + self.update_cell_offsets(source_map); + self.update_cell_content(transformed); + self.content = transformed.to_string(); + } + + /// Return `true` if the notebook is a Python notebook, `false` otherwise. + pub fn is_python_notebook(&self) -> bool { + self.raw + .metadata + .language_info + .as_ref() + .map_or(true, |language| language.name == "python") + } + + fn write_inner(&self, writer: &mut impl Write) -> anyhow::Result<()> { + // https://github.com/psf/black/blob/69ca0a4c7a365c5f5eea519a90980bab72cab764/src/black/__init__.py#LL1041 + let formatter = serde_json::ser::PrettyFormatter::with_indent(b" "); + let mut serializer = serde_json::Serializer::with_formatter(writer, formatter); + SortAlphabetically(&self.raw).serialize(&mut serializer)?; + if self.trailing_newline { + writeln!(serializer.into_inner())?; + } + Ok(()) } /// Write back with an indent of 1, just like black pub fn write(&self, path: &Path) -> anyhow::Result<()> { let mut writer = BufWriter::new(File::create(path)?); - // https://github.com/psf/black/blob/69ca0a4c7a365c5f5eea519a90980bab72cab764/src/black/__init__.py#LL1041 - let formatter = serde_json::ser::PrettyFormatter::with_indent(b" "); - let mut ser = serde_json::Serializer::with_formatter(&mut writer, formatter); - self.serialize(&mut ser)?; + self.write_inner(&mut writer)?; Ok(()) } } #[cfg(test)] -mod test { +mod tests { use std::path::Path; - #[cfg(feature = "jupyter_notebook")] - use crate::jupyter::is_jupyter_notebook; - use crate::jupyter::{JupyterIndex, JupyterNotebook}; + use anyhow::Result; + use test_case::test_case; + + use crate::jupyter::index::JupyterIndex; + use crate::jupyter::schema::Cell; + use crate::jupyter::Notebook; + use crate::registry::Rule; + use crate::test::{read_jupyter_notebook, test_notebook_path, test_resource_path}; + use crate::{assert_messages, settings}; + + /// Read a Jupyter cell from the `resources/test/fixtures/jupyter/cell` directory. + fn read_jupyter_cell(path: impl AsRef) -> Result { + let path = test_resource_path("fixtures/jupyter/cell").join(path); + let contents = std::fs::read_to_string(path)?; + Ok(serde_json::from_str(&contents)?) + } #[test] fn test_valid() { - let path = Path::new("resources/test/fixtures/jupyter/valid.ipynb"); - assert!(JupyterNotebook::read(path).is_ok()); + assert!(read_jupyter_notebook(Path::new("valid.ipynb")).is_ok()); } #[test] fn test_r() { // We can load this, it will be filtered out later - let path = Path::new("resources/test/fixtures/jupyter/R.ipynb"); - assert!(JupyterNotebook::read(path).is_ok()); + assert!(read_jupyter_notebook(Path::new("R.ipynb")).is_ok()); } #[test] fn test_invalid() { let path = Path::new("resources/test/fixtures/jupyter/invalid_extension.ipynb"); assert_eq!( - JupyterNotebook::read(path).unwrap_err().kind.body, + Notebook::read(path).unwrap_err().kind.body, "SyntaxError: Expected a Jupyter Notebook (.ipynb extension), \ which must be internally stored as JSON, \ but found a Python source file: \ @@ -230,36 +480,33 @@ mod test { ); let path = Path::new("resources/test/fixtures/jupyter/not_json.ipynb"); assert_eq!( - JupyterNotebook::read(path).unwrap_err().kind.body, + Notebook::read(path).unwrap_err().kind.body, "SyntaxError: A Jupyter Notebook (.ipynb) must internally be JSON, \ but this file isn't valid JSON: \ expected value at line 1 column 1" ); let path = Path::new("resources/test/fixtures/jupyter/wrong_schema.ipynb"); assert_eq!( - JupyterNotebook::read(path).unwrap_err().kind.body, + Notebook::read(path).unwrap_err().kind.body, "SyntaxError: This file does not match the schema expected of Jupyter Notebooks: \ missing field `cells` at line 1 column 2" ); } - #[test] - #[cfg(feature = "jupyter_notebook")] - fn inclusions() { - let path = Path::new("foo/bar/baz"); - assert!(!is_jupyter_notebook(path)); - - let path = Path::new("foo/bar/baz.ipynb"); - assert!(is_jupyter_notebook(path)); + #[test_case(Path::new("markdown.json"), false; "markdown")] + #[test_case(Path::new("only_magic.json"), false; "only_magic")] + #[test_case(Path::new("code_and_magic.json"), false; "code_and_magic")] + #[test_case(Path::new("only_code.json"), true; "only_code")] + fn test_is_valid_code_cell(path: &Path, expected: bool) -> Result<()> { + assert_eq!(read_jupyter_cell(path)?.is_valid_code_cell(), expected); + Ok(()) } #[test] - fn test_concat_notebook() { - let path = Path::new("resources/test/fixtures/jupyter/valid.ipynb"); - let notebook = JupyterNotebook::read(path).unwrap(); - let (contents, index) = notebook.index(); + fn test_concat_notebook() -> Result<()> { + let notebook = read_jupyter_notebook(Path::new("valid.ipynb"))?; assert_eq!( - contents, + notebook.content, r#"def unused_variable(): x = 1 y = 2 @@ -270,14 +517,74 @@ def mutable_argument(z=set()): print(f"cell two: {z}") mutable_argument() + + + + +print("after empty cells") "# ); assert_eq!( - index, - JupyterIndex { - row_to_cell: vec![0, 1, 1, 1, 1, 1, 1, 3, 3, 3, 3], - row_to_row_in_cell: vec![0, 1, 2, 3, 4, 5, 6, 1, 2, 3, 4], + notebook.index(), + &JupyterIndex { + row_to_cell: vec![0, 1, 1, 1, 1, 1, 1, 3, 3, 3, 3, 3, 5, 7, 7, 8], + row_to_row_in_cell: vec![0, 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 1, 1, 2, 1], } ); + assert_eq!( + notebook.cell_offsets(), + &[ + 0.into(), + 90.into(), + 168.into(), + 169.into(), + 171.into(), + 198.into() + ] + ); + Ok(()) + } + + #[test] + fn test_import_sorting() -> Result<()> { + let path = "isort.ipynb".to_string(); + let (diagnostics, source_kind) = test_notebook_path( + &path, + Path::new("isort_expected.ipynb"), + &settings::Settings::for_rule(Rule::UnsortedImports), + )?; + assert_messages!(diagnostics, path, source_kind); + Ok(()) + } + + #[test] + fn test_json_consistency() -> Result<()> { + let path = "before_fix.ipynb".to_string(); + let (_, source_kind) = test_notebook_path( + path, + Path::new("after_fix.ipynb"), + &settings::Settings::for_rule(Rule::UnusedImport), + )?; + let mut writer = Vec::new(); + source_kind.expect_jupyter().write_inner(&mut writer)?; + let actual = String::from_utf8(writer)?; + let expected = + std::fs::read_to_string(test_resource_path("fixtures/jupyter/after_fix.ipynb"))?; + assert_eq!(actual, expected); + Ok(()) + } + + #[test_case(Path::new("before_fix.ipynb"), true; "trailing_newline")] + #[test_case(Path::new("no_trailing_newline.ipynb"), false; "no_trailing_newline")] + fn test_trailing_newline(path: &Path, trailing_newline: bool) -> Result<()> { + let notebook = read_jupyter_notebook(path)?; + assert_eq!(notebook.trailing_newline, trailing_newline); + + let mut writer = Vec::new(); + notebook.write_inner(&mut writer)?; + let string = String::from_utf8(writer)?; + assert_eq!(string.ends_with('\n'), trailing_newline); + + Ok(()) } } diff --git a/crates/ruff/src/jupyter/schema.rs b/crates/ruff/src/jupyter/schema.rs index 33b120f7ee..b6f9ed3c47 100644 --- a/crates/ruff/src/jupyter/schema.rs +++ b/crates/ruff/src/jupyter/schema.rs @@ -1,30 +1,96 @@ -//! The JSON schema of a Jupyter Notebook, entrypoint is [`JupyterNotebook`] +//! The JSON schema of a Jupyter Notebook, entrypoint is [`RawNotebook`] //! //! Generated by from //! //! Jupyter Notebook v4.5 JSON schema. //! -//! The following changes were made to the generated version: `Cell::id` is optional because it -//! wasn't required ` -//! for `"additionalProperties": true` as preparation for round-trip support. +//! The following changes were made to the generated version: +//! * Only keep the required structs and enums. +//! * `Cell::id` is optional because it wasn't required ` for +//! `"additionalProperties": true` as preparation for round-trip support. +//! * `#[serde(skip_serializing_none)]` was added to all structs where one or +//! more fields were optional to avoid serializing `null` values. +//! * `Cell::execution_count` is a required property only for code cells, but +//! we serialize it for all cells. This is because we can't know if a cell is +//! a code cell or not without looking at the `cell_type` property, which +//! would require a custom serializer. -use std::collections::{BTreeMap, HashMap}; +use std::collections::BTreeMap; use serde::{Deserialize, Serialize}; use serde_json::Value; +use serde_with::skip_serializing_none; + +fn sort_alphabetically( + value: &T, + serializer: S, +) -> Result { + let value = serde_json::to_value(value).map_err(serde::ser::Error::custom)?; + value.serialize(serializer) +} + +/// This is used to serialize any value implementing [`Serialize`] alphabetically. +/// +/// The reason for this is to maintain consistency in the generated JSON string, +/// which is useful for diffing. The default serializer keeps the order of the +/// fields as they are defined in the struct, which will not be consistent when +/// there are `extra` fields. +/// +/// # Example +/// +/// ``` +/// use std::collections::BTreeMap; +/// +/// use serde::Serialize; +/// +/// use ruff::jupyter::SortAlphabetically; +/// +/// #[derive(Serialize)] +/// struct MyStruct { +/// a: String, +/// #[serde(flatten)] +/// extra: BTreeMap, +/// b: String, +/// } +/// +/// let my_struct = MyStruct { +/// a: "a".to_string(), +/// extra: BTreeMap::from([ +/// ("d".to_string(), "d".to_string()), +/// ("c".to_string(), "c".to_string()), +/// ]), +/// b: "b".to_string(), +/// }; +/// +/// let serialized = serde_json::to_string_pretty(&SortAlphabetically(&my_struct)).unwrap(); +/// assert_eq!( +/// serialized, +/// r#"{ +/// "a": "a", +/// "b": "b", +/// "c": "c", +/// "d": "d" +/// }"# +/// ); +/// ``` +#[derive(Serialize)] +pub struct SortAlphabetically(#[serde(serialize_with = "sort_alphabetically")] pub T); /// The root of the JSON of a Jupyter Notebook /// /// Generated by from /// /// Jupyter Notebook v4.5 JSON schema. -#[derive(Debug, Serialize, Deserialize)] -pub struct JupyterNotebook { +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct RawNotebook { /// Array of cells of the current notebook. pub cells: Vec, /// Notebook root-level metadata. - pub metadata: JupyterNotebookMetadata, + pub metadata: RawNotebookMetadata, /// Notebook format (major number). Incremented between backwards incompatible changes to the /// notebook format. pub nbformat: i64, @@ -33,114 +99,73 @@ pub struct JupyterNotebook { pub nbformat_minor: i64, } +/// String identifying the type of cell. +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +#[serde(tag = "cell_type")] +pub enum Cell { + #[serde(rename = "code")] + Code(CodeCell), + #[serde(rename = "markdown")] + Markdown(MarkdownCell), + #[serde(rename = "raw")] + Raw(RawCell), +} + /// Notebook raw nbconvert cell. -/// -/// Notebook markdown cell. -/// -/// Notebook code cell. -#[derive(Debug, Serialize, Deserialize)] +#[skip_serializing_none] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(deny_unknown_fields)] -pub struct Cell { - pub attachments: Option>>, - /// String identifying the type of cell. - pub cell_type: CellType, +pub struct RawCell { + pub attachments: Option, /// Technically, id isn't required (it's not even present) in schema v4.0 through v4.4, but /// it's required in v4.5. Main issue is that pycharm creates notebooks without an id /// pub id: Option, /// Cell-level metadata. - pub metadata: CellMetadata, + pub metadata: Value, pub source: SourceValue, +} + +/// Notebook markdown cell. +#[skip_serializing_none] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct MarkdownCell { + pub attachments: Option, + /// Technically, id isn't required (it's not even present) in schema v4.0 through v4.4, but + /// it's required in v4.5. Main issue is that pycharm creates notebooks without an id + /// + pub id: Option, + /// Cell-level metadata. + pub metadata: Value, + pub source: SourceValue, +} + +/// Notebook code cell. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct CodeCell { /// The code cell's prompt number. Will be null if the cell has not been run. pub execution_count: Option, + /// Technically, id isn't required (it's not even present) in schema v4.0 through v4.4, but + /// it's required in v4.5. Main issue is that pycharm creates notebooks without an id + /// + pub id: Option, + /// Cell-level metadata. + pub metadata: Value, /// Execution, display, or stream outputs. - pub outputs: Option>, -} - -/// Cell-level metadata. -#[derive(Debug, Serialize, Deserialize)] -pub struct CellMetadata { - /// Raw cell metadata format for nbconvert. - pub format: Option, - /// Official Jupyter Metadata for Raw Cells - /// - /// Official Jupyter Metadata for Markdown Cells - /// - /// Official Jupyter Metadata for Code Cells - pub jupyter: Option>>, - pub name: Option, - pub tags: Option>, - /// Whether the cell's output is collapsed/expanded. - pub collapsed: Option, - /// Execution time for the code in the cell. This tracks time at which messages are received - /// from iopub or shell channels - pub execution: Option, - /// Whether the cell's output is scrolled, unscrolled, or autoscrolled. - pub scrolled: Option, - /// Custom added: round-trip support - #[serde(flatten)] - pub other: BTreeMap, -} - -/// Execution time for the code in the cell. This tracks time at which messages are received -/// from iopub or shell channels -#[derive(Debug, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct Execution { - /// header.date (in ISO 8601 format) of iopub channel's execute_input message. It indicates - /// the time at which the kernel broadcasts an execute_input message to connected frontends - #[serde(rename = "iopub.execute_input")] - pub iopub_execute_input: Option, - /// header.date (in ISO 8601 format) of iopub channel's kernel status message when the status - /// is 'busy' - #[serde(rename = "iopub.status.busy")] - pub iopub_status_busy: Option, - /// header.date (in ISO 8601 format) of iopub channel's kernel status message when the status - /// is 'idle'. It indicates the time at which kernel finished processing the associated - /// request - #[serde(rename = "iopub.status.idle")] - pub iopub_status_idle: Option, - /// header.date (in ISO 8601 format) of the shell channel's execute_reply message. It - /// indicates the time at which the execute_reply message was created - #[serde(rename = "shell.execute_reply")] - pub shell_execute_reply: Option, -} - -/// Result of executing a code cell. -/// -/// Data displayed as a result of code cell execution. -/// -/// Stream output from a code cell. -/// -/// Output of an error that occurred during code cell execution. -#[derive(Debug, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct Output { - pub data: Option>, - /// A result's prompt number. - pub execution_count: Option, - pub metadata: Option>>, - /// Type of cell output. - pub output_type: OutputType, - /// The name of the stream (stdout, stderr). - pub name: Option, - /// The stream's text output, represented as an array of strings. - pub text: Option, - /// The name of the error. - pub ename: Option, - /// The value, or message, of the error. - pub evalue: Option, - /// The error's traceback, represented as an array of strings. - pub traceback: Option>, + pub outputs: Vec, + pub source: SourceValue, } /// Notebook root-level metadata. -#[derive(Debug, Serialize, Deserialize)] -pub struct JupyterNotebookMetadata { +#[skip_serializing_none] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct RawNotebookMetadata { /// The author(s) of the notebook document - pub authors: Option>>, + pub authors: Option, /// Kernel information. - pub kernelspec: Option, + pub kernelspec: Option, /// Kernel information. pub language_info: Option, /// Original notebook format (major number) before converting the notebook between versions. @@ -148,28 +173,17 @@ pub struct JupyterNotebookMetadata { pub orig_nbformat: Option, /// The title of the notebook document pub title: Option, - /// Custom added: round-trip support + /// For additional properties. #[serde(flatten)] - pub other: BTreeMap, + pub extra: BTreeMap, } /// Kernel information. -#[derive(Debug, Serialize, Deserialize)] -pub struct Kernelspec { - /// Name to display in UI. - pub display_name: String, - /// Name of the kernel specification. - pub name: String, - /// Custom added: round-trip support - #[serde(flatten)] - pub other: BTreeMap, -} - -/// Kernel information. -#[derive(Debug, Serialize, Deserialize)] +#[skip_serializing_none] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct LanguageInfo { /// The codemirror mode to use for code in this language. - pub codemirror_mode: Option, + pub codemirror_mode: Option, /// The file extension for files in this language. pub file_extension: Option, /// The mimetype corresponding to files in this language. @@ -178,9 +192,9 @@ pub struct LanguageInfo { pub name: String, /// The pygments lexer to use for code in this language. pub pygments_lexer: Option, - /// Custom added: round-trip support + /// For additional properties. #[serde(flatten)] - pub other: BTreeMap, + pub extra: BTreeMap, } /// mimetype output (e.g. text/plain), represented as either an array of strings or a @@ -189,68 +203,9 @@ pub struct LanguageInfo { /// Contents of the cell, represented as an array of lines. /// /// The stream's text output, represented as an array of strings. -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(untagged)] pub enum SourceValue { String(String), StringArray(Vec), } - -/// Whether the cell's output is scrolled, unscrolled, or autoscrolled. -#[derive(Debug, Serialize, Deserialize)] -#[serde(untagged)] -pub enum ScrolledUnion { - Bool(bool), - Enum(ScrolledEnum), -} - -/// mimetype output (e.g. text/plain), represented as either an array of strings or a -/// string. -/// -/// Contents of the cell, represented as an array of lines. -/// -/// The stream's text output, represented as an array of strings. -#[derive(Debug, Serialize, Deserialize)] -#[serde(untagged)] -pub enum TextUnion { - String(String), - StringArray(Vec), -} - -/// The codemirror mode to use for code in this language. -#[derive(Debug, Serialize, Deserialize)] -#[serde(untagged)] -pub enum CodemirrorMode { - AnythingMap(HashMap>), - String(String), -} - -/// String identifying the type of cell. -#[derive(Debug, Serialize, Deserialize, PartialEq, Copy, Clone)] -pub enum CellType { - #[serde(rename = "code")] - Code, - #[serde(rename = "markdown")] - Markdown, - #[serde(rename = "raw")] - Raw, -} - -#[derive(Debug, Serialize, Deserialize, Copy, Clone)] -pub enum ScrolledEnum { - #[serde(rename = "auto")] - Auto, -} - -/// Type of cell output. -#[derive(Debug, Serialize, Deserialize, Copy, Clone)] -pub enum OutputType { - #[serde(rename = "display_data")] - DisplayData, - #[serde(rename = "error")] - Error, - #[serde(rename = "execute_result")] - ExecuteResult, - #[serde(rename = "stream")] - Stream, -} diff --git a/crates/ruff/src/jupyter/snapshots/ruff__jupyter__notebook__tests__import_sorting.snap b/crates/ruff/src/jupyter/snapshots/ruff__jupyter__notebook__tests__import_sorting.snap new file mode 100644 index 0000000000..240556c375 --- /dev/null +++ b/crates/ruff/src/jupyter/snapshots/ruff__jupyter__notebook__tests__import_sorting.snap @@ -0,0 +1,50 @@ +--- +source: crates/ruff/src/jupyter/notebook.rs +--- +isort.ipynb:cell 1:1:1: I001 [*] Import block is un-sorted or un-formatted + | +1 | / from pathlib import Path +2 | | import random +3 | | import math +4 | | from typing import Any + | |_^ I001 +5 | import collections +6 | # Newline should be added here + | + = help: Organize imports + +ℹ Fix + 1 |+import math + 2 |+import random +1 3 | from pathlib import Path +2 |-import random +3 |-import math +4 4 | from typing import Any +5 5 | import collections +6 6 | # Newline should be added here + +isort.ipynb:cell 2:1:1: I001 [*] Import block is un-sorted or un-formatted + | +1 | / from typing import Any +2 | | import collections +3 | | # Newline should be added here + | |_^ I001 +4 | def foo(): +5 | pass + | + = help: Organize imports + +ℹ Fix +1 1 | from pathlib import Path +2 2 | import random +3 3 | import math + 4 |+import collections +4 5 | from typing import Any +5 |-import collections + 6 |+ + 7 |+ +6 8 | # Newline should be added here +7 9 | def foo(): +8 10 | pass + + diff --git a/crates/ruff/src/lib.rs b/crates/ruff/src/lib.rs index 3ed0cd1d8d..0f65d8ffbf 100644 --- a/crates/ruff/src/lib.rs +++ b/crates/ruff/src/lib.rs @@ -9,9 +9,12 @@ pub use ruff_python_ast::source_code::round_trip; pub use rule_selector::RuleSelector; pub use rules::pycodestyle::rules::IOError; +pub const VERSION: &str = env!("CARGO_PKG_VERSION"); + mod autofix; mod checkers; mod codes; +mod comments; mod cst; pub mod directives; mod doc_lines; @@ -29,11 +32,14 @@ mod noqa; pub mod packaging; pub mod pyproject_toml; pub mod registry; +mod renamer; pub mod resolver; mod rule_redirects; mod rule_selector; pub mod rules; pub mod settings; +pub mod source_kind; +pub mod upstream_categories; #[cfg(any(test, fuzzing))] pub mod test; diff --git a/crates/ruff/src/linter.rs b/crates/ruff/src/linter.rs index bd8f713d87..ebdfded081 100644 --- a/crates/ruff/src/linter.rs +++ b/crates/ruff/src/linter.rs @@ -15,7 +15,7 @@ use ruff_python_ast::imports::ImportMap; use ruff_python_ast::source_code::{Indexer, Locator, SourceFileBuilder, Stylist}; use ruff_python_stdlib::path::is_python_stub_file; -use crate::autofix::fix_file; +use crate::autofix::{fix_file, FixResult}; use crate::checkers::ast::check_ast; use crate::checkers::filesystem::check_file_path; use crate::checkers::imports::check_imports; @@ -30,6 +30,7 @@ use crate::noqa::add_noqa; use crate::registry::{AsRule, Rule}; use crate::rules::pycodestyle; use crate::settings::{flags, Settings}; +use crate::source_kind::SourceKind; use crate::{directives, fs}; const CARGO_PKG_NAME: &str = env!("CARGO_PKG_NAME"); @@ -77,6 +78,7 @@ pub fn check_path( directives: &Directives, settings: &Settings, noqa: flags::Noqa, + source_kind: Option<&SourceKind>, ) -> LinterResult<(Vec, Option)> { // Aggregate all diagnostics. let mut diagnostics = vec![]; @@ -157,6 +159,7 @@ pub fn check_path( stylist, path, package, + source_kind, ); imports = module_imports; diagnostics.extend(import_diagnostics); @@ -285,11 +288,17 @@ pub fn add_noqa_to_path(path: &Path, package: Option<&Path>, settings: &Settings &directives, settings, flags::Noqa::Disabled, + None, ); // Log any parse errors. if let Some(err) = error { - error!("{}", DisplayParseError::new(err, locator.to_source_code())); + // TODO(dhruvmanila): This should use `SourceKind`, update when + // `--add-noqa` is supported for Jupyter notebooks. + error!( + "{}", + DisplayParseError::new(err, locator.to_source_code(), None) + ); } // Add any missing `# noqa` pragmas. @@ -343,6 +352,7 @@ pub fn lint_only( &directives, settings, noqa, + None, ); result.map(|(diagnostics, imports)| { @@ -388,6 +398,7 @@ pub fn lint_fix<'a>( package: Option<&Path>, noqa: flags::Noqa, settings: &Settings, + source_kind: &mut SourceKind, ) -> Result> { let mut transformed = Cow::Borrowed(contents); @@ -433,6 +444,7 @@ pub fn lint_fix<'a>( &directives, settings, noqa, + Some(source_kind), ); if iterations == 0 { @@ -453,13 +465,22 @@ pub fn lint_fix<'a>( } // Apply autofix. - if let Some((fixed_contents, applied)) = fix_file(&result.data.0, &locator) { + if let Some(FixResult { + code: fixed_contents, + fixes: applied, + source_map, + }) = fix_file(&result.data.0, &locator) + { if iterations < MAX_ITERATIONS { // Count the number of fixed errors. for (rule, count) in applied { *fixed.entry(rule).or_default() += count; } + if let SourceKind::Jupyter(notebook) = source_kind { + notebook.update(&source_map, &fixed_contents); + } + // Store the fixed contents. transformed = Cow::Owned(fixed_contents); diff --git a/crates/ruff/src/logging.rs b/crates/ruff/src/logging.rs index ede7c8f43b..df950899eb 100644 --- a/crates/ruff/src/logging.rs +++ b/crates/ruff/src/logging.rs @@ -9,9 +9,11 @@ use log::Level; use once_cell::sync::Lazy; use rustpython_parser::{ParseError, ParseErrorType}; -use ruff_python_ast::source_code::SourceCode; +use ruff_python_ast::source_code::{OneIndexed, SourceCode, SourceLocation}; use crate::fs; +use crate::jupyter::Notebook; +use crate::source_kind::SourceKind; pub(crate) static WARNINGS: Lazy>> = Lazy::new(Mutex::default); @@ -137,25 +139,70 @@ pub fn set_up_logging(level: &LogLevel) -> Result<()> { pub struct DisplayParseError<'a> { error: ParseError, source_code: SourceCode<'a, 'a>, + source_kind: Option<&'a SourceKind>, } impl<'a> DisplayParseError<'a> { - pub fn new(error: ParseError, source_code: SourceCode<'a, 'a>) -> Self { - Self { error, source_code } + pub fn new( + error: ParseError, + source_code: SourceCode<'a, 'a>, + source_kind: Option<&'a SourceKind>, + ) -> Self { + Self { + error, + source_code, + source_kind, + } } } impl Display for DisplayParseError<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{header} {path}{colon}", + header = "Failed to parse".bold(), + path = fs::relativize_path(Path::new(&self.error.source_path)).bold(), + colon = ":".cyan(), + )?; + let source_location = self.source_code.source_location(self.error.offset); + // If we're working on a Jupyter notebook, translate the positions + // with respect to the cell and row in the cell. This is the same + // format as the `TextEmitter`. + let error_location = if let Some(jupyter_index) = self + .source_kind + .and_then(SourceKind::notebook) + .map(Notebook::index) + { + write!( + f, + "cell {cell}{colon}", + cell = jupyter_index + .cell(source_location.row.get()) + .unwrap_or_default(), + colon = ":".cyan(), + )?; + + SourceLocation { + row: OneIndexed::new( + jupyter_index + .cell_row(source_location.row.get()) + .unwrap_or(1) as usize, + ) + .unwrap(), + column: source_location.column, + } + } else { + source_location + }; + write!( f, - "{header} {path}{colon}{row}{colon}{column}{colon} {inner}", - header = "Failed to parse".bold(), - path = fs::relativize_path(Path::new(&self.error.source_path)).bold(), - row = source_location.row, - column = source_location.column, + "{row}{colon}{column}{colon} {inner}", + row = error_location.row, + column = error_location.column, colon = ":".cyan(), inner = &DisplayParseErrorType(&self.error.error) ) @@ -184,11 +231,11 @@ impl Display for DisplayParseErrorType<'_> { if let Some(expected) = expected.as_ref() { write!( f, - "expected '{expected}', but got {tok}", + "Expected '{expected}', but got {tok}", tok = TruncateAtNewline(&tok) ) } else { - write!(f, "unexpected token {tok}", tok = TruncateAtNewline(&tok)) + write!(f, "Unexpected token {tok}", tok = TruncateAtNewline(&tok)) } } ParseErrorType::Lexical(ref error) => write!(f, "{error}"), diff --git a/crates/ruff/src/message/azure.rs b/crates/ruff/src/message/azure.rs index d5119faca0..f775fe27ab 100644 --- a/crates/ruff/src/message/azure.rs +++ b/crates/ruff/src/message/azure.rs @@ -51,7 +51,7 @@ mod tests { #[test] fn output() { - let mut emitter = AzureEmitter::default(); + let mut emitter = AzureEmitter; let content = capture_emitter_output(&mut emitter, &create_messages()); assert_snapshot!(content); diff --git a/crates/ruff/src/message/diff.rs b/crates/ruff/src/message/diff.rs index 8bda1c0534..c665578c2a 100644 --- a/crates/ruff/src/message/diff.rs +++ b/crates/ruff/src/message/diff.rs @@ -60,7 +60,7 @@ impl Display for Diff<'_> { Applicability::Automatic => "Fix", Applicability::Suggested => "Suggested fix", Applicability::Manual => "Possible fix", - Applicability::Unspecified => "Suggested fix", // For backwards compatibility, unspecified fixes are 'suggested' + Applicability::Unspecified => "Suggested fix", /* For backwards compatibility, unspecified fixes are 'suggested' */ }; writeln!(f, "ℹ {}", message.blue())?; diff --git a/crates/ruff/src/message/github.rs b/crates/ruff/src/message/github.rs index 23ddae5d67..97a28a4e97 100644 --- a/crates/ruff/src/message/github.rs +++ b/crates/ruff/src/message/github.rs @@ -66,7 +66,7 @@ mod tests { #[test] fn output() { - let mut emitter = GithubEmitter::default(); + let mut emitter = GithubEmitter; let content = capture_emitter_output(&mut emitter, &create_messages()); assert_snapshot!(content); diff --git a/crates/ruff/src/message/grouped.rs b/crates/ruff/src/message/grouped.rs index 5f8bfa411d..f6f2cd335c 100644 --- a/crates/ruff/src/message/grouped.rs +++ b/crates/ruff/src/message/grouped.rs @@ -7,12 +7,13 @@ use colored::Colorize; use ruff_python_ast::source_code::OneIndexed; use crate::fs::relativize_path; -use crate::jupyter::JupyterIndex; +use crate::jupyter::{JupyterIndex, Notebook}; use crate::message::diff::calculate_print_width; use crate::message::text::{MessageCodeFrame, RuleCodeAndBody}; use crate::message::{ group_messages_by_filename, Emitter, EmitterContext, Message, MessageWithLocation, }; +use crate::source_kind::SourceKind; #[derive(Default)] pub struct GroupedEmitter { @@ -65,7 +66,10 @@ impl Emitter for GroupedEmitter { writer, "{}", DisplayGroupedMessage { - jupyter_index: context.jupyter_index(message.filename()), + jupyter_index: context + .source_kind(message.filename()) + .and_then(SourceKind::notebook) + .map(Notebook::index), message, show_fix_status: self.show_fix_status, show_source: self.show_source, @@ -114,11 +118,15 @@ impl Display for DisplayGroupedMessage<'_> { write!( f, "cell {cell}{sep}", - cell = jupyter_index.row_to_cell[start_location.row.get()], + cell = jupyter_index + .cell(start_location.row.get()) + .unwrap_or_default(), sep = ":".cyan() )?; ( - jupyter_index.row_to_row_in_cell[start_location.row.get()] as usize, + jupyter_index + .cell_row(start_location.row.get()) + .unwrap_or(1) as usize, start_location.column.get(), ) } else { @@ -141,7 +149,14 @@ impl Display for DisplayGroupedMessage<'_> { if self.show_source { use std::fmt::Write; let mut padded = PadAdapter::new(f); - writeln!(padded, "{}", MessageCodeFrame { message })?; + writeln!( + padded, + "{}", + MessageCodeFrame { + message, + jupyter_index: self.jupyter_index + } + )?; } Ok(()) diff --git a/crates/ruff/src/message/json.rs b/crates/ruff/src/message/json.rs index c3adda51ba..8a80635be9 100644 --- a/crates/ruff/src/message/json.rs +++ b/crates/ruff/src/message/json.rs @@ -2,7 +2,7 @@ use std::io::Write; use serde::ser::SerializeSeq; use serde::{Serialize, Serializer}; -use serde_json::json; +use serde_json::{json, Value}; use ruff_diagnostics::Edit; use ruff_python_ast::source_code::SourceCode; @@ -38,30 +38,7 @@ impl Serialize for ExpandedMessages<'_> { let mut s = serializer.serialize_seq(Some(self.messages.len()))?; for message in self.messages { - let source_code = message.file.to_source_code(); - - let fix = message.fix.as_ref().map(|fix| { - json!({ - "applicability": fix.applicability(), - "message": message.kind.suggestion.as_deref(), - "edits": &ExpandedEdits { edits: fix.edits(), source_code: &source_code }, - }) - }); - - let start_location = source_code.source_location(message.start()); - let end_location = source_code.source_location(message.end()); - let noqa_location = source_code.source_location(message.noqa_offset); - - let value = json!({ - "code": message.kind.rule().noqa_code().to_string(), - "message": message.kind.body, - "fix": fix, - "location": start_location, - "end_location": end_location, - "filename": message.filename(), - "noqa_row": noqa_location.row - }); - + let value = message_to_json_value(message); s.serialize_element(&value)?; } @@ -69,6 +46,33 @@ impl Serialize for ExpandedMessages<'_> { } } +pub(crate) fn message_to_json_value(message: &Message) -> Value { + let source_code = message.file.to_source_code(); + + let fix = message.fix.as_ref().map(|fix| { + json!({ + "applicability": fix.applicability(), + "message": message.kind.suggestion.as_deref(), + "edits": &ExpandedEdits { edits: fix.edits(), source_code: &source_code }, + }) + }); + + let start_location = source_code.source_location(message.start()); + let end_location = source_code.source_location(message.end()); + let noqa_location = source_code.source_location(message.noqa_offset); + + json!({ + "code": message.kind.rule().noqa_code().to_string(), + "url": message.kind.rule().url(), + "message": message.kind.body, + "fix": fix, + "location": start_location, + "end_location": end_location, + "filename": message.filename(), + "noqa_row": noqa_location.row + }) +} + struct ExpandedEdits<'a> { edits: &'a [Edit], source_code: &'a SourceCode<'a, 'a>, @@ -104,7 +108,7 @@ mod tests { #[test] fn output() { - let mut emitter = JsonEmitter::default(); + let mut emitter = JsonEmitter; let content = capture_emitter_output(&mut emitter, &create_messages()); assert_snapshot!(content); diff --git a/crates/ruff/src/message/json_lines.rs b/crates/ruff/src/message/json_lines.rs new file mode 100644 index 0000000000..360e7ec6a7 --- /dev/null +++ b/crates/ruff/src/message/json_lines.rs @@ -0,0 +1,39 @@ +use std::io::Write; + +use crate::message::json::message_to_json_value; +use crate::message::{Emitter, EmitterContext, Message}; + +#[derive(Default)] +pub struct JsonLinesEmitter; + +impl Emitter for JsonLinesEmitter { + fn emit( + &mut self, + writer: &mut dyn Write, + messages: &[Message], + _context: &EmitterContext, + ) -> anyhow::Result<()> { + let mut w = writer; + for message in messages { + serde_json::to_writer(&mut w, &message_to_json_value(message))?; + w.write_all(b"\n")?; + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use insta::assert_snapshot; + + use crate::message::json_lines::JsonLinesEmitter; + use crate::message::tests::{capture_emitter_output, create_messages}; + + #[test] + fn output() { + let mut emitter = JsonLinesEmitter; + let content = capture_emitter_output(&mut emitter, &create_messages()); + + assert_snapshot!(content); + } +} diff --git a/crates/ruff/src/message/junit.rs b/crates/ruff/src/message/junit.rs index f910b7e6ed..26d7161f2e 100644 --- a/crates/ruff/src/message/junit.rs +++ b/crates/ruff/src/message/junit.rs @@ -93,7 +93,7 @@ mod tests { #[test] fn output() { - let mut emitter = JunitEmitter::default(); + let mut emitter = JunitEmitter; let content = capture_emitter_output(&mut emitter, &create_messages()); assert_snapshot!(content); diff --git a/crates/ruff/src/message/mod.rs b/crates/ruff/src/message/mod.rs index 072bf79fae..f86e408ecd 100644 --- a/crates/ruff/src/message/mod.rs +++ b/crates/ruff/src/message/mod.rs @@ -6,25 +6,26 @@ use std::ops::Deref; use ruff_text_size::{TextRange, TextSize}; use rustc_hash::FxHashMap; +use crate::source_kind::SourceKind; pub use azure::AzureEmitter; pub use github::GithubEmitter; pub use gitlab::GitlabEmitter; pub use grouped::GroupedEmitter; pub use json::JsonEmitter; +pub use json_lines::JsonLinesEmitter; pub use junit::JunitEmitter; pub use pylint::PylintEmitter; use ruff_diagnostics::{Diagnostic, DiagnosticKind, Fix}; use ruff_python_ast::source_code::{SourceFile, SourceLocation}; pub use text::TextEmitter; -use crate::jupyter::JupyterIndex; - mod azure; mod diff; mod github; mod gitlab; mod grouped; mod json; +mod json_lines; mod junit; mod pylint; mod text; @@ -93,6 +94,7 @@ struct MessageWithLocation<'a> { impl Deref for MessageWithLocation<'_> { type Target = Message; + fn deref(&self) -> &Self::Target { self.message } @@ -127,22 +129,23 @@ pub trait Emitter { /// Context passed to [`Emitter`]. pub struct EmitterContext<'a> { - jupyter_indices: &'a FxHashMap, + source_kind: &'a FxHashMap, } impl<'a> EmitterContext<'a> { - pub fn new(jupyter_indices: &'a FxHashMap) -> Self { - Self { jupyter_indices } + pub fn new(source_kind: &'a FxHashMap) -> Self { + Self { source_kind } } /// Tests if the file with `name` is a jupyter notebook. pub fn is_jupyter_notebook(&self, name: &str) -> bool { - self.jupyter_indices.contains_key(name) + self.source_kind + .get(name) + .map_or(false, SourceKind::is_jupyter) } - /// Returns the file's [`JupyterIndex`] if the file `name` is a jupyter notebook. - pub fn jupyter_index(&self, name: &str) -> Option<&JupyterIndex> { - self.jupyter_indices.get(name) + pub fn source_kind(&self, name: &str) -> Option<&SourceKind> { + self.source_kind.get(name) } } @@ -226,8 +229,8 @@ def fibonacci(n): emitter: &mut dyn Emitter, messages: &[Message], ) -> String { - let indices = FxHashMap::default(); - let context = EmitterContext::new(&indices); + let source_kinds = FxHashMap::default(); + let context = EmitterContext::new(&source_kinds); let mut output: Vec = Vec::new(); emitter.emit(&mut output, messages, &context).unwrap(); diff --git a/crates/ruff/src/message/pylint.rs b/crates/ruff/src/message/pylint.rs index edede90422..7453495bb9 100644 --- a/crates/ruff/src/message/pylint.rs +++ b/crates/ruff/src/message/pylint.rs @@ -49,7 +49,7 @@ mod tests { #[test] fn output() { - let mut emitter = PylintEmitter::default(); + let mut emitter = PylintEmitter; let content = capture_emitter_output(&mut emitter, &create_messages()); assert_snapshot!(content); diff --git a/crates/ruff/src/message/snapshots/ruff__message__gitlab__tests__output.snap b/crates/ruff/src/message/snapshots/ruff__message__gitlab__tests__output.snap index e83d0d23d9..43e03eb1b2 100644 --- a/crates/ruff/src/message/snapshots/ruff__message__gitlab__tests__output.snap +++ b/crates/ruff/src/message/snapshots/ruff__message__gitlab__tests__output.snap @@ -5,38 +5,38 @@ expression: redact_fingerprint(&content) [ { "description": "(F401) `os` imported but unused", - "severity": "major", "fingerprint": "", "location": { - "path": "fib.py", "lines": { "begin": 1, "end": 1 - } - } + }, + "path": "fib.py" + }, + "severity": "major" }, { "description": "(F841) Local variable `x` is assigned to but never used", - "severity": "major", "fingerprint": "", "location": { - "path": "fib.py", "lines": { "begin": 6, "end": 6 - } - } + }, + "path": "fib.py" + }, + "severity": "major" }, { "description": "(F821) Undefined name `a`", - "severity": "major", "fingerprint": "", "location": { - "path": "undef.py", "lines": { "begin": 1, "end": 1 - } - } + }, + "path": "undef.py" + }, + "severity": "major" } ] diff --git a/crates/ruff/src/message/snapshots/ruff__message__json__tests__output.snap b/crates/ruff/src/message/snapshots/ruff__message__json__tests__output.snap index e9272931d6..de7b932dbe 100644 --- a/crates/ruff/src/message/snapshots/ruff__message__json__tests__output.snap +++ b/crates/ruff/src/message/snapshots/ruff__message__json__tests__output.snap @@ -5,79 +5,82 @@ expression: content [ { "code": "F401", - "message": "`os` imported but unused", + "end_location": { + "column": 10, + "row": 1 + }, + "filename": "fib.py", "fix": { "applicability": "Suggested", - "message": "Remove unused import: `os`", "edits": [ { "content": "", - "location": { - "row": 1, - "column": 1 - }, "end_location": { - "row": 2, - "column": 1 + "column": 1, + "row": 2 + }, + "location": { + "column": 1, + "row": 1 } } - ] + ], + "message": "Remove unused import: `os`" }, "location": { - "row": 1, - "column": 8 + "column": 8, + "row": 1 }, - "end_location": { - "row": 1, - "column": 10 - }, - "filename": "fib.py", - "noqa_row": 1 + "message": "`os` imported but unused", + "noqa_row": 1, + "url": "https://beta.ruff.rs/docs/rules/unused-import" }, { "code": "F841", - "message": "Local variable `x` is assigned to but never used", + "end_location": { + "column": 6, + "row": 6 + }, + "filename": "fib.py", "fix": { "applicability": "Suggested", - "message": "Remove assignment to unused variable `x`", "edits": [ { "content": "", - "location": { - "row": 6, - "column": 5 - }, "end_location": { - "row": 6, - "column": 10 + "column": 10, + "row": 6 + }, + "location": { + "column": 5, + "row": 6 } } - ] + ], + "message": "Remove assignment to unused variable `x`" }, "location": { - "row": 6, - "column": 5 + "column": 5, + "row": 6 }, - "end_location": { - "row": 6, - "column": 6 - }, - "filename": "fib.py", - "noqa_row": 6 + "message": "Local variable `x` is assigned to but never used", + "noqa_row": 6, + "url": "https://beta.ruff.rs/docs/rules/unused-variable" }, { "code": "F821", - "message": "Undefined name `a`", - "fix": null, - "location": { - "row": 1, - "column": 4 - }, "end_location": { - "row": 1, - "column": 5 + "column": 5, + "row": 1 }, "filename": "undef.py", - "noqa_row": 1 + "fix": null, + "location": { + "column": 4, + "row": 1 + }, + "message": "Undefined name `a`", + "noqa_row": 1, + "url": "https://beta.ruff.rs/docs/rules/undefined-name" } ] diff --git a/crates/ruff/src/message/snapshots/ruff__message__json_lines__tests__output.snap b/crates/ruff/src/message/snapshots/ruff__message__json_lines__tests__output.snap new file mode 100644 index 0000000000..90749c6753 --- /dev/null +++ b/crates/ruff/src/message/snapshots/ruff__message__json_lines__tests__output.snap @@ -0,0 +1,8 @@ +--- +source: crates/ruff/src/message/json_lines.rs +expression: content +--- +{"code":"F401","end_location":{"column":10,"row":1},"filename":"fib.py","fix":{"applicability":"Suggested","edits":[{"content":"","end_location":{"column":1,"row":2},"location":{"column":1,"row":1}}],"message":"Remove unused import: `os`"},"location":{"column":8,"row":1},"message":"`os` imported but unused","noqa_row":1,"url":"https://beta.ruff.rs/docs/rules/unused-import"} +{"code":"F841","end_location":{"column":6,"row":6},"filename":"fib.py","fix":{"applicability":"Suggested","edits":[{"content":"","end_location":{"column":10,"row":6},"location":{"column":5,"row":6}}],"message":"Remove assignment to unused variable `x`"},"location":{"column":5,"row":6},"message":"Local variable `x` is assigned to but never used","noqa_row":6,"url":"https://beta.ruff.rs/docs/rules/unused-variable"} +{"code":"F821","end_location":{"column":5,"row":1},"filename":"undef.py","fix":null,"location":{"column":4,"row":1},"message":"Undefined name `a`","noqa_row":1,"url":"https://beta.ruff.rs/docs/rules/undefined-name"} + diff --git a/crates/ruff/src/message/text.rs b/crates/ruff/src/message/text.rs index e15cd3d63a..237b304d28 100644 --- a/crates/ruff/src/message/text.rs +++ b/crates/ruff/src/message/text.rs @@ -11,10 +11,12 @@ use ruff_text_size::{TextRange, TextSize}; use ruff_python_ast::source_code::{OneIndexed, SourceLocation}; use crate::fs::relativize_path; +use crate::jupyter::{JupyterIndex, Notebook}; use crate::line_width::{LineWidth, TabSize}; use crate::message::diff::Diff; use crate::message::{Emitter, EmitterContext, Message}; use crate::registry::AsRule; +use crate::source_kind::SourceKind; bitflags! { #[derive(Default)] @@ -70,27 +72,34 @@ impl Emitter for TextEmitter { )?; let start_location = message.compute_start_location(); + let jupyter_index = context + .source_kind(message.filename()) + .and_then(SourceKind::notebook) + .map(Notebook::index); // Check if we're working on a jupyter notebook and translate positions with cell accordingly - let diagnostic_location = - if let Some(jupyter_index) = context.jupyter_index(message.filename()) { - write!( - writer, - "cell {cell}{sep}", - cell = jupyter_index.row_to_cell[start_location.row.get()], - sep = ":".cyan(), - )?; + let diagnostic_location = if let Some(jupyter_index) = jupyter_index { + write!( + writer, + "cell {cell}{sep}", + cell = jupyter_index + .cell(start_location.row.get()) + .unwrap_or_default(), + sep = ":".cyan(), + )?; - SourceLocation { - row: OneIndexed::new( - jupyter_index.row_to_row_in_cell[start_location.row.get()] as usize, - ) - .unwrap(), - column: start_location.column, - } - } else { - start_location - }; + SourceLocation { + row: OneIndexed::new( + jupyter_index + .cell_row(start_location.row.get()) + .unwrap_or(1) as usize, + ) + .unwrap(), + column: start_location.column, + } + } else { + start_location + }; writeln!( writer, @@ -105,7 +114,14 @@ impl Emitter for TextEmitter { )?; if self.flags.contains(EmitterFlags::SHOW_SOURCE) { - writeln!(writer, "{}", MessageCodeFrame { message })?; + writeln!( + writer, + "{}", + MessageCodeFrame { + message, + jupyter_index + } + )?; } if self.flags.contains(EmitterFlags::SHOW_FIX_DIFF) { @@ -149,6 +165,7 @@ impl Display for RuleCodeAndBody<'_> { pub(super) struct MessageCodeFrame<'a> { pub(crate) message: &'a Message, + pub(crate) jupyter_index: Option<&'a JupyterIndex>, } impl Display for MessageCodeFrame<'_> { @@ -173,6 +190,20 @@ impl Display for MessageCodeFrame<'_> { let content_start_index = source_code.line_index(range.start()); let mut start_index = content_start_index.saturating_sub(2); + // If we're working on a jupyter notebook, skip the lines which are + // outside of the cell containing the diagnostic. + if let Some(jupyter_index) = self.jupyter_index { + let content_start_cell = jupyter_index + .cell(content_start_index.get()) + .unwrap_or_default(); + while start_index < content_start_index { + if jupyter_index.cell(start_index.get()).unwrap_or_default() == content_start_cell { + break; + } + start_index = start_index.saturating_add(1); + } + } + // Trim leading empty lines. while start_index < content_start_index { if !source_code.line_text(start_index).trim().is_empty() { @@ -186,7 +217,21 @@ impl Display for MessageCodeFrame<'_> { .saturating_add(2) .min(OneIndexed::from_zero_indexed(source_code.line_count())); - // Trim trailing empty lines + // If we're working on a jupyter notebook, skip the lines which are + // outside of the cell containing the diagnostic. + if let Some(jupyter_index) = self.jupyter_index { + let content_end_cell = jupyter_index + .cell(content_end_index.get()) + .unwrap_or_default(); + while end_index > content_end_index { + if jupyter_index.cell(end_index.get()).unwrap_or_default() == content_end_cell { + break; + } + end_index = end_index.saturating_sub(1); + } + } + + // Trim trailing empty lines. while end_index > content_end_index { if !source_code.line_text(end_index).trim().is_empty() { break; @@ -215,7 +260,14 @@ impl Display for MessageCodeFrame<'_> { title: None, slices: vec![Slice { source: &source.text, - line_start: start_index.get(), + line_start: self.jupyter_index.map_or_else( + || start_index.get(), + |jupyter_index| { + jupyter_index + .cell_row(start_index.get()) + .unwrap_or_default() as usize + }, + ), annotations: vec![SourceAnnotation { label: &label, annotation_type: AnnotationType::Error, diff --git a/crates/ruff/src/noqa.rs b/crates/ruff/src/noqa.rs index 1d8aa49dae..0660b36702 100644 --- a/crates/ruff/src/noqa.rs +++ b/crates/ruff/src/noqa.rs @@ -1,123 +1,188 @@ use std::collections::BTreeMap; +use std::error::Error; use std::fmt::{Display, Write}; use std::fs; +use std::ops::Add; use std::path::Path; use anyhow::Result; use itertools::Itertools; use log::warn; -use once_cell::sync::Lazy; -use regex::Regex; use ruff_text_size::{TextLen, TextRange, TextSize}; +use rustpython_parser::ast::Ranged; use ruff_diagnostics::Diagnostic; use ruff_python_ast::source_code::Locator; -use ruff_python_whitespace::{LineEnding, PythonWhitespace}; +use ruff_python_whitespace::LineEnding; use crate::codes::NoqaCode; use crate::registry::{AsRule, Rule, RuleSet}; use crate::rule_redirects::get_redirect_target; -static NOQA_LINE_REGEX: Lazy = Lazy::new(|| { - Regex::new( - r"(?P\s*)(?P(?i:# noqa)(?::\s?(?P(?:[A-Z]+[0-9]+)(?:[,\s]+[A-Z]+[0-9]+)*))?)(?P\s*)", - ) - .unwrap() -}); - +/// A directive to ignore a set of rules for a given line of Python source code (e.g., +/// `# noqa: F401, F841`). #[derive(Debug)] pub(crate) enum Directive<'a> { - None, - // (leading spaces, noqa_range, trailing_spaces) - All(TextSize, TextRange, TextSize), - // (leading spaces, start_offset, end_offset, codes, trailing_spaces) - Codes(TextSize, TextRange, Vec<&'a str>, TextSize), + /// The `noqa` directive ignores all rules (e.g., `# noqa`). + All(All), + /// The `noqa` directive ignores specific rules (e.g., `# noqa: F401, F841`). + Codes(Codes<'a>), } -/// Extract the noqa `Directive` from a line of Python source code. -pub(crate) fn extract_noqa_directive<'a>(range: TextRange, locator: &'a Locator) -> Directive<'a> { - let text = &locator.contents()[range]; - match NOQA_LINE_REGEX.captures(text) { - Some(caps) => match ( - caps.name("leading_spaces"), - caps.name("noqa"), - caps.name("codes"), - caps.name("trailing_spaces"), - ) { - (Some(leading_spaces), Some(noqa), Some(codes), Some(trailing_spaces)) => { - let codes = codes - .as_str() - .split(|c: char| c.is_whitespace() || c == ',') - .map(str::trim) - .filter(|code| !code.is_empty()) - .collect_vec(); - let start = range.start() + TextSize::try_from(noqa.start()).unwrap(); - if codes.is_empty() { - #[allow(deprecated)] - let line = locator.compute_line_index(start); - warn!("Expected rule codes on `noqa` directive: \"{line}\""); - } - Directive::Codes( - leading_spaces.as_str().text_len(), - TextRange::at(start, noqa.as_str().text_len()), - codes, - trailing_spaces.as_str().text_len(), - ) +impl<'a> Directive<'a> { + /// Extract the noqa `Directive` from a line of Python source code. + pub(crate) fn try_extract(text: &'a str, offset: TextSize) -> Result, ParseError> { + for (char_index, char) in text.char_indices() { + // Only bother checking for the `noqa` literal if the character is `n` or `N`. + if !matches!(char, 'n' | 'N') { + continue; } - (Some(leading_spaces), Some(noqa), None, Some(trailing_spaces)) => Directive::All( - leading_spaces.as_str().text_len(), - TextRange::at( - range.start() + TextSize::try_from(noqa.start()).unwrap(), - noqa.as_str().text_len(), - ), - trailing_spaces.as_str().text_len(), - ), - _ => Directive::None, - }, - None => Directive::None, - } -} - -enum ParsedExemption<'a> { - None, - All, - Codes(Vec<&'a str>), -} - -/// Return a [`ParsedExemption`] for a given comment line. -fn parse_file_exemption(line: &str) -> ParsedExemption { - let line = line.trim_whitespace_start(); - - if line.starts_with("# flake8: noqa") - || line.starts_with("# flake8: NOQA") - || line.starts_with("# flake8: NoQA") - { - return ParsedExemption::All; - } - - if let Some(remainder) = line - .strip_prefix("# ruff: noqa") - .or_else(|| line.strip_prefix("# ruff: NOQA")) - .or_else(|| line.strip_prefix("# ruff: NoQA")) - { - if remainder.is_empty() { - return ParsedExemption::All; - } else if let Some(codes) = remainder.strip_prefix(':') { - let codes = codes - .split(|c: char| c.is_whitespace() || c == ',') - .map(str::trim) - .filter(|code| !code.is_empty()) - .collect_vec(); - if codes.is_empty() { - warn!("Expected rule codes on `noqa` directive: \"{line}\""); + // Determine the start of the `noqa` literal. + if !matches!( + text[char_index..].as_bytes(), + [b'n' | b'N', b'o' | b'O', b'q' | b'Q', b'a' | b'A', ..] + ) { + continue; } - return ParsedExemption::Codes(codes); + + let noqa_literal_start = char_index; + let noqa_literal_end = noqa_literal_start + "noqa".len(); + + // Determine the start of the comment. + let mut comment_start = noqa_literal_start; + + // Trim any whitespace between the `#` character and the `noqa` literal. + comment_start = text[..comment_start].trim_end().len(); + + // The next character has to be the `#` character. + if text[..comment_start] + .chars() + .last() + .map_or(false, |c| c != '#') + { + continue; + } + comment_start -= '#'.len_utf8(); + + // If the next character is `:`, then it's a list of codes. Otherwise, it's a directive + // to ignore all rules. + return Ok(Some( + if text[noqa_literal_end..] + .chars() + .next() + .map_or(false, |c| c == ':') + { + // E.g., `# noqa: F401, F841`. + let mut codes_start = noqa_literal_end; + + // Skip the `:` character. + codes_start += ':'.len_utf8(); + + // Skip any whitespace between the `:` and the codes. + codes_start += text[codes_start..] + .find(|c: char| !c.is_whitespace()) + .unwrap_or(0); + + // Extract the comma-separated list of codes. + let mut codes = vec![]; + let mut codes_end = codes_start; + let mut leading_space = 0; + while let Some(code) = Self::lex_code(&text[codes_end + leading_space..]) { + codes.push(code); + codes_end += leading_space; + codes_end += code.len(); + + // Codes can be comma- or whitespace-delimited. Compute the length of the + // delimiter, but only add it in the next iteration, once we find the next + // code. + if let Some(space_between) = + text[codes_end..].find(|c: char| !(c.is_whitespace() || c == ',')) + { + leading_space = space_between; + } else { + break; + } + } + + // If we didn't identify any codes, warn. + if codes.is_empty() { + return Err(ParseError::MissingCodes); + } + + let range = TextRange::new( + TextSize::try_from(comment_start).unwrap(), + TextSize::try_from(codes_end).unwrap(), + ); + + Self::Codes(Codes { + range: range.add(offset), + codes, + }) + } else { + // E.g., `# noqa`. + let range = TextRange::new( + TextSize::try_from(comment_start).unwrap(), + TextSize::try_from(noqa_literal_end).unwrap(), + ); + Self::All(All { + range: range.add(offset), + }) + }, + )); } - warn!("Unexpected suffix on `noqa` directive: \"{line}\""); + + Ok(None) } - ParsedExemption::None + /// Lex an individual rule code (e.g., `F401`). + #[inline] + fn lex_code(line: &str) -> Option<&str> { + // Extract, e.g., the `F` in `F401`. + let prefix = line.chars().take_while(char::is_ascii_uppercase).count(); + // Extract, e.g., the `401` in `F401`. + let suffix = line[prefix..] + .chars() + .take_while(char::is_ascii_digit) + .count(); + if prefix > 0 && suffix > 0 { + Some(&line[..prefix + suffix]) + } else { + None + } + } +} + +#[derive(Debug)] +pub(crate) struct All { + range: TextRange, +} + +impl Ranged for All { + /// The range of the `noqa` directive. + fn range(&self) -> TextRange { + self.range + } +} + +#[derive(Debug)] +pub(crate) struct Codes<'a> { + range: TextRange, + codes: Vec<&'a str>, +} + +impl Codes<'_> { + /// The codes that are ignored by the `noqa` directive. + pub(crate) fn codes(&self) -> &[&str] { + &self.codes + } +} + +impl Ranged for Codes<'_> { + /// The range of the `noqa` directive. + fn range(&self) -> TextRange { + self.range + } } /// Returns `true` if the string list of `codes` includes `code` (or an alias @@ -138,50 +203,230 @@ pub(crate) fn rule_is_ignored( ) -> bool { let offset = noqa_line_for.resolve(offset); let line_range = locator.line_range(offset); - match extract_noqa_directive(line_range, locator) { - Directive::None => false, - Directive::All(..) => true, - Directive::Codes(.., codes, _) => includes(code, &codes), + match Directive::try_extract(locator.slice(line_range), line_range.start()) { + Ok(Some(Directive::All(_))) => true, + Ok(Some(Directive::Codes(Codes { codes, range: _ }))) => includes(code, &codes), + _ => false, } } +/// The file-level exemptions extracted from a given Python file. +#[derive(Debug)] pub(crate) enum FileExemption { - None, + /// The file is exempt from all rules. All, + /// The file is exempt from the given rules. Codes(Vec), } -/// Extract the [`FileExemption`] for a given Python source file, enumerating any rules that are -/// globally ignored within the file. -pub(crate) fn file_exemption(contents: &str, comment_ranges: &[TextRange]) -> FileExemption { - let mut exempt_codes: Vec = vec![]; +impl FileExemption { + /// Extract the [`FileExemption`] for a given Python source file, enumerating any rules that are + /// globally ignored within the file. + pub(crate) fn try_extract( + contents: &str, + comment_ranges: &[TextRange], + locator: &Locator, + ) -> Option { + let mut exempt_codes: Vec = vec![]; - for range in comment_ranges { - match parse_file_exemption(&contents[*range]) { - ParsedExemption::All => { - return FileExemption::All; + for range in comment_ranges { + match ParsedFileExemption::try_extract(&contents[*range]) { + Err(err) => { + #[allow(deprecated)] + let line = locator.compute_line_index(range.start()); + warn!("Invalid `# noqa` directive on line {line}: {err}"); + } + Ok(Some(ParsedFileExemption::All)) => { + return Some(Self::All); + } + Ok(Some(ParsedFileExemption::Codes(codes))) => { + exempt_codes.extend(codes.into_iter().filter_map(|code| { + if let Ok(rule) = Rule::from_code(get_redirect_target(code).unwrap_or(code)) + { + Some(rule.noqa_code()) + } else { + #[allow(deprecated)] + let line = locator.compute_line_index(range.start()); + warn!("Invalid code provided to `# ruff: noqa` on line {line}: {code}"); + None + } + })); + } + Ok(None) => {} } - ParsedExemption::Codes(codes) => { - exempt_codes.extend(codes.into_iter().filter_map(|code| { - if let Ok(rule) = Rule::from_code(get_redirect_target(code).unwrap_or(code)) { - Some(rule.noqa_code()) - } else { - warn!("Invalid code provided to `# ruff: noqa`: {}", code); - None - } - })); + } + + if exempt_codes.is_empty() { + None + } else { + Some(Self::Codes(exempt_codes)) + } + } +} + +/// An individual file-level exemption (e.g., `# ruff: noqa` or `# ruff: noqa: F401, F841`). Like +/// [`FileExemption`], but only for a single line, as opposed to an aggregated set of exemptions +/// across a source file. +#[derive(Debug)] +enum ParsedFileExemption<'a> { + /// The file-level exemption ignores all rules (e.g., `# ruff: noqa`). + All, + /// The file-level exemption ignores specific rules (e.g., `# ruff: noqa: F401, F841`). + Codes(Vec<&'a str>), +} + +impl<'a> ParsedFileExemption<'a> { + /// Return a [`ParsedFileExemption`] for a given comment line. + fn try_extract(line: &'a str) -> Result, ParseError> { + let line = Self::lex_whitespace(line); + let Some(line) = Self::lex_char(line, '#') else { + return Ok(None); + }; + let line = Self::lex_whitespace(line); + + let Some(line) = Self::lex_flake8(line).or_else(|| Self::lex_ruff(line)) else { + return Ok(None); + }; + + let line = Self::lex_whitespace(line); + let Some(line) = Self::lex_char(line, ':') else { + return Ok(None); + }; + let line = Self::lex_whitespace(line); + let Some(line) = Self::lex_noqa(line) else { + return Ok(None); + }; + let line = Self::lex_whitespace(line); + + Ok(Some(if line.is_empty() { + // Ex) `# ruff: noqa` + Self::All + } else { + // Ex) `# ruff: noqa: F401, F841` + let Some(line) = Self::lex_char(line, ':') else { + return Err(ParseError::InvalidSuffix); + }; + let line = Self::lex_whitespace(line); + + // Extract the codes from the line (e.g., `F401, F841`). + let mut codes = vec![]; + let mut line = line; + while let Some(code) = Self::lex_code(line) { + codes.push(code); + line = &line[code.len()..]; + + // Codes can be comma- or whitespace-delimited. + if let Some(rest) = Self::lex_delimiter(line).map(Self::lex_whitespace) { + line = rest; + } else { + break; + } } - ParsedExemption::None => {} + + // If we didn't identify any codes, warn. + if codes.is_empty() { + return Err(ParseError::MissingCodes); + } + + Self::Codes(codes) + })) + } + + /// Lex optional leading whitespace. + #[inline] + fn lex_whitespace(line: &str) -> &str { + line.trim_start() + } + + /// Lex a specific character, or return `None` if the character is not the first character in + /// the line. + #[inline] + fn lex_char(line: &str, c: char) -> Option<&str> { + let mut chars = line.chars(); + if chars.next() == Some(c) { + Some(chars.as_str()) + } else { + None } } - if exempt_codes.is_empty() { - FileExemption::None - } else { - FileExemption::Codes(exempt_codes) + /// Lex the "flake8" prefix of a `noqa` directive. + #[inline] + fn lex_flake8(line: &str) -> Option<&str> { + line.strip_prefix("flake8") + } + + /// Lex the "ruff" prefix of a `noqa` directive. + #[inline] + fn lex_ruff(line: &str) -> Option<&str> { + line.strip_prefix("ruff") + } + + /// Lex a `noqa` directive with case-insensitive matching. + #[inline] + fn lex_noqa(line: &str) -> Option<&str> { + match line.as_bytes() { + [b'n' | b'N', b'o' | b'O', b'q' | b'Q', b'a' | b'A', ..] => Some(&line["noqa".len()..]), + _ => None, + } + } + + /// Lex a code delimiter, which can either be a comma or whitespace. + #[inline] + fn lex_delimiter(line: &str) -> Option<&str> { + let mut chars = line.chars(); + if let Some(c) = chars.next() { + if c == ',' || c.is_whitespace() { + Some(chars.as_str()) + } else { + None + } + } else { + None + } + } + + /// Lex an individual rule code (e.g., `F401`). + #[inline] + fn lex_code(line: &str) -> Option<&str> { + // Extract, e.g., the `F` in `F401`. + let prefix = line.chars().take_while(char::is_ascii_uppercase).count(); + // Extract, e.g., the `401` in `F401`. + let suffix = line[prefix..] + .chars() + .take_while(char::is_ascii_digit) + .count(); + if prefix > 0 && suffix > 0 { + Some(&line[..prefix + suffix]) + } else { + None + } } } +/// The result of an [`Importer::get_or_import_symbol`] call. +#[derive(Debug)] +pub(crate) enum ParseError { + /// The `noqa` directive was missing valid codes (e.g., `# noqa: unused-import` instead of `# noqa: F401`). + MissingCodes, + /// The `noqa` directive used an invalid suffix (e.g., `# noqa; F401` instead of `# noqa: F401`). + InvalidSuffix, +} + +impl Display for ParseError { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ParseError::MissingCodes => fmt.write_str("expected a comma-separated list of codes (e.g., `# noqa: F401, F841`)."), + ParseError::InvalidSuffix => { + fmt.write_str("expected `:` followed by a comma-separated list of codes (e.g., `# noqa: F401, F841`).") + } + + } + } +} + +impl Error for ParseError {} + /// Adds noqa comments to suppress all diagnostics of a file. pub(crate) fn add_noqa( path: &Path, @@ -215,23 +460,23 @@ fn add_noqa_inner( // Whether the file is exempted from all checks. // Codes that are globally exempted (within the current file). - let exemption = file_exemption(locator.contents(), commented_ranges); + let exemption = FileExemption::try_extract(locator.contents(), commented_ranges, locator); let directives = NoqaDirectives::from_commented_ranges(commented_ranges, locator); // Mark any non-ignored diagnostics. for diagnostic in diagnostics { match &exemption { - FileExemption::All => { + Some(FileExemption::All) => { // If the file is exempted, don't add any noqa directives. continue; } - FileExemption::Codes(codes) => { + Some(FileExemption::Codes(codes)) => { // If the diagnostic is ignored by a global exemption, don't add a noqa directive. if codes.contains(&diagnostic.kind.rule().noqa_code()) { continue; } } - FileExemption::None => {} + None => {} } // Is the violation ignored by a `noqa` directive on the parent line? @@ -240,28 +485,27 @@ fn add_noqa_inner( directives.find_line_with_directive(noqa_line_for.resolve(parent)) { match &directive_line.directive { - Directive::All(..) => { + Directive::All(_) => { continue; } - Directive::Codes(.., codes, _) => { + Directive::Codes(Codes { codes, range: _ }) => { if includes(diagnostic.kind.rule(), codes) { continue; } } - Directive::None => {} } } } let noqa_offset = noqa_line_for.resolve(diagnostic.start()); - // Or ignored by the directive itself + // Or ignored by the directive itself? if let Some(directive_line) = directives.find_line_with_directive(noqa_offset) { match &directive_line.directive { - Directive::All(..) => { + Directive::All(_) => { continue; } - Directive::Codes(.., codes, _) => { + Directive::Codes(Codes { codes, range: _ }) => { let rule = diagnostic.kind.rule(); if !includes(rule, codes) { matches_by_line @@ -274,7 +518,6 @@ fn add_noqa_inner( } continue; } - Directive::None => {} } } @@ -296,7 +539,7 @@ fn add_noqa_inner( let line = locator.full_line(offset); match directive { - None | Some(Directive::None) => { + None => { // Add existing content. output.push_str(line.trim_end()); @@ -308,10 +551,10 @@ fn add_noqa_inner( output.push_str(&line_ending); count += 1; } - Some(Directive::All(..)) => { + Some(Directive::All(_)) => { // Does not get inserted into the map. } - Some(Directive::Codes(_, noqa_range, existing, _)) => { + Some(Directive::Codes(Codes { range, codes })) => { // Reconstruct the line based on the preserved rule codes. // This enables us to tally the number of edits. let output_start = output.len(); @@ -319,7 +562,7 @@ fn add_noqa_inner( // Add existing content. output.push_str( locator - .slice(TextRange::new(offset, noqa_range.start())) + .slice(TextRange::new(offset, range.start())) .trim_end(), ); @@ -331,8 +574,8 @@ fn add_noqa_inner( &mut output, rules .iter() - .map(|r| r.noqa_code().to_string()) - .chain(existing.iter().map(ToString::to_string)) + .map(|rule| rule.noqa_code().to_string()) + .chain(codes.iter().map(ToString::to_string)) .sorted_unstable(), ); @@ -366,9 +609,11 @@ fn push_codes(str: &mut String, codes: impl Iterator) { #[derive(Debug)] pub(crate) struct NoqaDirectiveLine<'a> { - // The range of the text line for which the noqa directive applies. + /// The range of the text line for which the noqa directive applies. pub(crate) range: TextRange, + /// The noqa directive. pub(crate) directive: Directive<'a>, + /// The codes that are ignored by the directive. pub(crate) matches: Vec, } @@ -384,21 +629,23 @@ impl<'a> NoqaDirectives<'a> { ) -> Self { let mut directives = Vec::new(); - for comment_range in comment_ranges { - let line_range = locator.line_range(comment_range.start()); - let directive = match extract_noqa_directive(line_range, locator) { - Directive::None => { - continue; + for range in comment_ranges { + match Directive::try_extract(locator.slice(*range), range.start()) { + Err(err) => { + #[allow(deprecated)] + let line = locator.compute_line_index(range.start()); + warn!("Invalid `# noqa` directive on line {line}: {err}"); } - directive @ (Directive::All(..) | Directive::Codes(..)) => directive, - }; - - // noqa comments are guaranteed to be single line. - directives.push(NoqaDirectiveLine { - range: line_range, - directive, - matches: Vec::new(), - }); + Ok(Some(directive)) => { + // noqa comments are guaranteed to be single line. + directives.push(NoqaDirectiveLine { + range: locator.line_range(range.start()), + directive, + matches: Vec::new(), + }); + } + Ok(None) => {} + } } // Extend a mapping at the end of the file to also include the EOF token. @@ -460,7 +707,7 @@ impl NoqaMapping { } /// Returns the re-mapped position or `position` if no mapping exists. - pub fn resolve(&self, offset: TextSize) -> TextSize { + pub(crate) fn resolve(&self, offset: TextSize) -> TextSize { let index = self.ranges.binary_search_by(|range| { if range.end() < offset { std::cmp::Ordering::Less @@ -478,7 +725,7 @@ impl NoqaMapping { } } - pub fn push_mapping(&mut self, range: TextRange) { + pub(crate) fn push_mapping(&mut self, range: TextRange) { if let Some(last_range) = self.ranges.last_mut() { // Strictly sorted insertion if last_range.end() <= range.start() { @@ -511,28 +758,190 @@ impl FromIterator for NoqaMapping { #[cfg(test)] mod tests { + use insta::assert_debug_snapshot; use ruff_text_size::{TextRange, TextSize}; use ruff_diagnostics::Diagnostic; use ruff_python_ast::source_code::Locator; use ruff_python_whitespace::LineEnding; - use crate::noqa::{add_noqa_inner, NoqaMapping, NOQA_LINE_REGEX}; + use crate::noqa::{add_noqa_inner, Directive, NoqaMapping, ParsedFileExemption}; use crate::rules::pycodestyle::rules::AmbiguousVariableName; - use crate::rules::pyflakes; + use crate::rules::pyflakes::rules::UnusedVariable; #[test] - fn regex() { - assert!(NOQA_LINE_REGEX.is_match("# noqa")); - assert!(NOQA_LINE_REGEX.is_match("# NoQA")); + fn noqa_all() { + let source = "# noqa"; + assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); + } - assert!(NOQA_LINE_REGEX.is_match("# noqa: F401")); - assert!(NOQA_LINE_REGEX.is_match("# NoQA: F401")); - assert!(NOQA_LINE_REGEX.is_match("# noqa: F401, E501")); + #[test] + fn noqa_code() { + let source = "# noqa: F401"; + assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); + } - assert!(NOQA_LINE_REGEX.is_match("# noqa:F401")); - assert!(NOQA_LINE_REGEX.is_match("# NoQA:F401")); - assert!(NOQA_LINE_REGEX.is_match("# noqa:F401, E501")); + #[test] + fn noqa_codes() { + let source = "# noqa: F401, F841"; + assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); + } + + #[test] + fn noqa_all_case_insensitive() { + let source = "# NOQA"; + assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); + } + + #[test] + fn noqa_code_case_insensitive() { + let source = "# NOQA: F401"; + assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); + } + + #[test] + fn noqa_codes_case_insensitive() { + let source = "# NOQA: F401, F841"; + assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); + } + + #[test] + fn noqa_leading_space() { + let source = "# # noqa: F401"; + assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); + } + + #[test] + fn noqa_trailing_space() { + let source = "# noqa: F401 #"; + assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); + } + + #[test] + fn noqa_all_no_space() { + let source = "#noqa"; + assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); + } + + #[test] + fn noqa_code_no_space() { + let source = "#noqa:F401"; + assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); + } + + #[test] + fn noqa_codes_no_space() { + let source = "#noqa:F401,F841"; + assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); + } + + #[test] + fn noqa_all_multi_space() { + let source = "# noqa"; + assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); + } + + #[test] + fn noqa_code_multi_space() { + let source = "# noqa: F401"; + assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); + } + + #[test] + fn noqa_codes_multi_space() { + let source = "# noqa: F401, F841"; + assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); + } + + #[test] + fn noqa_all_leading_comment() { + let source = "# Some comment describing the noqa # noqa"; + assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); + } + + #[test] + fn noqa_code_leading_comment() { + let source = "# Some comment describing the noqa # noqa: F401"; + assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); + } + + #[test] + fn noqa_codes_leading_comment() { + let source = "# Some comment describing the noqa # noqa: F401, F841"; + assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); + } + + #[test] + fn noqa_all_trailing_comment() { + let source = "# noqa # Some comment describing the noqa"; + assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); + } + + #[test] + fn noqa_code_trailing_comment() { + let source = "# noqa: F401 # Some comment describing the noqa"; + assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); + } + + #[test] + fn noqa_codes_trailing_comment() { + let source = "# noqa: F401, F841 # Some comment describing the noqa"; + assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); + } + + #[test] + fn noqa_invalid_codes() { + let source = "# noqa: unused-import, F401, some other code"; + assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); + } + + #[test] + fn flake8_exemption_all() { + let source = "# flake8: noqa"; + assert_debug_snapshot!(ParsedFileExemption::try_extract(source)); + } + + #[test] + fn ruff_exemption_all() { + let source = "# ruff: noqa"; + assert_debug_snapshot!(ParsedFileExemption::try_extract(source)); + } + + #[test] + fn flake8_exemption_all_no_space() { + let source = "#flake8:noqa"; + assert_debug_snapshot!(ParsedFileExemption::try_extract(source)); + } + + #[test] + fn ruff_exemption_all_no_space() { + let source = "#ruff:noqa"; + assert_debug_snapshot!(ParsedFileExemption::try_extract(source)); + } + + #[test] + fn flake8_exemption_codes() { + // Note: Flake8 doesn't support this; it's treated as a blanket exemption. + let source = "# flake8: noqa: F401, F841"; + assert_debug_snapshot!(ParsedFileExemption::try_extract(source)); + } + + #[test] + fn ruff_exemption_codes() { + let source = "# ruff: noqa: F401, F841"; + assert_debug_snapshot!(ParsedFileExemption::try_extract(source)); + } + + #[test] + fn flake8_exemption_all_case_insensitive() { + let source = "# flake8: NoQa"; + assert_debug_snapshot!(ParsedFileExemption::try_extract(source)); + } + + #[test] + fn ruff_exemption_all_case_insensitive() { + let source = "# ruff: NoQa"; + assert_debug_snapshot!(ParsedFileExemption::try_extract(source)); } #[test] @@ -550,7 +959,7 @@ mod tests { assert_eq!(output, format!("{contents}")); let diagnostics = [Diagnostic::new( - pyflakes::rules::UnusedVariable { + UnusedVariable { name: "x".to_string(), }, TextRange::new(TextSize::from(0), TextSize::from(0)), @@ -574,7 +983,7 @@ mod tests { TextRange::new(TextSize::from(0), TextSize::from(0)), ), Diagnostic::new( - pyflakes::rules::UnusedVariable { + UnusedVariable { name: "x".to_string(), }, TextRange::new(TextSize::from(0), TextSize::from(0)), @@ -598,7 +1007,7 @@ mod tests { TextRange::new(TextSize::from(0), TextSize::from(0)), ), Diagnostic::new( - pyflakes::rules::UnusedVariable { + UnusedVariable { name: "x".to_string(), }, TextRange::new(TextSize::from(0), TextSize::from(0)), diff --git a/crates/ruff/src/packaging.rs b/crates/ruff/src/packaging.rs index c80eedecd0..c746bb4d6f 100644 --- a/crates/ruff/src/packaging.rs +++ b/crates/ruff/src/packaging.rs @@ -78,7 +78,7 @@ fn detect_package_root_with_cache<'a>( current } -/// Return a mapping from Python file to its package root. +/// Return a mapping from Python package to its package root. pub fn detect_package_roots<'a>( files: &[&'a Path], resolver: &'a Resolver, diff --git a/crates/ruff/src/pyproject_toml.rs b/crates/ruff/src/pyproject_toml.rs index cabdff1b39..a0cfeb961b 100644 --- a/crates/ruff/src/pyproject_toml.rs +++ b/crates/ruff/src/pyproject_toml.rs @@ -7,7 +7,9 @@ use ruff_diagnostics::Diagnostic; use ruff_python_ast::source_code::SourceFile; use crate::message::Message; +use crate::registry::Rule; use crate::rules::ruff::rules::InvalidPyprojectToml; +use crate::settings::Settings; use crate::IOError; /// Unlike [`pyproject_toml::PyProjectToml`], in our case `build_system` is also optional @@ -20,9 +22,11 @@ struct PyProjectToml { project: Option, } -pub fn lint_pyproject_toml(source_file: SourceFile) -> Result> { +pub fn lint_pyproject_toml(source_file: SourceFile, settings: &Settings) -> Result> { + let mut messages = vec![]; + let err = match toml::from_str::(source_file.source_text()) { - Ok(_) => return Ok(Vec::default()), + Ok(_) => return Ok(messages), Err(err) => err, }; @@ -32,17 +36,20 @@ pub fn lint_pyproject_toml(source_file: SourceFile) -> Result> { None => TextRange::default(), Some(range) => { let Ok(end) = TextSize::try_from(range.end) else { - let diagnostic = Diagnostic::new( - IOError { - message: "pyproject.toml is larger than 4GB".to_string(), - }, - TextRange::default(), - ); - return Ok(vec![Message::from_diagnostic( - diagnostic, - source_file, - TextSize::default(), - )]); + if settings.rules.enabled(Rule::IOError) { + let diagnostic = Diagnostic::new( + IOError { + message: "pyproject.toml is larger than 4GB".to_string(), + }, + TextRange::default(), + ); + messages.push(Message::from_diagnostic( + diagnostic, + source_file, + TextSize::default(), + )); + } + return Ok(messages); }; TextRange::new( // start <= end, so if end < 4GB follows start < 4GB @@ -52,11 +59,15 @@ pub fn lint_pyproject_toml(source_file: SourceFile) -> Result> { } }; - let toml_err = err.message().to_string(); - let diagnostic = Diagnostic::new(InvalidPyprojectToml { message: toml_err }, range); - Ok(vec![Message::from_diagnostic( - diagnostic, - source_file, - TextSize::default(), - )]) + if settings.rules.enabled(Rule::InvalidPyprojectToml) { + let toml_err = err.message().to_string(); + let diagnostic = Diagnostic::new(InvalidPyprojectToml { message: toml_err }, range); + messages.push(Message::from_diagnostic( + diagnostic, + source_file, + TextSize::default(), + )); + } + + Ok(messages) } diff --git a/crates/ruff/src/registry.rs b/crates/ruff/src/registry.rs index 5100ec9384..4b05c341a8 100644 --- a/crates/ruff/src/registry.rs +++ b/crates/ruff/src/registry.rs @@ -7,7 +7,7 @@ pub use codes::Rule; use ruff_macros::RuleNamespace; pub use rule_set::{RuleSet, RuleSetIterator}; -use crate::codes::{self, RuleCodePrefix}; +use crate::codes::{self}; mod rule_set; @@ -18,8 +18,10 @@ pub trait AsRule { impl Rule { pub fn from_code(code: &str) -> Result { let (linter, code) = Linter::parse_code(code).ok_or(FromCodeError::Unknown)?; - let prefix: RuleCodePrefix = RuleCodePrefix::parse(&linter, code)?; - Ok(prefix.into_iter().next().unwrap()) + linter + .all_rules() + .find(|rule| rule.noqa_code().suffix() == code) + .ok_or(FromCodeError::Unknown) } } @@ -80,6 +82,9 @@ pub enum Linter { /// [flake8-commas](https://pypi.org/project/flake8-commas/) #[prefix = "COM"] Flake8Commas, + /// [flake8-copyright](https://pypi.org/project/flake8-copyright/) + #[prefix = "CPY"] + Flake8Copyright, /// [flake8-comprehensions](https://pypi.org/project/flake8-comprehensions/) #[prefix = "C4"] Flake8Comprehensions, @@ -107,7 +112,7 @@ pub enum Linter { /// [flake8-import-conventions](https://github.com/joaopalmeiro/flake8-import-conventions) #[prefix = "ICN"] Flake8ImportConventions, - /// [flake8-logging-format](https://pypi.org/project/flake8-logging-format/0.9.0/) + /// [flake8-logging-format](https://pypi.org/project/flake8-logging-format/) #[prefix = "G"] Flake8LoggingFormat, /// [flake8-no-pep420](https://pypi.org/project/flake8-no-pep420/) @@ -176,7 +181,7 @@ pub enum Linter { /// [Pylint](https://pypi.org/project/pylint/) #[prefix = "PL"] Pylint, - /// [tryceratops](https://pypi.org/project/tryceratops/1.1.0/) + /// [tryceratops](https://pypi.org/project/tryceratops/) #[prefix = "TRY"] Tryceratops, /// [flynt](https://pypi.org/project/flynt/) @@ -188,6 +193,9 @@ pub enum Linter { /// [Airflow](https://pypi.org/project/apache-airflow/) #[prefix = "AIR"] Airflow, + /// [Perflint](https://pypi.org/project/perflint/) + #[prefix = "PERF"] + Perflint, /// Ruff-specific rules #[prefix = "RUF"] Ruff, @@ -210,30 +218,6 @@ pub trait RuleNamespace: Sized { fn url(&self) -> Option<&'static str>; } -/// The prefix and name for an upstream linter category. -pub struct UpstreamCategory(pub RuleCodePrefix, pub &'static str); - -impl Linter { - pub const fn upstream_categories(&self) -> Option<&'static [UpstreamCategory]> { - match self { - Linter::Pycodestyle => Some(&[ - UpstreamCategory(RuleCodePrefix::Pycodestyle(codes::Pycodestyle::E), "Error"), - UpstreamCategory( - RuleCodePrefix::Pycodestyle(codes::Pycodestyle::W), - "Warning", - ), - ]), - Linter::Pylint => Some(&[ - UpstreamCategory(RuleCodePrefix::Pylint(codes::Pylint::C), "Convention"), - UpstreamCategory(RuleCodePrefix::Pylint(codes::Pylint::E), "Error"), - UpstreamCategory(RuleCodePrefix::Pylint(codes::Pylint::R), "Refactor"), - UpstreamCategory(RuleCodePrefix::Pylint(codes::Pylint::W), "Warning"), - ]), - _ => None, - } - } -} - #[derive(is_macro::Is, Copy, Clone)] pub enum LintSource { Ast, @@ -244,6 +228,7 @@ pub enum LintSource { Imports, Noqa, Filesystem, + PyprojectToml, } impl Rule { @@ -251,6 +236,7 @@ impl Rule { /// physical lines). pub const fn lint_source(&self) -> LintSource { match self { + Rule::InvalidPyprojectToml => LintSource::PyprojectToml, Rule::UnusedNOQA => LintSource::Noqa, Rule::BlanketNOQA | Rule::BlanketTypeIgnore @@ -267,6 +253,7 @@ impl Rule { | Rule::ShebangLeadingWhitespace | Rule::TrailingWhitespace | Rule::TabIndentation + | Rule::MissingCopyrightNotice | Rule::BlankLineWithWhitespace => LintSource::PhysicalLines, Rule::AmbiguousUnicodeCharacterComment | Rule::AmbiguousUnicodeCharacterDocstring @@ -340,6 +327,13 @@ impl Rule { _ => LintSource::Ast, } } + + /// Return the URL for the rule documentation, if it exists. + pub fn url(&self) -> Option { + self.explanation() + .is_some() + .then(|| format!("{}/rules/{}", env!("CARGO_PKG_HOMEPAGE"), self.as_ref())) + } } /// Pairs of checks that shouldn't be enabled together. diff --git a/crates/ruff/src/registry/rule_set.rs b/crates/ruff/src/registry/rule_set.rs index 7fdbf8b19d..555ba0e5b2 100644 --- a/crates/ruff/src/registry/rule_set.rs +++ b/crates/ruff/src/registry/rule_set.rs @@ -3,7 +3,7 @@ use ruff_macros::CacheKey; use std::fmt::{Debug, Formatter}; use std::iter::FusedIterator; -const RULESET_SIZE: usize = 10; +const RULESET_SIZE: usize = 11; /// A set of [`Rule`]s. /// @@ -13,7 +13,6 @@ pub struct RuleSet([u64; RULESET_SIZE]); impl RuleSet { const EMPTY: [u64; RULESET_SIZE] = [0; RULESET_SIZE]; - // 64 fits into a u16 without truncation #[allow(clippy::cast_possible_truncation)] const SLICE_BITS: u16 = u64::BITS as u16; @@ -290,8 +289,8 @@ impl Extend for RuleSet { } impl IntoIterator for RuleSet { - type Item = Rule; type IntoIter = RuleSetIterator; + type Item = Rule; fn into_iter(self) -> Self::IntoIter { self.iter() @@ -299,8 +298,8 @@ impl IntoIterator for RuleSet { } impl IntoIterator for &RuleSet { - type Item = Rule; type IntoIter = RuleSetIterator; + type Item = Rule; fn into_iter(self) -> Self::IntoIter { self.iter() diff --git a/crates/ruff/src/renamer.rs b/crates/ruff/src/renamer.rs new file mode 100644 index 0000000000..14aec66ef7 --- /dev/null +++ b/crates/ruff/src/renamer.rs @@ -0,0 +1,259 @@ +//! Code modification struct to support symbol renaming within a scope. + +use anyhow::{anyhow, Result}; +use itertools::Itertools; + +use ruff_diagnostics::Edit; +use ruff_python_semantic::{Binding, BindingKind, Scope, ScopeId, SemanticModel}; + +pub(crate) struct Renamer; + +impl Renamer { + /// Rename a symbol (from `name` to `target`). + /// + /// ## How it works + /// + /// The renaming algorithm is as follows: + /// + /// 1. Determine the scope in which the rename should occur. This is typically the scope passed + /// in by the caller. However, if a symbol is `nonlocal` or `global`, then the rename needs + /// to occur in the scope in which the symbol is declared. For example, attempting to rename + /// `x` in `foo` below should trigger a rename in the module scope: + /// + /// ```python + /// x = 1 + /// + /// def foo(): + /// global x + /// x = 2 + /// ``` + /// + /// 1. Determine whether the symbol is rebound in another scope. This is effectively the inverse + /// of the previous step: when attempting to rename `x` in the module scope, we need to + /// detect that `x` is rebound in the `foo` scope. Determine every scope in which the symbol + /// is rebound, and add it to the set of scopes in which the rename should occur. + /// + /// 1. Start with the first scope in the stack. Take the first [`Binding`] in the scope, for the + /// given name. For example, in the following snippet, we'd start by examining the `x = 1` + /// binding: + /// + /// ```python + /// if True: + /// x = 1 + /// print(x) + /// else: + /// x = 2 + /// print(x) + /// + /// print(x) + /// ``` + /// + /// 1. Rename the [`Binding`]. In most cases, this is a simple replacement. For example, + /// renaming `x` to `y` above would require replacing `x = 1` with `y = 1`. After the + /// first replacement in the snippet above, we'd have: + /// + /// ```python + /// if True: + /// y = 1 + /// print(x) + /// else: + /// x = 2 + /// print(x) + /// + /// print(x) + /// ``` + /// + /// Note that, when renaming imports, we need to instead rename (or add) an alias. For + /// example, to rename `pandas` to `pd`, we may need to rewrite `import pandas` to + /// `import pandas as pd`, rather than `import pd`. + /// + /// 1. Rename every reference to the [`Binding`]. For example, renaming the references to the + /// `x = 1` binding above would give us: + /// + /// ```python + /// if True: + /// y = 1 + /// print(y) + /// else: + /// x = 2 + /// print(x) + /// + /// print(x) + /// ``` + /// + /// 1. Rename every delayed annotation. (See [`SemanticModel::delayed_annotations`].) + /// + /// 1. Repeat the above process for every [`Binding`] in the scope with the given name. + /// After renaming the `x = 2` binding, we'd have: + /// + /// ```python + /// if True: + /// y = 1 + /// print(y) + /// else: + /// y = 2 + /// print(y) + /// + /// print(y) + /// ``` + /// + /// 1. Repeat the above process for every scope in the stack. + pub(crate) fn rename( + name: &str, + target: &str, + scope: &Scope, + semantic: &SemanticModel, + ) -> Result<(Edit, Vec)> { + let mut edits = vec![]; + + // Determine whether the symbol is `nonlocal` or `global`. (A symbol can't be both; Python + // raises a `SyntaxError`.) If the symbol is `nonlocal` or `global`, we need to rename it in + // the scope in which it's declared, rather than the current scope. For example, given: + // + // ```python + // x = 1 + // + // def foo(): + // global x + // ``` + // + // When renaming `x` in `foo`, we detect that `x` is a global, and back out to the module + // scope. + let scope_id = scope.get_all(name).find_map(|binding_id| { + let binding = semantic.binding(binding_id); + match binding.kind { + BindingKind::Global => Some(ScopeId::global()), + BindingKind::Nonlocal(symbol_id) => Some(symbol_id), + _ => None, + } + }); + + let scope = scope_id.map_or(scope, |scope_id| &semantic.scopes[scope_id]); + edits.extend(Renamer::rename_in_scope(name, target, scope, semantic)); + + // Find any scopes in which the symbol is referenced as `nonlocal` or `global`. For example, + // given: + // + // ```python + // x = 1 + // + // def foo(): + // global x + // + // def bar(): + // global x + // ``` + // + // When renaming `x` in `foo`, we detect that `x` is a global, and back out to the module + // scope. But we need to rename `x` in `bar` too. + // + // Note that it's impossible for a symbol to be referenced as both `nonlocal` and `global` + // in the same program. If a symbol is referenced as `global`, then it must be defined in + // the module scope. If a symbol is referenced as `nonlocal`, then it _can't_ be defined in + // the module scope (because `nonlocal` can only be used in a nested scope). + for scope_id in scope + .get_all(name) + .filter_map(|binding_id| semantic.rebinding_scopes(binding_id)) + .flatten() + .dedup() + .copied() + { + let scope = &semantic.scopes[scope_id]; + edits.extend(Renamer::rename_in_scope(name, target, scope, semantic)); + } + + // Deduplicate any edits. + edits.sort(); + edits.dedup(); + + let edit = edits + .pop() + .ok_or(anyhow!("Unable to rename any references to `{name}`"))?; + + Ok((edit, edits)) + } + + /// Rename a symbol in a single [`Scope`]. + fn rename_in_scope( + name: &str, + target: &str, + scope: &Scope, + semantic: &SemanticModel, + ) -> Vec { + let mut edits = vec![]; + + // Iterate over every binding to the name in the scope. + for binding_id in scope.get_all(name) { + let binding = semantic.binding(binding_id); + + // Rename the binding. + if let Some(edit) = Renamer::rename_binding(binding, name, target) { + edits.push(edit); + + // Rename any delayed annotations. + if let Some(annotations) = semantic.delayed_annotations(binding_id) { + edits.extend(annotations.iter().filter_map(|annotation_id| { + let annotation = semantic.binding(*annotation_id); + Renamer::rename_binding(annotation, name, target) + })); + } + + // Rename the references to the binding. + edits.extend(binding.references().map(|reference_id| { + let reference = semantic.reference(reference_id); + Edit::range_replacement(target.to_string(), reference.range()) + })); + } + } + + // Deduplicate any edits. In some cases, a reference can be both a read _and_ a write. For + // example, `x += 1` is both a read of and a write to `x`. + edits.sort(); + edits.dedup(); + + edits + } + + /// Rename a [`Binding`] reference. + fn rename_binding(binding: &Binding, name: &str, target: &str) -> Option { + match &binding.kind { + BindingKind::Import(_) | BindingKind::FromImport(_) => { + if binding.is_alias() { + // Ex) Rename `import pandas as alias` to `import pandas as pd`. + Some(Edit::range_replacement(target.to_string(), binding.range)) + } else { + // Ex) Rename `import pandas` to `import pandas as pd`. + Some(Edit::range_replacement( + format!("{name} as {target}"), + binding.range, + )) + } + } + BindingKind::SubmoduleImport(import) => { + // Ex) Rename `import pandas.core` to `import pandas as pd`. + let module_name = import.qualified_name.split('.').next().unwrap(); + Some(Edit::range_replacement( + format!("{module_name} as {target}"), + binding.range, + )) + } + // Avoid renaming builtins and other "special" bindings. + BindingKind::FutureImport | BindingKind::Builtin | BindingKind::Export(_) => None, + // By default, replace the binding's name with the target name. + BindingKind::Annotation + | BindingKind::Argument + | BindingKind::NamedExprAssignment + | BindingKind::UnpackedAssignment + | BindingKind::Assignment + | BindingKind::LoopVar + | BindingKind::Global + | BindingKind::Nonlocal(_) + | BindingKind::ClassDefinition(_) + | BindingKind::FunctionDefinition(_) + | BindingKind::Deletion + | BindingKind::UnboundException(_) => { + Some(Edit::range_replacement(target.to_string(), binding.range)) + } + } + } +} diff --git a/crates/ruff/src/rule_selector.rs b/crates/ruff/src/rule_selector.rs index 6985c1be3c..5eb5f1461b 100644 --- a/crates/ruff/src/rule_selector.rs +++ b/crates/ruff/src/rule_selector.rs @@ -145,8 +145,8 @@ impl From for RuleSelector { } impl IntoIterator for &RuleSelector { - type Item = Rule; type IntoIter = RuleSelectorIter; + type Item = Rule; fn into_iter(self) -> Self::IntoIter { match self { @@ -158,16 +158,16 @@ impl IntoIterator for &RuleSelector { } RuleSelector::C => RuleSelectorIter::Chain( Linter::Flake8Comprehensions - .into_iter() - .chain(Linter::McCabe.into_iter()), + .rules() + .chain(Linter::McCabe.rules()), ), RuleSelector::T => RuleSelectorIter::Chain( Linter::Flake8Debugger - .into_iter() - .chain(Linter::Flake8Print.into_iter()), + .rules() + .chain(Linter::Flake8Print.rules()), ), - RuleSelector::Linter(linter) => RuleSelectorIter::Vec(linter.into_iter()), - RuleSelector::Prefix { prefix, .. } => RuleSelectorIter::Vec(prefix.into_iter()), + RuleSelector::Linter(linter) => RuleSelectorIter::Vec(linter.rules()), + RuleSelector::Prefix { prefix, .. } => RuleSelectorIter::Vec(prefix.clone().rules()), } } } @@ -346,7 +346,7 @@ mod clap_completion { let prefix = p.linter().common_prefix(); let code = p.short_code(); - let mut rules_iter = p.into_iter(); + let mut rules_iter = p.rules(); let rule1 = rules_iter.next(); let rule2 = rules_iter.next(); diff --git a/crates/ruff/src/rules/airflow/rules/mod.rs b/crates/ruff/src/rules/airflow/rules/mod.rs index 0dbd1cf914..f9917250a8 100644 --- a/crates/ruff/src/rules/airflow/rules/mod.rs +++ b/crates/ruff/src/rules/airflow/rules/mod.rs @@ -1,3 +1,3 @@ -mod task_variable_name; +pub(crate) use task_variable_name::*; -pub(crate) use task_variable_name::{variable_name_task_id, AirflowVariableNameTaskIdMismatch}; +mod task_variable_name; diff --git a/crates/ruff/src/rules/airflow/rules/task_variable_name.rs b/crates/ruff/src/rules/airflow/rules/task_variable_name.rs index eae2490138..8e9930e564 100644 --- a/crates/ruff/src/rules/airflow/rules/task_variable_name.rs +++ b/crates/ruff/src/rules/airflow/rules/task_variable_name.rs @@ -3,7 +3,7 @@ use rustpython_parser::ast::{Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::prelude::Constant; +use rustpython_parser::ast::Constant; use crate::checkers::ast::Checker; @@ -67,7 +67,7 @@ pub(crate) fn variable_name_task_id( // If the function doesn't come from Airflow, we can't do anything. if !checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| matches!(call_path[0], "airflow")) { diff --git a/crates/ruff/src/rules/eradicate/rules/commented_out_code.rs b/crates/ruff/src/rules/eradicate/rules/commented_out_code.rs index 3d1a5d2290..15e23ab6f1 100644 --- a/crates/ruff/src/rules/eradicate/rules/commented_out_code.rs +++ b/crates/ruff/src/rules/eradicate/rules/commented_out_code.rs @@ -14,13 +14,13 @@ use super::super::detection::comment_contains_code; /// Commented-out code is dead code, and is often included inadvertently. /// It should be removed. /// -/// ## Options -/// - `task-tags` -/// /// ## Example /// ```python /// # print('foo') /// ``` +/// +/// ## Options +/// - `task-tags` #[violation] pub struct CommentedOutCode; @@ -48,12 +48,11 @@ fn is_standalone_comment(line: &str) -> bool { /// ERA001 pub(crate) fn commented_out_code( - indexer: &Indexer, + diagnostics: &mut Vec, locator: &Locator, + indexer: &Indexer, settings: &Settings, -) -> Vec { - let mut diagnostics = vec![]; - +) { for range in indexer.comment_ranges() { let line = locator.full_lines(*range); @@ -62,14 +61,11 @@ pub(crate) fn commented_out_code( let mut diagnostic = Diagnostic::new(CommentedOutCode, *range); if settings.rules.should_fix(Rule::CommentedOutCode) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_deletion( + diagnostic.set_fix(Fix::manual(Edit::range_deletion( locator.full_lines_range(*range), ))); } diagnostics.push(diagnostic); } } - - diagnostics } diff --git a/crates/ruff/src/rules/eradicate/rules/mod.rs b/crates/ruff/src/rules/eradicate/rules/mod.rs index 8ec37813d9..b4c263a058 100644 --- a/crates/ruff/src/rules/eradicate/rules/mod.rs +++ b/crates/ruff/src/rules/eradicate/rules/mod.rs @@ -1,3 +1,3 @@ -pub(crate) use commented_out_code::{commented_out_code, CommentedOutCode}; +pub(crate) use commented_out_code::*; mod commented_out_code; diff --git a/crates/ruff/src/rules/eradicate/snapshots/ruff__rules__eradicate__tests__ERA001_ERA001.py.snap b/crates/ruff/src/rules/eradicate/snapshots/ruff__rules__eradicate__tests__ERA001_ERA001.py.snap index 9aff20a52b..17ccde3ca7 100644 --- a/crates/ruff/src/rules/eradicate/snapshots/ruff__rules__eradicate__tests__ERA001_ERA001.py.snap +++ b/crates/ruff/src/rules/eradicate/snapshots/ruff__rules__eradicate__tests__ERA001_ERA001.py.snap @@ -10,7 +10,7 @@ ERA001.py:1:1: ERA001 [*] Found commented-out code | = help: Remove commented-out code -ℹ Suggested fix +ℹ Possible fix 1 |-#import os 2 1 | # from foo import junk 3 2 | #a = 3 @@ -26,7 +26,7 @@ ERA001.py:2:1: ERA001 [*] Found commented-out code | = help: Remove commented-out code -ℹ Suggested fix +ℹ Possible fix 1 1 | #import os 2 |-# from foo import junk 3 2 | #a = 3 @@ -44,7 +44,7 @@ ERA001.py:3:1: ERA001 [*] Found commented-out code | = help: Remove commented-out code -ℹ Suggested fix +ℹ Possible fix 1 1 | #import os 2 2 | # from foo import junk 3 |-#a = 3 @@ -63,7 +63,7 @@ ERA001.py:5:1: ERA001 [*] Found commented-out code | = help: Remove commented-out code -ℹ Suggested fix +ℹ Possible fix 2 2 | # from foo import junk 3 3 | #a = 3 4 4 | a = 4 @@ -82,7 +82,7 @@ ERA001.py:13:5: ERA001 [*] Found commented-out code | = help: Remove commented-out code -ℹ Suggested fix +ℹ Possible fix 10 10 | 11 11 | # This is a real comment. 12 12 | # # This is a (nested) comment. @@ -100,7 +100,7 @@ ERA001.py:21:5: ERA001 [*] Found commented-out code | = help: Remove commented-out code -ℹ Suggested fix +ℹ Possible fix 18 18 | 19 19 | class A(): 20 20 | pass diff --git a/crates/ruff/src/rules/flake8_2020/helpers.rs b/crates/ruff/src/rules/flake8_2020/helpers.rs index e5bb8fd4b6..3f86f9bfd9 100644 --- a/crates/ruff/src/rules/flake8_2020/helpers.rs +++ b/crates/ruff/src/rules/flake8_2020/helpers.rs @@ -1,8 +1,9 @@ -use ruff_python_semantic::model::SemanticModel; use rustpython_parser::ast::Expr; -pub(super) fn is_sys(model: &SemanticModel, expr: &Expr, target: &str) -> bool { - model +use ruff_python_semantic::SemanticModel; + +pub(super) fn is_sys(expr: &Expr, target: &str, semantic: &SemanticModel) -> bool { + semantic .resolve_call_path(expr) .map_or(false, |call_path| call_path.as_slice() == ["sys", target]) } diff --git a/crates/ruff/src/rules/flake8_2020/rules/compare.rs b/crates/ruff/src/rules/flake8_2020/rules/compare.rs index 9877be4fae..55197df851 100644 --- a/crates/ruff/src/rules/flake8_2020/rules/compare.rs +++ b/crates/ruff/src/rules/flake8_2020/rules/compare.rs @@ -1,5 +1,5 @@ use num_bigint::BigInt; -use rustpython_parser::ast::{self, Cmpop, Constant, Expr, Ranged}; +use rustpython_parser::ast::{self, CmpOp, Constant, Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -9,6 +9,38 @@ use crate::registry::Rule; use super::super::helpers::is_sys; +/// ## What it does +/// Checks for comparisons that test `sys.version` against string literals, +/// such that the comparison will evaluate to `False` on Python 3.10 or later. +/// +/// ## Why is this bad? +/// Comparing `sys.version` to a string is error-prone and may cause subtle +/// bugs, as the comparison will be performed lexicographically, not +/// semantically. For example, `sys.version > "3.9"` will evaluate to `False` +/// when using Python 3.10, as `"3.10"` is lexicographically "less" than +/// `"3.9"`. +/// +/// Instead, use `sys.version_info` to access the current major and minor +/// version numbers as a tuple, which can be compared to other tuples +/// without issue. +/// +/// ## Example +/// ```python +/// import sys +/// +/// sys.version > "3.9" # `False` on Python 3.10. +/// ``` +/// +/// Use instead: +/// ```python +/// import sys +/// +/// sys.version_info > (3, 9) # `True` on Python 3.10. +/// ``` +/// +/// ## References +/// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version) +/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info) #[violation] pub struct SysVersionCmpStr3; @@ -19,6 +51,43 @@ impl Violation for SysVersionCmpStr3 { } } +/// ## What it does +/// Checks for equality comparisons against the major version returned by +/// `sys.version_info` (e.g., `sys.version_info[0] == 3`). +/// +/// ## Why is this bad? +/// Using `sys.version_info[0] == 3` to verify that the major version is +/// Python 3 or greater will fail if the major version number is ever +/// incremented (e.g., to Python 4). This is likely unintended, as code +/// that uses this comparison is likely intended to be run on Python 2, +/// but would now run on Python 4 too. +/// +/// Instead, use `>=` to check if the major version number is 3 or greater, +/// to future-proof the code. +/// +/// ## Example +/// ```python +/// import sys +/// +/// if sys.version_info[0] == 3: +/// ... +/// else: +/// print("Python 2") # This will be printed on Python 4. +/// ``` +/// +/// Use instead: +/// ```python +/// import sys +/// +/// if sys.version_info >= (3,): +/// ... +/// else: +/// print("Python 2") # This will not be printed on Python 4. +/// ``` +/// +/// ## References +/// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version) +/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info) #[violation] pub struct SysVersionInfo0Eq3; @@ -29,6 +98,36 @@ impl Violation for SysVersionInfo0Eq3 { } } +/// ## What it does +/// Checks for comparisons that test `sys.version_info[1]` against an integer. +/// +/// ## Why is this bad? +/// Comparisons based on the current minor version number alone can cause +/// subtle bugs and would likely lead to unintended effects if the Python +/// major version number were ever incremented (e.g., to Python 4). +/// +/// Instead, compare `sys.version_info` to a tuple, including the major and +/// minor version numbers, to future-proof the code. +/// +/// ## Example +/// ```python +/// import sys +/// +/// if sys.version_info[1] < 7: +/// print("Python 3.6 or earlier.") # This will be printed on Python 4.0. +/// ``` +/// +/// Use instead: +/// ```python +/// import sys +/// +/// if sys.version_info < (3, 7): +/// print("Python 3.6 or earlier.") +/// ``` +/// +/// ## References +/// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version) +/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info) #[violation] pub struct SysVersionInfo1CmpInt; @@ -42,6 +141,36 @@ impl Violation for SysVersionInfo1CmpInt { } } +/// ## What it does +/// Checks for comparisons that test `sys.version_info.minor` against an integer. +/// +/// ## Why is this bad? +/// Comparisons based on the current minor version number alone can cause +/// subtle bugs and would likely lead to unintended effects if the Python +/// major version number were ever incremented (e.g., to Python 4). +/// +/// Instead, compare `sys.version_info` to a tuple, including the major and +/// minor version numbers, to future-proof the code. +/// +/// ## Example +/// ```python +/// import sys +/// +/// if sys.version_info.minor < 7: +/// print("Python 3.6 or earlier.") # This will be printed on Python 4.0. +/// ``` +/// +/// Use instead: +/// ```python +/// import sys +/// +/// if sys.version_info < (3, 7): +/// print("Python 3.6 or earlier.") +/// ``` +/// +/// ## References +/// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version) +/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info) #[violation] pub struct SysVersionInfoMinorCmpInt; @@ -55,6 +184,37 @@ impl Violation for SysVersionInfoMinorCmpInt { } } +/// ## What it does +/// Checks for comparisons that test `sys.version` against string literals, +/// such that the comparison would fail if the major version number were +/// ever incremented to Python 10 or higher. +/// +/// ## Why is this bad? +/// Comparing `sys.version` to a string is error-prone and may cause subtle +/// bugs, as the comparison will be performed lexicographically, not +/// semantically. +/// +/// Instead, use `sys.version_info` to access the current major and minor +/// version numbers as a tuple, which can be compared to other tuples +/// without issue. +/// +/// ## Example +/// ```python +/// import sys +/// +/// sys.version >= "3" # `False` on Python 10. +/// ``` +/// +/// Use instead: +/// ```python +/// import sys +/// +/// sys.version_info >= (3,) # `True` on Python 10. +/// ``` +/// +/// ## References +/// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version) +/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info) #[violation] pub struct SysVersionCmpStr10; @@ -66,10 +226,10 @@ impl Violation for SysVersionCmpStr10 { } /// YTT103, YTT201, YTT203, YTT204, YTT302 -pub(crate) fn compare(checker: &mut Checker, left: &Expr, ops: &[Cmpop], comparators: &[Expr]) { +pub(crate) fn compare(checker: &mut Checker, left: &Expr, ops: &[CmpOp], comparators: &[Expr]) { match left { Expr::Subscript(ast::ExprSubscript { value, slice, .. }) - if is_sys(checker.semantic_model(), value, "version_info") => + if is_sys(value, "version_info", checker.semantic()) => { if let Expr::Constant(ast::ExprConstant { value: Constant::Int(i), @@ -78,7 +238,7 @@ pub(crate) fn compare(checker: &mut Checker, left: &Expr, ops: &[Cmpop], compara { if *i == BigInt::from(0) { if let ( - [Cmpop::Eq | Cmpop::NotEq], + [CmpOp::Eq | CmpOp::NotEq], [Expr::Constant(ast::ExprConstant { value: Constant::Int(n), .. @@ -93,7 +253,7 @@ pub(crate) fn compare(checker: &mut Checker, left: &Expr, ops: &[Cmpop], compara } } else if *i == BigInt::from(1) { if let ( - [Cmpop::Lt | Cmpop::LtE | Cmpop::Gt | Cmpop::GtE], + [CmpOp::Lt | CmpOp::LtE | CmpOp::Gt | CmpOp::GtE], [Expr::Constant(ast::ExprConstant { value: Constant::Int(_), .. @@ -111,10 +271,10 @@ pub(crate) fn compare(checker: &mut Checker, left: &Expr, ops: &[Cmpop], compara } Expr::Attribute(ast::ExprAttribute { value, attr, .. }) - if is_sys(checker.semantic_model(), value, "version_info") && attr == "minor" => + if is_sys(value, "version_info", checker.semantic()) && attr == "minor" => { if let ( - [Cmpop::Lt | Cmpop::LtE | Cmpop::Gt | Cmpop::GtE], + [CmpOp::Lt | CmpOp::LtE | CmpOp::Gt | CmpOp::GtE], [Expr::Constant(ast::ExprConstant { value: Constant::Int(_), .. @@ -132,9 +292,9 @@ pub(crate) fn compare(checker: &mut Checker, left: &Expr, ops: &[Cmpop], compara _ => {} } - if is_sys(checker.semantic_model(), left, "version") { + if is_sys(left, "version", checker.semantic()) { if let ( - [Cmpop::Lt | Cmpop::LtE | Cmpop::Gt | Cmpop::GtE], + [CmpOp::Lt | CmpOp::LtE | CmpOp::Gt | CmpOp::GtE], [Expr::Constant(ast::ExprConstant { value: Constant::Str(s), .. diff --git a/crates/ruff/src/rules/flake8_2020/rules/mod.rs b/crates/ruff/src/rules/flake8_2020/rules/mod.rs index cb77bcc0dd..9f662e64e3 100644 --- a/crates/ruff/src/rules/flake8_2020/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_2020/rules/mod.rs @@ -1,11 +1,6 @@ -pub(crate) use compare::{ - compare, SysVersionCmpStr10, SysVersionCmpStr3, SysVersionInfo0Eq3, SysVersionInfo1CmpInt, - SysVersionInfoMinorCmpInt, -}; -pub(crate) use name_or_attribute::{name_or_attribute, SixPY3}; -pub(crate) use subscript::{ - subscript, SysVersion0, SysVersion2, SysVersionSlice1, SysVersionSlice3, -}; +pub(crate) use compare::*; +pub(crate) use name_or_attribute::*; +pub(crate) use subscript::*; mod compare; mod name_or_attribute; diff --git a/crates/ruff/src/rules/flake8_2020/rules/name_or_attribute.rs b/crates/ruff/src/rules/flake8_2020/rules/name_or_attribute.rs index d861abd262..47bdd31421 100644 --- a/crates/ruff/src/rules/flake8_2020/rules/name_or_attribute.rs +++ b/crates/ruff/src/rules/flake8_2020/rules/name_or_attribute.rs @@ -5,6 +5,35 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for uses of `six.PY3`. +/// +/// ## Why is this bad? +/// `six.PY3` will evaluate to `False` on Python 4 and greater. This is likely +/// unintended, and may cause code intended to run on Python 2 to run on Python 4 +/// too. +/// +/// Instead, use `not six.PY2` to validate that the current Python major version is +/// _not_ equal to 2, to future-proof the code. +/// +/// ## Example +/// ```python +/// import six +/// +/// six.PY3 # `False` on Python 4. +/// ``` +/// +/// Use instead: +/// ```python +/// import six +/// +/// not six.PY2 # `True` on Python 4. +/// ``` +/// +/// ## References +/// - [PyPI: `six`](https://pypi.org/project/six/) +/// - [Six documentation: `six.PY2`](https://six.readthedocs.io/#six.PY2) +/// - [Six documentation: `six.PY3`](https://six.readthedocs.io/#six.PY3) #[violation] pub struct SixPY3; @@ -18,9 +47,11 @@ impl Violation for SixPY3 { /// YTT202 pub(crate) fn name_or_attribute(checker: &mut Checker, expr: &Expr) { if checker - .semantic_model() + .semantic() .resolve_call_path(expr) - .map_or(false, |call_path| call_path.as_slice() == ["six", "PY3"]) + .map_or(false, |call_path| { + matches!(call_path.as_slice(), ["six", "PY3"]) + }) { checker .diagnostics diff --git a/crates/ruff/src/rules/flake8_2020/rules/subscript.rs b/crates/ruff/src/rules/flake8_2020/rules/subscript.rs index b55a602423..0f18cfae68 100644 --- a/crates/ruff/src/rules/flake8_2020/rules/subscript.rs +++ b/crates/ruff/src/rules/flake8_2020/rules/subscript.rs @@ -8,6 +8,36 @@ use crate::checkers::ast::Checker; use crate::registry::Rule; use crate::rules::flake8_2020::helpers::is_sys; +/// ## What it does +/// Checks for uses of `sys.version[:3]`. +/// +/// ## Why is this bad? +/// If the current major or minor version consists of multiple digits, +/// `sys.version[:3]` will truncate the version number (e.g., `"3.10"` would +/// become `"3.1"`). This is likely unintended, and can lead to subtle bugs if +/// the version string is used to test against a specific Python version. +/// +/// Instead, use `sys.version_info` to access the current major and minor +/// version numbers as a tuple, which can be compared to other tuples +/// without issue. +/// +/// ## Example +/// ```python +/// import sys +/// +/// sys.version[:3] # Evaluates to "3.1" on Python 3.10. +/// ``` +/// +/// Use instead: +/// ```python +/// import sys +/// +/// sys.version_info[:2] # Evaluates to (3, 10) on Python 3.10. +/// ``` +/// +/// ## References +/// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version) +/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info) #[violation] pub struct SysVersionSlice3; @@ -18,6 +48,36 @@ impl Violation for SysVersionSlice3 { } } +/// ## What it does +/// Checks for uses of `sys.version[2]`. +/// +/// ## Why is this bad? +/// If the current major or minor version consists of multiple digits, +/// `sys.version[2]` will select the first digit of the minor number only +/// (e.g., `"3.10"` would evaluate to `"1"`). This is likely unintended, and +/// can lead to subtle bugs if the version is used to test against a minor +/// version number. +/// +/// Instead, use `sys.version_info.minor` to access the current minor version +/// number. +/// +/// ## Example +/// ```python +/// import sys +/// +/// sys.version[2] # Evaluates to "1" on Python 3.10. +/// ``` +/// +/// Use instead: +/// ```python +/// import sys +/// +/// f"{sys.version_info.minor}" # Evaluates to "10" on Python 3.10. +/// ``` +/// +/// ## References +/// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version) +/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info) #[violation] pub struct SysVersion2; @@ -28,6 +88,36 @@ impl Violation for SysVersion2 { } } +/// ## What it does +/// Checks for uses of `sys.version[0]`. +/// +/// ## Why is this bad? +/// If the current major or minor version consists of multiple digits, +/// `sys.version[0]` will select the first digit of the major version number +/// only (e.g., `"3.10"` would evaluate to `"1"`). This is likely unintended, +/// and can lead to subtle bugs if the version string is used to test against a +/// major version number. +/// +/// Instead, use `sys.version_info.major` to access the current major version +/// number. +/// +/// ## Example +/// ```python +/// import sys +/// +/// sys.version[0] # If using Python 10, this evaluates to "1". +/// ``` +/// +/// Use instead: +/// ```python +/// import sys +/// +/// f"{sys.version_info.major}" # If using Python 10, this evaluates to "10". +/// ``` +/// +/// ## References +/// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version) +/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info) #[violation] pub struct SysVersion0; @@ -38,6 +128,36 @@ impl Violation for SysVersion0 { } } +/// ## What it does +/// Checks for uses of `sys.version[:1]`. +/// +/// ## Why is this bad? +/// If the major version number consists of more than one digit, this will +/// select the first digit of the major version number only (e.g., `"10.0"` +/// would evaluate to `"1"`). This is likely unintended, and can lead to subtle +/// bugs in future versions of Python if the version string is used to test +/// against a specific major version number. +/// +/// Instead, use `sys.version_info.major` to access the current major version +/// number. +/// +/// ## Example +/// ```python +/// import sys +/// +/// sys.version[:1] # If using Python 10, this evaluates to "1". +/// ``` +/// +/// Use instead: +/// ```python +/// import sys +/// +/// f"{sys.version_info.major}" # If using Python 10, this evaluates to "10". +/// ``` +/// +/// ## References +/// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version) +/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info) #[violation] pub struct SysVersionSlice1; @@ -50,7 +170,7 @@ impl Violation for SysVersionSlice1 { /// YTT101, YTT102, YTT301, YTT303 pub(crate) fn subscript(checker: &mut Checker, value: &Expr, slice: &Expr) { - if is_sys(checker.semantic_model(), value, "version") { + if is_sys(value, "version", checker.semantic()) { match slice { Expr::Slice(ast::ExprSlice { lower: None, diff --git a/crates/ruff/src/rules/flake8_annotations/helpers.rs b/crates/ruff/src/rules/flake8_annotations/helpers.rs index 82732c5819..c3db27fa2f 100644 --- a/crates/ruff/src/rules/flake8_annotations/helpers.rs +++ b/crates/ruff/src/rules/flake8_annotations/helpers.rs @@ -2,8 +2,7 @@ use rustpython_parser::ast::{self, Arguments, Expr, Stmt}; use ruff_python_ast::cast; use ruff_python_semantic::analyze::visibility; -use ruff_python_semantic::definition::{Definition, Member, MemberKind}; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::{Definition, Member, MemberKind, SemanticModel}; pub(super) fn match_function_def( stmt: &Stmt, @@ -36,14 +35,14 @@ pub(super) fn match_function_def( } /// Return the name of the function, if it's overloaded. -pub(crate) fn overloaded_name(model: &SemanticModel, definition: &Definition) -> Option { +pub(crate) fn overloaded_name(definition: &Definition, semantic: &SemanticModel) -> Option { if let Definition::Member(Member { kind: MemberKind::Function | MemberKind::NestedFunction | MemberKind::Method, stmt, .. }) = definition { - if visibility::is_overload(model, cast::decorator_list(stmt)) { + if visibility::is_overload(cast::decorator_list(stmt), semantic) { let (name, ..) = match_function_def(stmt); Some(name.to_string()) } else { @@ -57,9 +56,9 @@ pub(crate) fn overloaded_name(model: &SemanticModel, definition: &Definition) -> /// Return `true` if the definition is the implementation for an overloaded /// function. pub(crate) fn is_overload_impl( - model: &SemanticModel, definition: &Definition, overloaded_name: &str, + semantic: &SemanticModel, ) -> bool { if let Definition::Member(Member { kind: MemberKind::Function | MemberKind::NestedFunction | MemberKind::Method, @@ -67,7 +66,7 @@ pub(crate) fn is_overload_impl( .. }) = definition { - if visibility::is_overload(model, cast::decorator_list(stmt)) { + if visibility::is_overload(cast::decorator_list(stmt), semantic) { false } else { let (name, ..) = match_function_def(stmt); diff --git a/crates/ruff/src/rules/flake8_annotations/rules/definition.rs b/crates/ruff/src/rules/flake8_annotations/rules/definition.rs index 492be79613..f672afd9b7 100644 --- a/crates/ruff/src/rules/flake8_annotations/rules/definition.rs +++ b/crates/ruff/src/rules/flake8_annotations/rules/definition.rs @@ -1,18 +1,19 @@ -use rustpython_parser::ast::{Expr, Ranged, Stmt}; +use rustpython_parser::ast::{self, ArgWithDefault, Constant, Expr, Ranged, Stmt}; -use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Violation}; +use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::cast; use ruff_python_ast::helpers::ReturnStatementVisitor; +use ruff_python_ast::identifier::Identifier; use ruff_python_ast::statement_visitor::StatementVisitor; -use ruff_python_ast::{cast, helpers}; +use ruff_python_ast::typing::parse_type_annotation; use ruff_python_semantic::analyze::visibility; -use ruff_python_semantic::analyze::visibility::Visibility; -use ruff_python_semantic::definition::{Definition, Member, MemberKind}; -use ruff_python_semantic::model::SemanticModel; -use ruff_python_stdlib::typing::SIMPLE_MAGIC_RETURN_TYPES; +use ruff_python_semantic::{Definition, Member, MemberKind}; +use ruff_python_stdlib::typing::simple_magic_return_type; use crate::checkers::ast::Checker; use crate::registry::{AsRule, Rule}; +use crate::rules::ruff::typing::type_hint_resolves_to_any; use super::super::fixes; use super::super::helpers::match_function_def; @@ -433,36 +434,58 @@ fn is_none_returning(body: &[Stmt]) -> bool { /// ANN401 fn check_dynamically_typed( - model: &SemanticModel, + checker: &Checker, annotation: &Expr, func: F, diagnostics: &mut Vec, - is_overridden: bool, ) where F: FnOnce() -> String, { - if !is_overridden && model.match_typing_expr(annotation, "Any") { - diagnostics.push(Diagnostic::new( - AnyType { name: func() }, - annotation.range(), - )); - }; + if let Expr::Constant(ast::ExprConstant { + range, + value: Constant::Str(string), + .. + }) = annotation + { + // Quoted annotations + if let Ok((parsed_annotation, _)) = parse_type_annotation(string, *range, checker.locator) { + if type_hint_resolves_to_any( + &parsed_annotation, + checker.semantic(), + checker.locator, + checker.settings.target_version.minor(), + ) { + diagnostics.push(Diagnostic::new( + AnyType { name: func() }, + annotation.range(), + )); + } + } + } else { + if type_hint_resolves_to_any( + annotation, + checker.semantic(), + checker.locator, + checker.settings.target_version.minor(), + ) { + diagnostics.push(Diagnostic::new( + AnyType { name: func() }, + annotation.range(), + )); + } + } } /// Generate flake8-annotation checks for a given `Definition`. pub(crate) fn definition( checker: &Checker, definition: &Definition, - visibility: Visibility, + visibility: visibility::Visibility, ) -> Vec { // TODO(charlie): Consider using the AST directly here rather than `Definition`. // We could adhere more closely to `flake8-annotations` by defining public // vs. secret vs. protected. - let Definition::Member(Member { - kind, - stmt, - .. - }) = definition else { + let Definition::Member(Member { kind, stmt, .. }) = definition else { return vec![]; }; @@ -472,7 +495,7 @@ pub(crate) fn definition( _ => return vec![], }; - let (name, args, returns, body, decorator_list) = match_function_def(stmt); + let (name, arguments, returns, body, decorator_list) = match_function_def(stmt); // Keep track of whether we've seen any typed arguments or return values. let mut has_any_typed_arg = false; // Any argument has been typed? let mut has_typed_return = false; // Return value has been typed? @@ -482,47 +505,47 @@ pub(crate) fn definition( // unless configured to suppress ANN* for declarations that are fully untyped. let mut diagnostics = Vec::new(); - let is_overridden = visibility::is_override(checker.semantic_model(), decorator_list); + let is_overridden = visibility::is_override(decorator_list, checker.semantic()); // ANN001, ANN401 - for arg in args + for ArgWithDefault { + def, + default: _, + range: _, + } in arguments .posonlyargs .iter() - .chain(args.args.iter()) - .chain(args.kwonlyargs.iter()) + .chain(&arguments.args) + .chain(&arguments.kwonlyargs) .skip( // If this is a non-static method, skip `cls` or `self`. usize::from( is_method - && !visibility::is_staticmethod( - checker.semantic_model(), - cast::decorator_list(stmt), - ), + && !visibility::is_staticmethod(cast::decorator_list(stmt), checker.semantic()), ), ) { // ANN401 for dynamically typed arguments - if let Some(annotation) = &arg.annotation { + if let Some(annotation) = &def.annotation { has_any_typed_arg = true; - if checker.enabled(Rule::AnyType) { + if checker.enabled(Rule::AnyType) && !is_overridden { check_dynamically_typed( - checker.semantic_model(), + checker, annotation, - || arg.arg.to_string(), + || def.arg.to_string(), &mut diagnostics, - is_overridden, ); } } else { if !(checker.settings.flake8_annotations.suppress_dummy_args - && checker.settings.dummy_variable_rgx.is_match(&arg.arg)) + && checker.settings.dummy_variable_rgx.is_match(&def.arg)) { if checker.enabled(Rule::MissingTypeFunctionArgument) { diagnostics.push(Diagnostic::new( MissingTypeFunctionArgument { - name: arg.arg.to_string(), + name: def.arg.to_string(), }, - arg.range(), + def.range(), )); } } @@ -530,19 +553,13 @@ pub(crate) fn definition( } // ANN002, ANN401 - if let Some(arg) = &args.vararg { + if let Some(arg) = &arguments.vararg { if let Some(expr) = &arg.annotation { has_any_typed_arg = true; if !checker.settings.flake8_annotations.allow_star_arg_any { - if checker.enabled(Rule::AnyType) { + if checker.enabled(Rule::AnyType) && !is_overridden { let name = &arg.arg; - check_dynamically_typed( - checker.semantic_model(), - expr, - || format!("*{name}"), - &mut diagnostics, - is_overridden, - ); + check_dynamically_typed(checker, expr, || format!("*{name}"), &mut diagnostics); } } } else { @@ -562,18 +579,17 @@ pub(crate) fn definition( } // ANN003, ANN401 - if let Some(arg) = &args.kwarg { + if let Some(arg) = &arguments.kwarg { if let Some(expr) = &arg.annotation { has_any_typed_arg = true; if !checker.settings.flake8_annotations.allow_star_arg_any { - if checker.enabled(Rule::AnyType) { + if checker.enabled(Rule::AnyType) && !is_overridden { let name = &arg.arg; check_dynamically_typed( - checker.semantic_model(), + checker, expr, || format!("**{name}"), &mut diagnostics, - is_overridden, ); } } @@ -594,28 +610,33 @@ pub(crate) fn definition( } // ANN101, ANN102 - if is_method - && !visibility::is_staticmethod(checker.semantic_model(), cast::decorator_list(stmt)) - { - if let Some(arg) = args.posonlyargs.first().or_else(|| args.args.first()) { - if arg.annotation.is_none() { - if visibility::is_classmethod(checker.semantic_model(), cast::decorator_list(stmt)) - { + if is_method && !visibility::is_staticmethod(cast::decorator_list(stmt), checker.semantic()) { + if let Some(ArgWithDefault { + def, + default: _, + range: _, + }) = arguments + .posonlyargs + .first() + .or_else(|| arguments.args.first()) + { + if def.annotation.is_none() { + if visibility::is_classmethod(cast::decorator_list(stmt), checker.semantic()) { if checker.enabled(Rule::MissingTypeCls) { diagnostics.push(Diagnostic::new( MissingTypeCls { - name: arg.arg.to_string(), + name: def.arg.to_string(), }, - arg.range(), + def.range(), )); } } else { if checker.enabled(Rule::MissingTypeSelf) { diagnostics.push(Diagnostic::new( MissingTypeSelf { - name: arg.arg.to_string(), + name: def.arg.to_string(), }, - arg.range(), + def.range(), )); } } @@ -628,40 +649,32 @@ pub(crate) fn definition( // ANN201, ANN202, ANN401 if let Some(expr) = &returns { has_typed_return = true; - if checker.enabled(Rule::AnyType) { - check_dynamically_typed( - checker.semantic_model(), - expr, - || name.to_string(), - &mut diagnostics, - is_overridden, - ); + if checker.enabled(Rule::AnyType) && !is_overridden { + check_dynamically_typed(checker, expr, || name.to_string(), &mut diagnostics); } } else if !( // Allow omission of return annotation if the function only returns `None` // (explicitly or implicitly). checker.settings.flake8_annotations.suppress_none_returning && is_none_returning(body) ) { - if is_method - && visibility::is_classmethod(checker.semantic_model(), cast::decorator_list(stmt)) - { + if is_method && visibility::is_classmethod(cast::decorator_list(stmt), checker.semantic()) { if checker.enabled(Rule::MissingReturnTypeClassMethod) { diagnostics.push(Diagnostic::new( MissingReturnTypeClassMethod { name: name.to_string(), }, - helpers::identifier_range(stmt, checker.locator), + stmt.identifier(), )); } } else if is_method - && visibility::is_staticmethod(checker.semantic_model(), cast::decorator_list(stmt)) + && visibility::is_staticmethod(cast::decorator_list(stmt), checker.semantic()) { if checker.enabled(Rule::MissingReturnTypeStaticMethod) { diagnostics.push(Diagnostic::new( MissingReturnTypeStaticMethod { name: name.to_string(), }, - helpers::identifier_range(stmt, checker.locator), + stmt.identifier(), )); } } else if is_method && visibility::is_init(name) { @@ -673,12 +686,12 @@ pub(crate) fn definition( MissingReturnTypeSpecialMethod { name: name.to_string(), }, - helpers::identifier_range(stmt, checker.locator), + stmt.identifier(), ); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.try_set_fix_from_edit(|| { + diagnostic.try_set_fix(|| { fixes::add_return_annotation(checker.locator, stmt, "None") + .map(Fix::suggested) }); } diagnostics.push(diagnostic); @@ -690,14 +703,13 @@ pub(crate) fn definition( MissingReturnTypeSpecialMethod { name: name.to_string(), }, - helpers::identifier_range(stmt, checker.locator), + stmt.identifier(), ); - let return_type = SIMPLE_MAGIC_RETURN_TYPES.get(name); - if let Some(return_type) = return_type { - if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.try_set_fix_from_edit(|| { + if checker.patch(diagnostic.kind.rule()) { + if let Some(return_type) = simple_magic_return_type(name) { + diagnostic.try_set_fix(|| { fixes::add_return_annotation(checker.locator, stmt, return_type) + .map(Fix::suggested) }); } } @@ -705,23 +717,23 @@ pub(crate) fn definition( } } else { match visibility { - Visibility::Public => { + visibility::Visibility::Public => { if checker.enabled(Rule::MissingReturnTypeUndocumentedPublicFunction) { diagnostics.push(Diagnostic::new( MissingReturnTypeUndocumentedPublicFunction { name: name.to_string(), }, - helpers::identifier_range(stmt, checker.locator), + stmt.identifier(), )); } } - Visibility::Private => { + visibility::Visibility::Private => { if checker.enabled(Rule::MissingReturnTypePrivateFunction) { diagnostics.push(Diagnostic::new( MissingReturnTypePrivateFunction { name: name.to_string(), }, - helpers::identifier_range(stmt, checker.locator), + stmt.identifier(), )); } } diff --git a/crates/ruff/src/rules/flake8_annotations/rules/mod.rs b/crates/ruff/src/rules/flake8_annotations/rules/mod.rs index b57c156b18..5b28734c91 100644 --- a/crates/ruff/src/rules/flake8_annotations/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_annotations/rules/mod.rs @@ -1,8 +1,3 @@ -pub(crate) use definition::{ - definition, AnyType, MissingReturnTypeClassMethod, MissingReturnTypePrivateFunction, - MissingReturnTypeSpecialMethod, MissingReturnTypeStaticMethod, - MissingReturnTypeUndocumentedPublicFunction, MissingTypeArgs, MissingTypeCls, - MissingTypeFunctionArgument, MissingTypeKwargs, MissingTypeSelf, -}; +pub(crate) use definition::*; mod definition; diff --git a/crates/ruff/src/rules/flake8_annotations/snapshots/ruff__rules__flake8_annotations__tests__defaults.snap b/crates/ruff/src/rules/flake8_annotations/snapshots/ruff__rules__flake8_annotations__tests__defaults.snap index 0eba6a0469..7cd87414d4 100644 --- a/crates/ruff/src/rules/flake8_annotations/snapshots/ruff__rules__flake8_annotations__tests__defaults.snap +++ b/crates/ruff/src/rules/flake8_annotations/snapshots/ruff__rules__flake8_annotations__tests__defaults.snap @@ -186,4 +186,60 @@ annotation_presence.py:134:13: ANN101 Missing type annotation for `self` in meth 135 | pass | +annotation_presence.py:148:10: ANN401 Dynamically typed expressions (typing.Any) are disallowed in `a` + | +147 | # ANN401 +148 | def f(a: Any | int) -> None: ... + | ^^^^^^^^^ ANN401 +149 | def f(a: int | Any) -> None: ... +150 | def f(a: Union[str, bytes, Any]) -> None: ... + | + +annotation_presence.py:149:10: ANN401 Dynamically typed expressions (typing.Any) are disallowed in `a` + | +147 | # ANN401 +148 | def f(a: Any | int) -> None: ... +149 | def f(a: int | Any) -> None: ... + | ^^^^^^^^^ ANN401 +150 | def f(a: Union[str, bytes, Any]) -> None: ... +151 | def f(a: Optional[Any]) -> None: ... + | + +annotation_presence.py:150:10: ANN401 Dynamically typed expressions (typing.Any) are disallowed in `a` + | +148 | def f(a: Any | int) -> None: ... +149 | def f(a: int | Any) -> None: ... +150 | def f(a: Union[str, bytes, Any]) -> None: ... + | ^^^^^^^^^^^^^^^^^^^^^^ ANN401 +151 | def f(a: Optional[Any]) -> None: ... +152 | def f(a: Annotated[Any, ...]) -> None: ... + | + +annotation_presence.py:151:10: ANN401 Dynamically typed expressions (typing.Any) are disallowed in `a` + | +149 | def f(a: int | Any) -> None: ... +150 | def f(a: Union[str, bytes, Any]) -> None: ... +151 | def f(a: Optional[Any]) -> None: ... + | ^^^^^^^^^^^^^ ANN401 +152 | def f(a: Annotated[Any, ...]) -> None: ... +153 | def f(a: "Union[str, bytes, Any]") -> None: ... + | + +annotation_presence.py:152:10: ANN401 Dynamically typed expressions (typing.Any) are disallowed in `a` + | +150 | def f(a: Union[str, bytes, Any]) -> None: ... +151 | def f(a: Optional[Any]) -> None: ... +152 | def f(a: Annotated[Any, ...]) -> None: ... + | ^^^^^^^^^^^^^^^^^^^ ANN401 +153 | def f(a: "Union[str, bytes, Any]") -> None: ... + | + +annotation_presence.py:153:10: ANN401 Dynamically typed expressions (typing.Any) are disallowed in `a` + | +151 | def f(a: Optional[Any]) -> None: ... +152 | def f(a: Annotated[Any, ...]) -> None: ... +153 | def f(a: "Union[str, bytes, Any]") -> None: ... + | ^^^^^^^^^^^^^^^^^^^^^^^^ ANN401 + | + diff --git a/crates/ruff/src/rules/flake8_async/rules/blocking_http_call.rs b/crates/ruff/src/rules/flake8_async/rules/blocking_http_call.rs index e7d72f8e45..2ab4e94f4d 100644 --- a/crates/ruff/src/rules/flake8_async/rules/blocking_http_call.rs +++ b/crates/ruff/src/rules/flake8_async/rules/blocking_http_call.rs @@ -3,6 +3,7 @@ use rustpython_parser::ast::{Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::call_path::CallPath; use crate::checkers::ast::Checker; @@ -40,37 +41,35 @@ impl Violation for BlockingHttpCallInAsyncFunction { } } -const BLOCKING_HTTP_CALLS: &[&[&str]] = &[ - &["urllib", "request", "urlopen"], - &["httpx", "get"], - &["httpx", "post"], - &["httpx", "delete"], - &["httpx", "patch"], - &["httpx", "put"], - &["httpx", "head"], - &["httpx", "connect"], - &["httpx", "options"], - &["httpx", "trace"], - &["requests", "get"], - &["requests", "post"], - &["requests", "delete"], - &["requests", "patch"], - &["requests", "put"], - &["requests", "head"], - &["requests", "connect"], - &["requests", "options"], - &["requests", "trace"], -]; +fn is_blocking_http_call(call_path: &CallPath) -> bool { + matches!( + call_path.as_slice(), + ["urllib", "request", "urlopen"] + | [ + "httpx" | "requests", + "get" + | "post" + | "delete" + | "patch" + | "put" + | "head" + | "connect" + | "options" + | "trace" + ] + ) +} /// ASYNC100 pub(crate) fn blocking_http_call(checker: &mut Checker, expr: &Expr) { - if checker.semantic_model().in_async_context() { + if checker.semantic().in_async_context() { if let Expr::Call(ast::ExprCall { func, .. }) = expr { - let call_path = checker.semantic_model().resolve_call_path(func); - let is_blocking = - call_path.map_or(false, |path| BLOCKING_HTTP_CALLS.contains(&path.as_slice())); - - if is_blocking { + if checker + .semantic() + .resolve_call_path(func) + .as_ref() + .map_or(false, is_blocking_http_call) + { checker.diagnostics.push(Diagnostic::new( BlockingHttpCallInAsyncFunction, func.range(), diff --git a/crates/ruff/src/rules/flake8_async/rules/blocking_os_call.rs b/crates/ruff/src/rules/flake8_async/rules/blocking_os_call.rs index ab0316f276..c08dece6f4 100644 --- a/crates/ruff/src/rules/flake8_async/rules/blocking_os_call.rs +++ b/crates/ruff/src/rules/flake8_async/rules/blocking_os_call.rs @@ -3,6 +3,7 @@ use rustpython_parser::ast::{Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::call_path::CallPath; use crate::checkers::ast::Checker; @@ -39,31 +40,16 @@ impl Violation for BlockingOsCallInAsyncFunction { } } -const UNSAFE_OS_METHODS: &[&[&str]] = &[ - &["os", "popen"], - &["os", "posix_spawn"], - &["os", "posix_spawnp"], - &["os", "spawnl"], - &["os", "spawnle"], - &["os", "spawnlp"], - &["os", "spawnlpe"], - &["os", "spawnv"], - &["os", "spawnve"], - &["os", "spawnvp"], - &["os", "spawnvpe"], - &["os", "system"], -]; - /// ASYNC102 pub(crate) fn blocking_os_call(checker: &mut Checker, expr: &Expr) { - if checker.semantic_model().in_async_context() { + if checker.semantic().in_async_context() { if let Expr::Call(ast::ExprCall { func, .. }) = expr { - let is_unsafe_os_method = checker - .semantic_model() + if checker + .semantic() .resolve_call_path(func) - .map_or(false, |path| UNSAFE_OS_METHODS.contains(&path.as_slice())); - - if is_unsafe_os_method { + .as_ref() + .map_or(false, is_unsafe_os_method) + { checker .diagnostics .push(Diagnostic::new(BlockingOsCallInAsyncFunction, func.range())); @@ -71,3 +57,24 @@ pub(crate) fn blocking_os_call(checker: &mut Checker, expr: &Expr) { } } } + +fn is_unsafe_os_method(call_path: &CallPath) -> bool { + matches!( + call_path.as_slice(), + [ + "os", + "popen" + | "posix_spawn" + | "posix_spawnp" + | "spawnl" + | "spawnle" + | "spawnlp" + | "spawnlpe" + | "spawnv" + | "spawnve" + | "spawnvp" + | "spawnvpe" + | "system" + ] + ) +} diff --git a/crates/ruff/src/rules/flake8_async/rules/mod.rs b/crates/ruff/src/rules/flake8_async/rules/mod.rs index 0f6e8faaca..d2c2472c5b 100644 --- a/crates/ruff/src/rules/flake8_async/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_async/rules/mod.rs @@ -1,8 +1,6 @@ -pub(crate) use blocking_http_call::{blocking_http_call, BlockingHttpCallInAsyncFunction}; -pub(crate) use blocking_os_call::{blocking_os_call, BlockingOsCallInAsyncFunction}; -pub(crate) use open_sleep_or_subprocess_call::{ - open_sleep_or_subprocess_call, OpenSleepOrSubprocessInAsyncFunction, -}; +pub(crate) use blocking_http_call::*; +pub(crate) use blocking_os_call::*; +pub(crate) use open_sleep_or_subprocess_call::*; mod blocking_http_call; mod blocking_os_call; diff --git a/crates/ruff/src/rules/flake8_async/rules/open_sleep_or_subprocess_call.rs b/crates/ruff/src/rules/flake8_async/rules/open_sleep_or_subprocess_call.rs index 1a252118c7..0d1f813ec6 100644 --- a/crates/ruff/src/rules/flake8_async/rules/open_sleep_or_subprocess_call.rs +++ b/crates/ruff/src/rules/flake8_async/rules/open_sleep_or_subprocess_call.rs @@ -3,6 +3,7 @@ use rustpython_parser::ast::{Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::call_path::CallPath; use crate::checkers::ast::Checker; @@ -39,36 +40,16 @@ impl Violation for OpenSleepOrSubprocessInAsyncFunction { } } -const OPEN_SLEEP_OR_SUBPROCESS_CALL: &[&[&str]] = &[ - &["", "open"], - &["time", "sleep"], - &["subprocess", "run"], - &["subprocess", "Popen"], - // Deprecated subprocess calls: - &["subprocess", "call"], - &["subprocess", "check_call"], - &["subprocess", "check_output"], - &["subprocess", "getoutput"], - &["subprocess", "getstatusoutput"], - &["os", "wait"], - &["os", "wait3"], - &["os", "wait4"], - &["os", "waitid"], - &["os", "waitpid"], -]; - /// ASYNC101 pub(crate) fn open_sleep_or_subprocess_call(checker: &mut Checker, expr: &Expr) { - if checker.semantic_model().in_async_context() { + if checker.semantic().in_async_context() { if let Expr::Call(ast::ExprCall { func, .. }) = expr { - let is_open_sleep_or_subprocess_call = checker - .semantic_model() + if checker + .semantic() .resolve_call_path(func) - .map_or(false, |path| { - OPEN_SLEEP_OR_SUBPROCESS_CALL.contains(&path.as_slice()) - }); - - if is_open_sleep_or_subprocess_call { + .as_ref() + .map_or(false, is_open_sleep_or_subprocess_call) + { checker.diagnostics.push(Diagnostic::new( OpenSleepOrSubprocessInAsyncFunction, func.range(), @@ -77,3 +58,22 @@ pub(crate) fn open_sleep_or_subprocess_call(checker: &mut Checker, expr: &Expr) } } } + +fn is_open_sleep_or_subprocess_call(call_path: &CallPath) -> bool { + matches!( + call_path.as_slice(), + ["", "open"] + | ["time", "sleep"] + | [ + "subprocess", + "run" + | "Popen" + | "call" + | "check_call" + | "check_output" + | "getoutput" + | "getstatusoutput" + ] + | ["os", "wait" | "wait3" | "wait4" | "waitid" | "waitpid"] + ) +} diff --git a/crates/ruff/src/rules/flake8_bandit/helpers.rs b/crates/ruff/src/rules/flake8_bandit/helpers.rs index 37b0d6d745..b480934b8b 100644 --- a/crates/ruff/src/rules/flake8_bandit/helpers.rs +++ b/crates/ruff/src/rules/flake8_bandit/helpers.rs @@ -2,7 +2,7 @@ use once_cell::sync::Lazy; use regex::Regex; use rustpython_parser::ast::{self, Constant, Expr}; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; static PASSWORD_CANDIDATE_REGEX: Lazy = Lazy::new(|| { Regex::new(r"(^|_)(?i)(pas+wo?r?d|pass(phrase)?|pwd|token|secrete?)($|_)").unwrap() @@ -22,20 +22,22 @@ pub(super) fn matches_password_name(string: &str) -> bool { PASSWORD_CANDIDATE_REGEX.is_match(string) } -pub(super) fn is_untyped_exception(type_: Option<&Expr>, model: &SemanticModel) -> bool { +pub(super) fn is_untyped_exception(type_: Option<&Expr>, semantic: &SemanticModel) -> bool { type_.map_or(true, |type_| { if let Expr::Tuple(ast::ExprTuple { elts, .. }) = &type_ { elts.iter().any(|type_| { - model.resolve_call_path(type_).map_or(false, |call_path| { - call_path.as_slice() == ["", "Exception"] - || call_path.as_slice() == ["", "BaseException"] - }) + semantic + .resolve_call_path(type_) + .map_or(false, |call_path| { + matches!(call_path.as_slice(), ["", "Exception" | "BaseException"]) + }) }) } else { - model.resolve_call_path(type_).map_or(false, |call_path| { - call_path.as_slice() == ["", "Exception"] - || call_path.as_slice() == ["", "BaseException"] - }) + semantic + .resolve_call_path(type_) + .map_or(false, |call_path| { + matches!(call_path.as_slice(), ["", "Exception" | "BaseException"]) + }) } }) } diff --git a/crates/ruff/src/rules/flake8_bandit/mod.rs b/crates/ruff/src/rules/flake8_bandit/mod.rs index 4abd69f58a..87d0e449eb 100644 --- a/crates/ruff/src/rules/flake8_bandit/mod.rs +++ b/crates/ruff/src/rules/flake8_bandit/mod.rs @@ -39,6 +39,7 @@ mod tests { #[test_case(Rule::SubprocessPopenWithShellEqualsTrue, Path::new("S602.py"))] #[test_case(Rule::SubprocessWithoutShellEqualsTrue, Path::new("S603.py"))] #[test_case(Rule::SuspiciousPickleUsage, Path::new("S301.py"))] + #[test_case(Rule::SuspiciousEvalUsage, Path::new("S307.py"))] #[test_case(Rule::SuspiciousTelnetUsage, Path::new("S312.py"))] #[test_case(Rule::TryExceptContinue, Path::new("S112.py"))] #[test_case(Rule::TryExceptPass, Path::new("S110.py"))] diff --git a/crates/ruff/src/rules/flake8_bandit/rules/bad_file_permissions.rs b/crates/ruff/src/rules/flake8_bandit/rules/bad_file_permissions.rs index 7bcf81dff6..8cad152de7 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/bad_file_permissions.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/bad_file_permissions.rs @@ -1,15 +1,39 @@ use num_traits::ToPrimitive; -use once_cell::sync::Lazy; -use rustc_hash::FxHashMap; use rustpython_parser::ast::{self, Constant, Expr, Keyword, Operator, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::call_path::compose_call_path; +use ruff_python_ast::call_path::CallPath; use ruff_python_ast::helpers::SimpleCallArgs; +use ruff_python_semantic::SemanticModel; use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for files with overly permissive permissions. +/// +/// ## Why is this bad? +/// Overly permissive file permissions may allow unintended access and +/// arbitrary code execution. +/// +/// ## Example +/// ```python +/// import os +/// +/// os.chmod("/etc/secrets.txt", 0o666) # rw-rw-rw- +/// ``` +/// +/// Use instead: +/// ```python +/// import os +/// +/// os.chmod("/etc/secrets.txt", 0o600) # rw------- +/// ``` +/// +/// ## References +/// - [Python documentation: `os.chmod`](https://docs.python.org/3/library/os.html#os.chmod) +/// - [Python documentation: `stat`](https://docs.python.org/3/library/stat.html) +/// - [Common Weakness Enumeration: CWE-732](https://cwe.mitre.org/data/definitions/732.html) #[violation] pub struct BadFilePermissions { mask: u16, @@ -19,84 +43,7 @@ impl Violation for BadFilePermissions { #[derive_message_formats] fn message(&self) -> String { let BadFilePermissions { mask } = self; - format!("`os.chmod` setting a permissive mask `{mask:#o}` on file or directory",) - } -} - -const WRITE_WORLD: u16 = 0o2; -const EXECUTE_GROUP: u16 = 0o10; - -static PYSTAT_MAPPING: Lazy> = Lazy::new(|| { - FxHashMap::from_iter([ - ("stat.ST_MODE", 0o0), - ("stat.S_IFDOOR", 0o0), - ("stat.S_IFPORT", 0o0), - ("stat.ST_INO", 0o1), - ("stat.S_IXOTH", 0o1), - ("stat.UF_NODUMP", 0o1), - ("stat.ST_DEV", 0o2), - ("stat.S_IWOTH", 0o2), - ("stat.UF_IMMUTABLE", 0o2), - ("stat.ST_NLINK", 0o3), - ("stat.ST_UID", 0o4), - ("stat.S_IROTH", 0o4), - ("stat.UF_APPEND", 0o4), - ("stat.ST_GID", 0o5), - ("stat.ST_SIZE", 0o6), - ("stat.ST_ATIME", 0o7), - ("stat.S_IRWXO", 0o7), - ("stat.ST_MTIME", 0o10), - ("stat.S_IXGRP", 0o10), - ("stat.UF_OPAQUE", 0o10), - ("stat.ST_CTIME", 0o11), - ("stat.S_IWGRP", 0o20), - ("stat.UF_NOUNLINK", 0o20), - ("stat.S_IRGRP", 0o40), - ("stat.UF_COMPRESSED", 0o40), - ("stat.S_IRWXG", 0o70), - ("stat.S_IEXEC", 0o100), - ("stat.S_IXUSR", 0o100), - ("stat.S_IWRITE", 0o200), - ("stat.S_IWUSR", 0o200), - ("stat.S_IREAD", 0o400), - ("stat.S_IRUSR", 0o400), - ("stat.S_IRWXU", 0o700), - ("stat.S_ISVTX", 0o1000), - ("stat.S_ISGID", 0o2000), - ("stat.S_ENFMT", 0o2000), - ("stat.S_ISUID", 0o4000), - ]) -}); - -fn get_int_value(expr: &Expr) -> Option { - match expr { - Expr::Constant(ast::ExprConstant { - value: Constant::Int(value), - .. - }) => value.to_u16(), - Expr::Attribute(_) => { - compose_call_path(expr).and_then(|path| PYSTAT_MAPPING.get(path.as_str()).copied()) - } - Expr::BinOp(ast::ExprBinOp { - left, - op, - right, - range: _, - }) => { - if let (Some(left_value), Some(right_value)) = - (get_int_value(left), get_int_value(right)) - { - match op { - Operator::BitAnd => Some(left_value & right_value), - Operator::BitOr => Some(left_value | right_value), - Operator::BitXor => Some(left_value ^ right_value), - _ => None, - } - } else { - None - } - } - _ => None, + format!("`os.chmod` setting a permissive mask `{mask:#o}` on file or directory") } } @@ -108,13 +55,15 @@ pub(crate) fn bad_file_permissions( keywords: &[Keyword], ) { if checker - .semantic_model() + .semantic() .resolve_call_path(func) - .map_or(false, |call_path| call_path.as_slice() == ["os", "chmod"]) + .map_or(false, |call_path| { + matches!(call_path.as_slice(), ["os", "chmod"]) + }) { let call_args = SimpleCallArgs::new(args, keywords); if let Some(mode_arg) = call_args.argument("mode", 1) { - if let Some(int_value) = get_int_value(mode_arg) { + if let Some(int_value) = int_value(mode_arg, checker.semantic()) { if (int_value & WRITE_WORLD > 0) || (int_value & EXECUTE_GROUP > 0) { checker.diagnostics.push(Diagnostic::new( BadFilePermissions { mask: int_value }, @@ -125,3 +74,75 @@ pub(crate) fn bad_file_permissions( } } } + +const WRITE_WORLD: u16 = 0o2; +const EXECUTE_GROUP: u16 = 0o10; + +fn py_stat(call_path: &CallPath) -> Option { + match call_path.as_slice() { + ["stat", "ST_MODE"] => Some(0o0), + ["stat", "S_IFDOOR"] => Some(0o0), + ["stat", "S_IFPORT"] => Some(0o0), + ["stat", "ST_INO"] => Some(0o1), + ["stat", "S_IXOTH"] => Some(0o1), + ["stat", "UF_NODUMP"] => Some(0o1), + ["stat", "ST_DEV"] => Some(0o2), + ["stat", "S_IWOTH"] => Some(0o2), + ["stat", "UF_IMMUTABLE"] => Some(0o2), + ["stat", "ST_NLINK"] => Some(0o3), + ["stat", "ST_UID"] => Some(0o4), + ["stat", "S_IROTH"] => Some(0o4), + ["stat", "UF_APPEND"] => Some(0o4), + ["stat", "ST_GID"] => Some(0o5), + ["stat", "ST_SIZE"] => Some(0o6), + ["stat", "ST_ATIME"] => Some(0o7), + ["stat", "S_IRWXO"] => Some(0o7), + ["stat", "ST_MTIME"] => Some(0o10), + ["stat", "S_IXGRP"] => Some(0o10), + ["stat", "UF_OPAQUE"] => Some(0o10), + ["stat", "ST_CTIME"] => Some(0o11), + ["stat", "S_IWGRP"] => Some(0o20), + ["stat", "UF_NOUNLINK"] => Some(0o20), + ["stat", "S_IRGRP"] => Some(0o40), + ["stat", "UF_COMPRESSED"] => Some(0o40), + ["stat", "S_IRWXG"] => Some(0o70), + ["stat", "S_IEXEC"] => Some(0o100), + ["stat", "S_IXUSR"] => Some(0o100), + ["stat", "S_IWRITE"] => Some(0o200), + ["stat", "S_IWUSR"] => Some(0o200), + ["stat", "S_IREAD"] => Some(0o400), + ["stat", "S_IRUSR"] => Some(0o400), + ["stat", "S_IRWXU"] => Some(0o700), + ["stat", "S_ISVTX"] => Some(0o1000), + ["stat", "S_ISGID"] => Some(0o2000), + ["stat", "S_ENFMT"] => Some(0o2000), + ["stat", "S_ISUID"] => Some(0o4000), + _ => None, + } +} + +fn int_value(expr: &Expr, model: &SemanticModel) -> Option { + match expr { + Expr::Constant(ast::ExprConstant { + value: Constant::Int(value), + .. + }) => value.to_u16(), + Expr::Attribute(_) => model.resolve_call_path(expr).as_ref().and_then(py_stat), + Expr::BinOp(ast::ExprBinOp { + left, + op, + right, + range: _, + }) => { + let left_value = int_value(left, model)?; + let right_value = int_value(right, model)?; + match op { + Operator::BitAnd => Some(left_value & right_value), + Operator::BitOr => Some(left_value | right_value), + Operator::BitXor => Some(left_value ^ right_value), + _ => None, + } + } + _ => None, + } +} diff --git a/crates/ruff/src/rules/flake8_bandit/rules/exec_used.rs b/crates/ruff/src/rules/flake8_bandit/rules/exec_used.rs index 3ff3db8ded..af0caabc1d 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/exec_used.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/exec_used.rs @@ -1,8 +1,25 @@ -use rustpython_parser::ast::{self, Expr, Ranged}; +use rustpython_parser::ast::{Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for uses of the builtin `exec` function. +/// +/// ## Why is this bad? +/// The `exec()` function is insecure as it allows for arbitrary code +/// execution. +/// +/// ## Example +/// ```python +/// exec("print('Hello World')") +/// ``` +/// +/// ## References +/// - [Python documentation: `exec`](https://docs.python.org/3/library/functions.html#exec) +/// - [Common Weakness Enumeration: CWE-78](https://cwe.mitre.org/data/definitions/78.html) #[violation] pub struct ExecBuiltin; @@ -14,12 +31,16 @@ impl Violation for ExecBuiltin { } /// S102 -pub(crate) fn exec_used(expr: &Expr, func: &Expr) -> Option { - let Expr::Name(ast::ExprName { id, .. }) = func else { - return None; - }; - if id != "exec" { - return None; +pub(crate) fn exec_used(checker: &mut Checker, func: &Expr) { + if checker + .semantic() + .resolve_call_path(func) + .map_or(false, |call_path| { + matches!(call_path.as_slice(), ["" | "builtin", "exec"]) + }) + { + checker + .diagnostics + .push(Diagnostic::new(ExecBuiltin, func.range())); } - Some(Diagnostic::new(ExecBuiltin, expr.range())) } diff --git a/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_bind_all_interfaces.rs b/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_bind_all_interfaces.rs index 86f68e10b8..ac8090395b 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_bind_all_interfaces.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_bind_all_interfaces.rs @@ -3,6 +3,27 @@ use ruff_text_size::TextRange; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +/// ## What it does +/// Checks for hardcoded bindings to all network interfaces (`0.0.0.0`). +/// +/// ## Why is this bad? +/// Binding to all network interfaces is insecure as it allows access from +/// unintended interfaces, which may be poorly secured or unauthorized. +/// +/// Instead, bind to specific interfaces. +/// +/// ## Example +/// ```python +/// ALLOWED_HOSTS = ["0.0.0.0"] +/// ``` +/// +/// Use instead: +/// ```python +/// ALLOWED_HOSTS = ["127.0.0.1", "localhost"] +/// ``` +/// +/// ## References +/// - [Common Weakness Enumeration: CWE-200](https://cwe.mitre.org/data/definitions/200.html) #[violation] pub struct HardcodedBindAllInterfaces; diff --git a/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_password_default.rs b/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_password_default.rs index 0f61c414df..7883d346ac 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_password_default.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_password_default.rs @@ -1,10 +1,42 @@ -use rustpython_parser::ast::{Arg, Arguments, Expr, Ranged}; +use rustpython_parser::ast::{Arg, ArgWithDefault, Arguments, Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +use crate::checkers::ast::Checker; + use super::super::helpers::{matches_password_name, string_literal}; +/// ## What it does +/// Checks for potential uses of hardcoded passwords in function argument +/// defaults. +/// +/// ## Why is this bad? +/// Including a hardcoded password in source code is a security risk, as an +/// attacker could discover the password and use it to gain unauthorized +/// access. +/// +/// Instead, store passwords and other secrets in configuration files, +/// environment variables, or other sources that are excluded from version +/// control. +/// +/// ## Example +/// ```python +/// def connect_to_server(password="hunter2"): +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// import os +/// +/// +/// def connect_to_server(password=os.environ["PASSWORD"]): +/// ... +/// ``` +/// +/// ## References +/// - [Common Weakness Enumeration: CWE-259](https://cwe.mitre.org/data/definitions/259.html) #[violation] pub struct HardcodedPasswordDefault { name: String, @@ -36,34 +68,22 @@ fn check_password_kwarg(arg: &Arg, default: &Expr) -> Option { } /// S107 -pub(crate) fn hardcoded_password_default(arguments: &Arguments) -> Vec { - let mut diagnostics: Vec = Vec::new(); - - let defaults_start = - arguments.posonlyargs.len() + arguments.args.len() - arguments.defaults.len(); - for (i, arg) in arguments +pub(crate) fn hardcoded_password_default(checker: &mut Checker, arguments: &Arguments) { + for ArgWithDefault { + def, + default, + range: _, + } in arguments .posonlyargs .iter() .chain(&arguments.args) - .enumerate() + .chain(&arguments.kwonlyargs) { - if let Some(i) = i.checked_sub(defaults_start) { - let default = &arguments.defaults[i]; - if let Some(diagnostic) = check_password_kwarg(arg, default) { - diagnostics.push(diagnostic); - } + let Some(default) = default else { + continue; + }; + if let Some(diagnostic) = check_password_kwarg(def, default) { + checker.diagnostics.push(diagnostic); } } - - let defaults_start = arguments.kwonlyargs.len() - arguments.kw_defaults.len(); - for (i, kwarg) in arguments.kwonlyargs.iter().enumerate() { - if let Some(i) = i.checked_sub(defaults_start) { - let default = &arguments.kw_defaults[i]; - if let Some(diagnostic) = check_password_kwarg(kwarg, default) { - diagnostics.push(diagnostic); - } - } - } - - diagnostics } diff --git a/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_password_func_arg.rs b/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_password_func_arg.rs index b98b5e5efa..5449b51171 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_password_func_arg.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_password_func_arg.rs @@ -3,8 +3,36 @@ use rustpython_parser::ast::{Keyword, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +use crate::checkers::ast::Checker; + use super::super::helpers::{matches_password_name, string_literal}; +/// ## What it does +/// Checks for potential uses of hardcoded passwords in function calls. +/// +/// ## Why is this bad? +/// Including a hardcoded password in source code is a security risk, as an +/// attacker could discover the password and use it to gain unauthorized +/// access. +/// +/// Instead, store passwords and other secrets in configuration files, +/// environment variables, or other sources that are excluded from version +/// control. +/// +/// ## Example +/// ```python +/// connect_to_server(password="hunter2") +/// ``` +/// +/// Use instead: +/// ```python +/// import os +/// +/// connect_to_server(password=os.environ["PASSWORD"]) +/// ``` +/// +/// ## References +/// - [Common Weakness Enumeration: CWE-259](https://cwe.mitre.org/data/definitions/259.html) #[violation] pub struct HardcodedPasswordFuncArg { name: String, @@ -22,10 +50,10 @@ impl Violation for HardcodedPasswordFuncArg { } /// S106 -pub(crate) fn hardcoded_password_func_arg(keywords: &[Keyword]) -> Vec { - keywords - .iter() - .filter_map(|keyword| { +pub(crate) fn hardcoded_password_func_arg(checker: &mut Checker, keywords: &[Keyword]) { + checker + .diagnostics + .extend(keywords.iter().filter_map(|keyword| { string_literal(&keyword.value).filter(|string| !string.is_empty())?; let arg = keyword.arg.as_ref()?; if !matches_password_name(arg) { @@ -37,6 +65,5 @@ pub(crate) fn hardcoded_password_func_arg(keywords: &[Keyword]) -> Vec Option<&str> { /// S105 pub(crate) fn compare_to_hardcoded_password_string( + checker: &mut Checker, left: &Expr, comparators: &[Expr], -) -> Vec { - comparators - .iter() - .filter_map(|comp| { +) { + checker + .diagnostics + .extend(comparators.iter().filter_map(|comp| { string_literal(comp).filter(|string| !string.is_empty())?; let Some(name) = password_target(left) else { return None; @@ -63,29 +92,29 @@ pub(crate) fn compare_to_hardcoded_password_string( }, comp.range(), )) - }) - .collect() + })); } /// S105 pub(crate) fn assign_hardcoded_password_string( + checker: &mut Checker, value: &Expr, targets: &[Expr], -) -> Option { +) { if string_literal(value) .filter(|string| !string.is_empty()) .is_some() { for target in targets { if let Some(name) = password_target(target) { - return Some(Diagnostic::new( + checker.diagnostics.push(Diagnostic::new( HardcodedPasswordString { name: name.to_string(), }, value.range(), )); + return; } } } - None } diff --git a/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_sql_expression.rs b/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_sql_expression.rs index 4595dce6c7..51b200b67e 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_sql_expression.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_sql_expression.rs @@ -60,14 +60,14 @@ fn unparse_string_format_expression(checker: &mut Checker, expr: &Expr) -> Optio op: Operator::Add | Operator::Mod, .. }) => { - let Some(parent) = checker.semantic_model().expr_parent() else { + let Some(parent) = checker.semantic().expr_parent() else { if any_over_expr(expr, &has_string_literal) { return Some(checker.generator().expr(expr)); } return None; }; // Only evaluate the full BinOp, not the nested components. - let Expr::BinOp(_ )= parent else { + let Expr::BinOp(_) = parent else { if any_over_expr(expr, &has_string_literal) { return Some(checker.generator().expr(expr)); } diff --git a/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_tmp_directory.rs b/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_tmp_directory.rs index 8c27763f80..2fae004907 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_tmp_directory.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_tmp_directory.rs @@ -3,6 +3,35 @@ use rustpython_parser::ast::{Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +/// ## What it does +/// Checks for the use of hardcoded temporary file or directory paths. +/// +/// ## Why is this bad? +/// The use of hardcoded paths for temporary files can be insecure. If an +/// attacker discovers the location of a hardcoded path, they can replace the +/// contents of the file or directory with a malicious payload. +/// +/// Other programs may also read or write contents to these hardcoded paths, +/// causing unexpected behavior. +/// +/// ## Example +/// ```python +/// with open("/tmp/foo.txt", "w") as file: +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// import tempfile +/// +/// with tempfile.NamedTemporaryFile() as file: +/// ... +/// ``` +/// +/// ## References +/// - [Common Weakness Enumeration: CWE-377](https://cwe.mitre.org/data/definitions/377.html) +/// - [Common Weakness Enumeration: CWE-379](https://cwe.mitre.org/data/definitions/379.html) +/// - [Python documentation: `tempfile`](https://docs.python.org/3/library/tempfile.html) #[violation] pub struct HardcodedTempFile { string: String, diff --git a/crates/ruff/src/rules/flake8_bandit/rules/hashlib_insecure_hash_functions.rs b/crates/ruff/src/rules/flake8_bandit/rules/hashlib_insecure_hash_functions.rs index 6948f6323b..7a8385e8d5 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/hashlib_insecure_hash_functions.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/hashlib_insecure_hash_functions.rs @@ -1,13 +1,51 @@ -use rustpython_parser::ast::{self, Constant, Expr, Keyword, Ranged}; +use rustpython_parser::ast::{Expr, Keyword, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::SimpleCallArgs; +use ruff_python_ast::helpers::{is_const_false, SimpleCallArgs}; use crate::checkers::ast::Checker; use super::super::helpers::string_literal; +/// ## What it does +/// Checks for uses of weak or broken cryptographic hash functions. +/// +/// ## Why is this bad? +/// Weak or broken cryptographic hash functions may be susceptible to +/// collision attacks (where two different inputs produce the same hash) or +/// pre-image attacks (where an attacker can find an input that produces a +/// given hash). This can lead to security vulnerabilities in applications +/// that rely on these hash functions. +/// +/// Avoid using weak or broken cryptographic hash functions in security +/// contexts. Instead, use a known secure hash function such as SHA256. +/// +/// ## Example +/// ```python +/// import hashlib +/// +/// +/// def certificate_is_valid(certificate: bytes, known_hash: str) -> bool: +/// hash = hashlib.md5(certificate).hexdigest() +/// return hash == known_hash +/// ``` +/// +/// Use instead: +/// ```python +/// import hashlib +/// +/// +/// def certificate_is_valid(certificate: bytes, known_hash: str) -> bool: +/// hash = hashlib.sha256(certificate).hexdigest() +/// return hash == known_hash +/// ``` +/// +/// ## References +/// - [Python documentation: `hashlib` — Secure hashes and message digests](https://docs.python.org/3/library/hashlib.html) +/// - [Common Weakness Enumeration: CWE-327](https://cwe.mitre.org/data/definitions/327.html) +/// - [Common Weakness Enumeration: CWE-328](https://cwe.mitre.org/data/definitions/328.html) +/// - [Common Weakness Enumeration: CWE-916](https://cwe.mitre.org/data/definitions/916.html) #[violation] pub struct HashlibInsecureHashFunction { string: String, @@ -21,26 +59,6 @@ impl Violation for HashlibInsecureHashFunction { } } -const WEAK_HASHES: [&str; 4] = ["md4", "md5", "sha", "sha1"]; - -fn is_used_for_security(call_args: &SimpleCallArgs) -> bool { - match call_args.keyword_argument("usedforsecurity") { - Some(expr) => !matches!( - expr, - Expr::Constant(ast::ExprConstant { - value: Constant::Bool(false), - .. - }) - ), - _ => true, - } -} - -enum HashlibCall { - New, - WeakHash(&'static str), -} - /// S324 pub(crate) fn hashlib_insecure_hash_functions( checker: &mut Checker, @@ -48,20 +66,17 @@ pub(crate) fn hashlib_insecure_hash_functions( args: &[Expr], keywords: &[Keyword], ) { - if let Some(hashlib_call) = - checker - .semantic_model() - .resolve_call_path(func) - .and_then(|call_path| { - if call_path.as_slice() == ["hashlib", "new"] { - Some(HashlibCall::New) - } else { - WEAK_HASHES - .iter() - .find(|hash| call_path.as_slice() == ["hashlib", hash]) - .map(|hash| HashlibCall::WeakHash(hash)) - } - }) + if let Some(hashlib_call) = checker + .semantic() + .resolve_call_path(func) + .and_then(|call_path| match call_path.as_slice() { + ["hashlib", "new"] => Some(HashlibCall::New), + ["hashlib", "md4"] => Some(HashlibCall::WeakHash("md4")), + ["hashlib", "md5"] => Some(HashlibCall::WeakHash("md5")), + ["hashlib", "sha"] => Some(HashlibCall::WeakHash("sha")), + ["hashlib", "sha1"] => Some(HashlibCall::WeakHash("sha1")), + _ => None, + }) { match hashlib_call { HashlibCall::New => { @@ -73,7 +88,12 @@ pub(crate) fn hashlib_insecure_hash_functions( if let Some(name_arg) = call_args.argument("name", 0) { if let Some(hash_func_name) = string_literal(name_arg) { - if WEAK_HASHES.contains(&hash_func_name.to_lowercase().as_str()) { + // `hashlib.new` accepts both lowercase and uppercase names for hash + // functions. + if matches!( + hash_func_name, + "md4" | "md5" | "sha" | "sha1" | "MD4" | "MD5" | "SHA" | "SHA1" + ) { checker.diagnostics.push(Diagnostic::new( HashlibInsecureHashFunction { string: hash_func_name.to_string(), @@ -101,3 +121,15 @@ pub(crate) fn hashlib_insecure_hash_functions( } } } + +fn is_used_for_security(call_args: &SimpleCallArgs) -> bool { + match call_args.keyword_argument("usedforsecurity") { + Some(expr) => !is_const_false(expr), + _ => true, + } +} + +enum HashlibCall { + New, + WeakHash(&'static str), +} diff --git a/crates/ruff/src/rules/flake8_bandit/rules/jinja2_autoescape_false.rs b/crates/ruff/src/rules/flake8_bandit/rules/jinja2_autoescape_false.rs index 06a2e77081..bf368e89e7 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/jinja2_autoescape_false.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/jinja2_autoescape_false.rs @@ -37,10 +37,10 @@ pub(crate) fn jinja2_autoescape_false( keywords: &[Keyword], ) { if checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["jinja2", "Environment"] + matches!(call_path.as_slice(), ["jinja2", "Environment"]) }) { let call_args = SimpleCallArgs::new(args, keywords); diff --git a/crates/ruff/src/rules/flake8_bandit/rules/logging_config_insecure_listen.rs b/crates/ruff/src/rules/flake8_bandit/rules/logging_config_insecure_listen.rs index 6fc645bb88..0953a96113 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/logging_config_insecure_listen.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/logging_config_insecure_listen.rs @@ -24,10 +24,10 @@ pub(crate) fn logging_config_insecure_listen( keywords: &[Keyword], ) { if checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["logging", "config", "listen"] + matches!(call_path.as_slice(), ["logging", "config", "listen"]) }) { let call_args = SimpleCallArgs::new(args, keywords); diff --git a/crates/ruff/src/rules/flake8_bandit/rules/mod.rs b/crates/ruff/src/rules/flake8_bandit/rules/mod.rs index 90f1266e42..83cf002caf 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/mod.rs @@ -1,50 +1,25 @@ -pub(crate) use assert_used::{assert_used, Assert}; -pub(crate) use bad_file_permissions::{bad_file_permissions, BadFilePermissions}; -pub(crate) use exec_used::{exec_used, ExecBuiltin}; -pub(crate) use hardcoded_bind_all_interfaces::{ - hardcoded_bind_all_interfaces, HardcodedBindAllInterfaces, -}; -pub(crate) use hardcoded_password_default::{hardcoded_password_default, HardcodedPasswordDefault}; -pub(crate) use hardcoded_password_func_arg::{ - hardcoded_password_func_arg, HardcodedPasswordFuncArg, -}; -pub(crate) use hardcoded_password_string::{ - assign_hardcoded_password_string, compare_to_hardcoded_password_string, HardcodedPasswordString, -}; -pub(crate) use hardcoded_sql_expression::{hardcoded_sql_expression, HardcodedSQLExpression}; -pub(crate) use hardcoded_tmp_directory::{hardcoded_tmp_directory, HardcodedTempFile}; -pub(crate) use hashlib_insecure_hash_functions::{ - hashlib_insecure_hash_functions, HashlibInsecureHashFunction, -}; -pub(crate) use jinja2_autoescape_false::{jinja2_autoescape_false, Jinja2AutoescapeFalse}; -pub(crate) use logging_config_insecure_listen::{ - logging_config_insecure_listen, LoggingConfigInsecureListen, -}; -pub(crate) use paramiko_calls::{paramiko_call, ParamikoCall}; -pub(crate) use request_with_no_cert_validation::{ - request_with_no_cert_validation, RequestWithNoCertValidation, -}; -pub(crate) use request_without_timeout::{request_without_timeout, RequestWithoutTimeout}; -pub(crate) use shell_injection::{ - shell_injection, CallWithShellEqualsTrue, StartProcessWithAShell, StartProcessWithNoShell, - StartProcessWithPartialPath, SubprocessPopenWithShellEqualsTrue, - SubprocessWithoutShellEqualsTrue, UnixCommandWildcardInjection, -}; -pub(crate) use snmp_insecure_version::{snmp_insecure_version, SnmpInsecureVersion}; -pub(crate) use snmp_weak_cryptography::{snmp_weak_cryptography, SnmpWeakCryptography}; -pub(crate) use suspicious_function_call::{ - suspicious_function_call, SuspiciousEvalUsage, SuspiciousFTPLibUsage, - SuspiciousInsecureCipherModeUsage, SuspiciousInsecureCipherUsage, SuspiciousInsecureHashUsage, - SuspiciousMarkSafeUsage, SuspiciousMarshalUsage, SuspiciousMktempUsage, - SuspiciousNonCryptographicRandomUsage, SuspiciousPickleUsage, SuspiciousTelnetUsage, - SuspiciousURLOpenUsage, SuspiciousUnverifiedContextUsage, SuspiciousXMLCElementTreeUsage, - SuspiciousXMLETreeUsage, SuspiciousXMLElementTreeUsage, SuspiciousXMLExpatBuilderUsage, - SuspiciousXMLExpatReaderUsage, SuspiciousXMLMiniDOMUsage, SuspiciousXMLPullDOMUsage, - SuspiciousXMLSaxUsage, -}; -pub(crate) use try_except_continue::{try_except_continue, TryExceptContinue}; -pub(crate) use try_except_pass::{try_except_pass, TryExceptPass}; -pub(crate) use unsafe_yaml_load::{unsafe_yaml_load, UnsafeYAMLLoad}; +pub(crate) use assert_used::*; +pub(crate) use bad_file_permissions::*; +pub(crate) use exec_used::*; +pub(crate) use hardcoded_bind_all_interfaces::*; +pub(crate) use hardcoded_password_default::*; +pub(crate) use hardcoded_password_func_arg::*; +pub(crate) use hardcoded_password_string::*; +pub(crate) use hardcoded_sql_expression::*; +pub(crate) use hardcoded_tmp_directory::*; +pub(crate) use hashlib_insecure_hash_functions::*; +pub(crate) use jinja2_autoescape_false::*; +pub(crate) use logging_config_insecure_listen::*; +pub(crate) use paramiko_calls::*; +pub(crate) use request_with_no_cert_validation::*; +pub(crate) use request_without_timeout::*; +pub(crate) use shell_injection::*; +pub(crate) use snmp_insecure_version::*; +pub(crate) use snmp_weak_cryptography::*; +pub(crate) use suspicious_function_call::*; +pub(crate) use try_except_continue::*; +pub(crate) use try_except_pass::*; +pub(crate) use unsafe_yaml_load::*; mod assert_used; mod bad_file_permissions; diff --git a/crates/ruff/src/rules/flake8_bandit/rules/paramiko_calls.rs b/crates/ruff/src/rules/flake8_bandit/rules/paramiko_calls.rs index e340a09ca4..e0e50828d1 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/paramiko_calls.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/paramiko_calls.rs @@ -18,10 +18,10 @@ impl Violation for ParamikoCall { /// S601 pub(crate) fn paramiko_call(checker: &mut Checker, func: &Expr) { if checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["paramiko", "exec_command"] + matches!(call_path.as_slice(), ["paramiko", "exec_command"]) }) { checker diff --git a/crates/ruff/src/rules/flake8_bandit/rules/request_with_no_cert_validation.rs b/crates/ruff/src/rules/flake8_bandit/rules/request_with_no_cert_validation.rs index fe45fb6e76..27c22af441 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/request_with_no_cert_validation.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/request_with_no_cert_validation.rs @@ -1,8 +1,8 @@ -use rustpython_parser::ast::{self, Constant, Expr, Keyword, Ranged}; +use rustpython_parser::ast::{Expr, Keyword, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::SimpleCallArgs; +use ruff_python_ast::helpers::{is_const_false, SimpleCallArgs}; use crate::checkers::ast::Checker; @@ -21,21 +21,6 @@ impl Violation for RequestWithNoCertValidation { } } -const REQUESTS_HTTP_VERBS: [&str; 7] = ["get", "options", "head", "post", "put", "patch", "delete"]; -const HTTPX_METHODS: [&str; 11] = [ - "get", - "options", - "head", - "post", - "put", - "patch", - "delete", - "request", - "stream", - "Client", - "AsyncClient", -]; - /// S501 pub(crate) fn request_with_no_cert_validation( checker: &mut Checker, @@ -44,27 +29,20 @@ pub(crate) fn request_with_no_cert_validation( keywords: &[Keyword], ) { if let Some(target) = checker - .semantic_model() + .semantic() .resolve_call_path(func) - .and_then(|call_path| { - if call_path.len() == 2 { - if call_path[0] == "requests" && REQUESTS_HTTP_VERBS.contains(&call_path[1]) { - return Some("requests"); - } - if call_path[0] == "httpx" && HTTPX_METHODS.contains(&call_path[1]) { - return Some("httpx"); - } + .and_then(|call_path| match call_path.as_slice() { + ["requests", "get" | "options" | "head" | "post" | "put" | "patch" | "delete"] => { + Some("requests") } - None + ["httpx", "get" | "options" | "head" | "post" | "put" | "patch" | "delete" | "request" + | "stream" | "Client" | "AsyncClient"] => Some("httpx"), + _ => None, }) { let call_args = SimpleCallArgs::new(args, keywords); if let Some(verify_arg) = call_args.keyword_argument("verify") { - if let Expr::Constant(ast::ExprConstant { - value: Constant::Bool(false), - .. - }) = &verify_arg - { + if is_const_false(verify_arg) { checker.diagnostics.push(Diagnostic::new( RequestWithNoCertValidation { string: target.to_string(), diff --git a/crates/ruff/src/rules/flake8_bandit/rules/request_without_timeout.rs b/crates/ruff/src/rules/flake8_bandit/rules/request_without_timeout.rs index b08edbd4fa..e62e92687c 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/request_without_timeout.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/request_without_timeout.rs @@ -1,31 +1,53 @@ -use rustpython_parser::ast::{self, Constant, Expr, Keyword, Ranged}; +use rustpython_parser::ast::{Expr, Keyword, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::SimpleCallArgs; +use ruff_python_ast::helpers::{is_const_none, SimpleCallArgs}; use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for uses of the Python `requests` module that omit the `timeout` +/// parameter. +/// +/// ## Why is this bad? +/// The `timeout` parameter is used to set the maximum time to wait for a +/// response from the server. By omitting the `timeout` parameter, the program +/// may hang indefinitely while awaiting a response. +/// +/// ## Example +/// ```python +/// import requests +/// +/// requests.get("https://www.example.com/") +/// ``` +/// +/// Use instead: +/// ```python +/// import requests +/// +/// requests.get("https://www.example.com/", timeout=10) +/// ``` +/// +/// ## References +/// - [Requests documentation: Timeouts](https://requests.readthedocs.io/en/latest/user/advanced/#timeouts) #[violation] pub struct RequestWithoutTimeout { - pub timeout: Option, + implicit: bool, } impl Violation for RequestWithoutTimeout { #[derive_message_formats] fn message(&self) -> String { - let RequestWithoutTimeout { timeout } = self; - match timeout { - Some(value) => { - format!("Probable use of requests call with timeout set to `{value}`") - } - None => format!("Probable use of requests call without timeout"), + let RequestWithoutTimeout { implicit } = self; + if *implicit { + format!("Probable use of requests call without timeout") + } else { + format!("Probable use of requests call with timeout set to `None`") } } } -const HTTP_VERBS: [&str; 7] = ["get", "options", "head", "post", "put", "patch", "delete"]; - /// S113 pub(crate) fn request_without_timeout( checker: &mut Checker, @@ -34,33 +56,29 @@ pub(crate) fn request_without_timeout( keywords: &[Keyword], ) { if checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - HTTP_VERBS - .iter() - .any(|func_name| call_path.as_slice() == ["requests", func_name]) + matches!( + call_path.as_slice(), + [ + "requests", + "get" | "options" | "head" | "post" | "put" | "patch" | "delete" + ] + ) }) { let call_args = SimpleCallArgs::new(args, keywords); - if let Some(timeout_arg) = call_args.keyword_argument("timeout") { - if let Some(timeout) = match timeout_arg { - Expr::Constant(ast::ExprConstant { - value: value @ Constant::None, - .. - }) => Some(checker.generator().constant(value)), - _ => None, - } { + if let Some(timeout) = call_args.keyword_argument("timeout") { + if is_const_none(timeout) { checker.diagnostics.push(Diagnostic::new( - RequestWithoutTimeout { - timeout: Some(timeout), - }, - timeout_arg.range(), + RequestWithoutTimeout { implicit: false }, + timeout.range(), )); } } else { checker.diagnostics.push(Diagnostic::new( - RequestWithoutTimeout { timeout: None }, + RequestWithoutTimeout { implicit: true }, func.range(), )); } diff --git a/crates/ruff/src/rules/flake8_bandit/rules/shell_injection.rs b/crates/ruff/src/rules/flake8_bandit/rules/shell_injection.rs index 6eb6c9f5e2..3617a56eb6 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/shell_injection.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/shell_injection.rs @@ -5,7 +5,7 @@ use rustpython_parser::ast::{self, Constant, Expr, Keyword, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::Truthiness; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; use crate::{ checkers::ast::Checker, registry::Rule, rules::flake8_bandit::helpers::string_literal, @@ -102,8 +102,8 @@ pub(crate) fn shell_injection( args: &[Expr], keywords: &[Keyword], ) { - let call_kind = get_call_kind(func, checker.semantic_model()); - let shell_keyword = find_shell_keyword(checker.semantic_model(), keywords); + let call_kind = get_call_kind(func, checker.semantic()); + let shell_keyword = find_shell_keyword(keywords, checker.semantic()); if matches!(call_kind, Some(CallKind::Subprocess)) { if let Some(arg) = args.first() { @@ -227,8 +227,8 @@ enum CallKind { } /// Return the [`CallKind`] of the given function call. -fn get_call_kind(func: &Expr, model: &SemanticModel) -> Option { - model +fn get_call_kind(func: &Expr, semantic: &SemanticModel) -> Option { + semantic .resolve_call_path(func) .and_then(|call_path| match call_path.as_slice() { &[module, submodule] => match module { @@ -269,14 +269,14 @@ struct ShellKeyword<'a> { /// Return the `shell` keyword argument to the given function call, if any. fn find_shell_keyword<'a>( - model: &SemanticModel, keywords: &'a [Keyword], + semantic: &SemanticModel, ) -> Option> { keywords .iter() .find(|keyword| keyword.arg.as_ref().map_or(false, |arg| arg == "shell")) .map(|keyword| ShellKeyword { - truthiness: Truthiness::from_expr(&keyword.value, |id| model.is_builtin(id)), + truthiness: Truthiness::from_expr(&keyword.value, |id| semantic.is_builtin(id)), keyword, }) } @@ -350,7 +350,7 @@ fn is_wildcard_command(expr: &Expr) -> bool { if let Expr::List(ast::ExprList { elts, .. }) = expr { let mut has_star = false; let mut has_command = false; - for elt in elts.iter() { + for elt in elts { if let Some(text) = string_literal(elt) { has_star |= text.contains('*'); has_command |= text.contains("chown") diff --git a/crates/ruff/src/rules/flake8_bandit/rules/snmp_insecure_version.rs b/crates/ruff/src/rules/flake8_bandit/rules/snmp_insecure_version.rs index 60fc3b33dd..1c4b032f97 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/snmp_insecure_version.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/snmp_insecure_version.rs @@ -25,10 +25,10 @@ pub(crate) fn snmp_insecure_version( keywords: &[Keyword], ) { if checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["pysnmp", "hlapi", "CommunityData"] + matches!(call_path.as_slice(), ["pysnmp", "hlapi", "CommunityData"]) }) { let call_args = SimpleCallArgs::new(args, keywords); diff --git a/crates/ruff/src/rules/flake8_bandit/rules/snmp_weak_cryptography.rs b/crates/ruff/src/rules/flake8_bandit/rules/snmp_weak_cryptography.rs index 313c32a187..f654c2550e 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/snmp_weak_cryptography.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/snmp_weak_cryptography.rs @@ -27,10 +27,10 @@ pub(crate) fn snmp_weak_cryptography( keywords: &[Keyword], ) { if checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["pysnmp", "hlapi", "UsmUserData"] + matches!(call_path.as_slice(), ["pysnmp", "hlapi", "UsmUserData"]) }) { let call_args = SimpleCallArgs::new(args, keywords); diff --git a/crates/ruff/src/rules/flake8_bandit/rules/suspicious_function_call.rs b/crates/ruff/src/rules/flake8_bandit/rules/suspicious_function_call.rs index 6c6eba80fe..79687fe458 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/suspicious_function_call.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/suspicious_function_call.rs @@ -9,6 +9,42 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Checks for calls to `pickle` functions or modules that wrap them. +/// +/// ## Why is this bad? +/// Deserializing untrusted data with `pickle` and other deserialization +/// modules is insecure as it can allow for the creation of arbitrary objects, +/// which can then be used to achieve arbitrary code execution and otherwise +/// unexpected behavior. +/// +/// Avoid deserializing untrusted data with `pickle` and other deserialization +/// modules. Instead, consider safer formats, such as JSON. +/// +/// If you must deserialize untrusted data with `pickle`, consider signing the +/// data with a secret key and verifying the signature before deserializing the +/// payload, This will prevent an attacker from injecting arbitrary objects +/// into the serialized data. +/// +/// ## Example +/// ```python +/// import pickle +/// +/// with open("foo.pickle", "rb") as file: +/// foo = pickle.load(file) +/// ``` +/// +/// Use instead: +/// ```python +/// import json +/// +/// with open("foo.json", "rb") as file: +/// foo = json.load(file) +/// ``` +/// +/// ## References +/// - [Python documentation: `pickle` — Python object serialization](https://docs.python.org/3/library/pickle.html) +/// - [Common Weakness Enumeration: CWE-502](https://cwe.mitre.org/data/definitions/502.html) #[violation] pub struct SuspiciousPickleUsage; @@ -19,6 +55,41 @@ impl Violation for SuspiciousPickleUsage { } } +/// ## What it does +/// Checks for calls to `marshal` functions. +/// +/// ## Why is this bad? +/// Deserializing untrusted data with `marshal` is insecure as it can allow for +/// the creation of arbitrary objects, which can then be used to achieve +/// arbitrary code execution and otherwise unexpected behavior. +/// +/// Avoid deserializing untrusted data with `marshal`. Instead, consider safer +/// formats, such as JSON. +/// +/// If you must deserialize untrusted data with `marshal`, consider signing the +/// data with a secret key and verifying the signature before deserializing the +/// payload, This will prevent an attacker from injecting arbitrary objects +/// into the serialized data. +/// +/// ## Example +/// ```python +/// import marshal +/// +/// with open("foo.marshal", "rb") as file: +/// foo = pickle.load(file) +/// ``` +/// +/// Use instead: +/// ```python +/// import json +/// +/// with open("foo.json", "rb") as file: +/// foo = json.load(file) +/// ``` +/// +/// ## References +/// - [Python documentation: `marshal` — Internal Python object serialization](https://docs.python.org/3/library/marshal.html) +/// - [Common Weakness Enumeration: CWE-502](https://cwe.mitre.org/data/definitions/502.html) #[violation] pub struct SuspiciousMarshalUsage; @@ -29,6 +100,42 @@ impl Violation for SuspiciousMarshalUsage { } } +/// ## What it does +/// Checks for uses of weak or broken cryptographic hash functions. +/// +/// ## Why is this bad? +/// Weak or broken cryptographic hash functions may be susceptible to +/// collision attacks (where two different inputs produce the same hash) or +/// pre-image attacks (where an attacker can find an input that produces a +/// given hash). This can lead to security vulnerabilities in applications +/// that rely on these hash functions. +/// +/// Avoid using weak or broken cryptographic hash functions in security +/// contexts. Instead, use a known secure hash function such as SHA-256. +/// +/// ## Example +/// ```python +/// from cryptography.hazmat.primitives import hashes +/// +/// digest = hashes.Hash(hashes.MD5()) +/// digest.update(b"Hello, world!") +/// digest.finalize() +/// ``` +/// +/// Use instead: +/// ```python +/// from cryptography.hazmat.primitives import hashes +/// +/// digest = hashes.Hash(hashes.SHA256()) +/// digest.update(b"Hello, world!") +/// digest.finalize() +/// ``` +/// +/// ## References +/// - [Python documentation: `hashlib` — Secure hashes and message digests](https://docs.python.org/3/library/hashlib.html) +/// - [Common Weakness Enumeration: CWE-327](https://cwe.mitre.org/data/definitions/327.html) +/// - [Common Weakness Enumeration: CWE-328](https://cwe.mitre.org/data/definitions/328.html) +/// - [Common Weakness Enumeration: CWE-916](https://cwe.mitre.org/data/definitions/916.html) #[violation] pub struct SuspiciousInsecureHashUsage; @@ -39,6 +146,34 @@ impl Violation for SuspiciousInsecureHashUsage { } } +/// ## What it does +/// Checks for uses of weak or broken cryptographic ciphers. +/// +/// ## Why is this bad? +/// Weak or broken cryptographic ciphers may be susceptible to attacks that +/// allow an attacker to decrypt ciphertext without knowing the key or +/// otherwise compromise the security of the cipher, such as forgeries. +/// +/// Use strong, modern cryptographic ciphers instead of weak or broken ones. +/// +/// ## Example +/// ```python +/// from cryptography.hazmat.primitives.ciphers import Cipher, algorithms +/// +/// algorithm = algorithms.ARC4(key) +/// cipher = Cipher(algorithm, mode=None) +/// encryptor = cipher.encryptor() +/// ``` +/// +/// Use instead: +/// ```python +/// from cryptography.fernet import Fernet +/// +/// fernet = Fernet(key) +/// ``` +/// +/// ## References +/// - [Common Weakness Enumeration: CWE-327](https://cwe.mitre.org/data/definitions/327.html) #[violation] pub struct SuspiciousInsecureCipherUsage; @@ -49,6 +184,36 @@ impl Violation for SuspiciousInsecureCipherUsage { } } +/// ## What it does +/// Checks for uses of weak or broken cryptographic cipher modes. +/// +/// ## Why is this bad? +/// Weak or broken cryptographic ciphers may be susceptible to attacks that +/// allow an attacker to decrypt ciphertext without knowing the key or +/// otherwise compromise the security of the cipher, such as forgeries. +/// +/// Use strong, modern cryptographic ciphers instead of weak or broken ones. +/// +/// ## Example +/// ```python +/// from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +/// +/// algorithm = algorithms.ARC4(key) +/// cipher = Cipher(algorithm, mode=modes.ECB(iv)) +/// encryptor = cipher.encryptor() +/// ``` +/// +/// Use instead: +/// ```python +/// from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +/// +/// algorithm = algorithms.ARC4(key) +/// cipher = Cipher(algorithm, mode=modes.CTR(iv)) +/// encryptor = cipher.encryptor() +/// ``` +/// +/// ## References +/// - [Common Weakness Enumeration: CWE-327](https://cwe.mitre.org/data/definitions/327.html) #[violation] pub struct SuspiciousInsecureCipherModeUsage; @@ -59,6 +224,40 @@ impl Violation for SuspiciousInsecureCipherModeUsage { } } +/// ## What it does +/// Checks for uses of `tempfile.mktemp`. +/// +/// ## Why is this bad? +/// `tempfile.mktemp` returns a pathname of a file that does not exist at the +/// time the call is made; then, the caller is responsible for creating the +/// file and subsequently using it. This is insecure because another process +/// could create a file with the same name between the time the function +/// returns and the time the caller creates the file. +/// +/// `tempfile.mktemp` is deprecated in favor of `tempfile.mkstemp` which +/// creates the file when it is called. Consider using `tempfile.mkstemp` +/// instead, either directly or via a context manager such as +/// `tempfile.TemporaryFile`. +/// +/// ## Example +/// ```python +/// import tempfile +/// +/// tmp_file = tempfile.mktemp() +/// with open(tmp_file, "w") as file: +/// file.write("Hello, world!") +/// ``` +/// +/// Use instead: +/// ```python +/// import tempfile +/// +/// with tempfile.TemporaryFile() as file: +/// file.write("Hello, world!") +/// ``` +/// +/// ## References +/// - [Python documentation:`mktemp`](https://docs.python.org/3/library/tempfile.html#tempfile.mktemp) #[violation] pub struct SuspiciousMktempUsage; @@ -69,6 +268,32 @@ impl Violation for SuspiciousMktempUsage { } } +/// ## What it does +/// Checks for uses of the builtin `eval()` function. +/// +/// ## Why is this bad? +/// The `eval()` function is insecure as it enables arbitrary code execution. +/// +/// If you need to evaluate an expression from a string, consider using +/// `ast.literal_eval()` instead, which will raise an exception if the +/// expression is not a valid Python literal. +/// +/// ## Example +/// ```python +/// x = eval(input("Enter a number: ")) +/// ``` +/// +/// Use instead: +/// ```python +/// from ast import literal_eval +/// +/// x = literal_eval(input("Enter a number: ")) +/// ``` +/// +/// ## References +/// - [Python documentation: `eval`](https://docs.python.org/3/library/functions.html#eval) +/// - [Python documentation: `literal_eval`](https://docs.python.org/3/library/ast.html#ast.literal_eval) +/// - [_Eval really is dangerous_ by Ned Batchelder](https://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html) #[violation] pub struct SuspiciousEvalUsage; @@ -79,6 +304,35 @@ impl Violation for SuspiciousEvalUsage { } } +/// ## What it does +/// Checks for uses of calls to `django.utils.safestring.mark_safe`. +/// +/// ## Why is this bad? +/// Cross-site scripting (XSS) vulnerabilities allow attackers to execute +/// arbitrary JavaScript. To guard against XSS attacks, Django templates +/// assumes that data is unsafe and automatically escapes malicious strings +/// before rending them. +/// +/// `django.utils.safestring.mark_safe` marks a string as safe for use in HTML +/// templates, bypassing XSS protection. This is dangerous because it may allow +/// cross-site scripting attacks if the string is not properly escaped. +/// +/// ## Example +/// ```python +/// from django.utils.safestring import mark_safe +/// +/// content = mark_safe("") # XSS. +/// ``` +/// +/// Use instead: +/// ```python +/// content = "" # Safe if rendered. +/// ``` +/// +/// ## References +/// - [Django documentation: `mark_safe`](https://docs.djangoproject.com/en/dev/ref/utils/#django.utils.safestring.mark_safe) +/// - [Django documentation: Cross Site Scripting (XSS) protection](https://docs.djangoproject.com/en/dev/topics/security/#cross-site-scripting-xss-protection) +/// - [Common Weakness Enumeration: CWE-80](https://cwe.mitre.org/data/definitions/80.html) #[violation] pub struct SuspiciousMarkSafeUsage; @@ -89,6 +343,44 @@ impl Violation for SuspiciousMarkSafeUsage { } } +/// ## What it does +/// Checks for uses of URL open functions that unexpected schemes. +/// +/// ## Why is this bad? +/// Some URL open functions allow the use of `file:` or custom schemes (for use +/// instead of `http:` or `https:`). An attacker may be able to use these +/// schemes to access or modify unauthorized resources, and cause unexpected +/// behavior. +/// +/// To mitigate this risk, audit all uses of URL open functions and ensure that +/// only permitted schemes are used (e.g., allowing `http:` and `https:` and +/// disallowing `file:` and `ftp:`). +/// +/// ## Example +/// ```python +/// from urllib.request import urlopen +/// +/// url = input("Enter a URL: ") +/// +/// with urlopen(url) as response: +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// from urllib.request import urlopen +/// +/// url = input("Enter a URL: ") +/// +/// if not url.startswith(("http:", "https:")): +/// raise ValueError("URL must start with 'http:' or 'https:'") +/// +/// with urlopen(url) as response: +/// ... +/// ``` +/// +/// ## References +/// - [Python documentation: `urlopen`](https://docs.python.org/3/library/urllib.request.html#urllib.request.urlopen) #[violation] pub struct SuspiciousURLOpenUsage; @@ -99,6 +391,34 @@ impl Violation for SuspiciousURLOpenUsage { } } +/// ## What it does +/// Checks for uses of cryptographically weak pseudo-random number generators. +/// +/// ## Why is this bad? +/// Cryptographically weak pseudo-random number generators are insecure as they +/// are easily predictable. This can allow an attacker to guess the generated +/// numbers and compromise the security of the system. +/// +/// Instead, use a cryptographically secure pseudo-random number generator +/// (such as using the [`secrets` module](https://docs.python.org/3/library/secrets.html)) +/// when generating random numbers for security purposes. +/// +/// ## Example +/// ```python +/// import random +/// +/// random.randrange(10) +/// ``` +/// +/// Use instead: +/// ```python +/// import secrets +/// +/// secrets.randbelow(10) +/// ``` +/// +/// ## References +/// - [Python documentation: `random` — Generate pseudo-random numbers](https://docs.python.org/3/library/random.html) #[violation] pub struct SuspiciousNonCryptographicRandomUsage; @@ -109,6 +429,36 @@ impl Violation for SuspiciousNonCryptographicRandomUsage { } } +/// ## What it does +/// Checks for uses of insecure XML parsers. +/// +/// ## Why is this bad? +/// Many XML parsers are vulnerable to XML attacks (such as entity expansion), +/// which cause excessive memory and CPU usage by exploiting recursion. An +/// attacker could use such methods to access unauthorized resources. +/// +/// Consider using the `defusedxml` package when parsing untrusted XML data, +/// to protect against XML attacks. +/// +/// ## Example +/// ```python +/// from xml.etree.cElementTree import parse +/// +/// tree = parse("untrusted.xml") # Vulnerable to XML attacks. +/// ``` +/// +/// Use instead: +/// ```python +/// from defusedxml.cElementTree import parse +/// +/// tree = parse("untrusted.xml") +/// ``` +/// +/// ## References +/// - [Python documentation: `xml` — XML processing modules](https://docs.python.org/3/library/xml.html) +/// - [PyPI: `defusedxml`](https://pypi.org/project/defusedxml/) +/// - [Common Weakness Enumeration: CWE-400](https://cwe.mitre.org/data/definitions/400.html) +/// - [Common Weakness Enumeration: CWE-776](https://cwe.mitre.org/data/definitions/776.html) #[violation] pub struct SuspiciousXMLCElementTreeUsage; @@ -119,6 +469,36 @@ impl Violation for SuspiciousXMLCElementTreeUsage { } } +/// ## What it does +/// Checks for uses of insecure XML parsers. +/// +/// ## Why is this bad? +/// Many XML parsers are vulnerable to XML attacks (such as entity expansion), +/// which cause excessive memory and CPU usage by exploiting recursion. An +/// attacker could use such methods to access unauthorized resources. +/// +/// Consider using the `defusedxml` package when parsing untrusted XML data, +/// to protect against XML attacks. +/// +/// ## Example +/// ```python +/// from xml.etree.ElementTree import parse +/// +/// tree = parse("untrusted.xml") # Vulnerable to XML attacks. +/// ``` +/// +/// Use instead: +/// ```python +/// from defusedxml.ElementTree import parse +/// +/// tree = parse("untrusted.xml") +/// ``` +/// +/// ## References +/// - [Python documentation: `xml` — XML processing modules](https://docs.python.org/3/library/xml.html) +/// - [PyPI: `defusedxml`](https://pypi.org/project/defusedxml/) +/// - [Common Weakness Enumeration: CWE-400](https://cwe.mitre.org/data/definitions/400.html) +/// - [Common Weakness Enumeration: CWE-776](https://cwe.mitre.org/data/definitions/776.html) #[violation] pub struct SuspiciousXMLElementTreeUsage; @@ -129,6 +509,36 @@ impl Violation for SuspiciousXMLElementTreeUsage { } } +/// ## What it does +/// Checks for uses of insecure XML parsers. +/// +/// ## Why is this bad? +/// Many XML parsers are vulnerable to XML attacks (such as entity expansion), +/// which cause excessive memory and CPU usage by exploiting recursion. An +/// attacker could use such methods to access unauthorized resources. +/// +/// Consider using the `defusedxml` package when parsing untrusted XML data, +/// to protect against XML attacks. +/// +/// ## Example +/// ```python +/// from xml.sax.expatreader import create_parser +/// +/// parser = create_parser() +/// ``` +/// +/// Use instead: +/// ```python +/// from defusedxml.sax import create_parser +/// +/// parser = create_parser() +/// ``` +/// +/// ## References +/// - [Python documentation: `xml` — XML processing modules](https://docs.python.org/3/library/xml.html) +/// - [PyPI: `defusedxml`](https://pypi.org/project/defusedxml/) +/// - [Common Weakness Enumeration: CWE-400](https://cwe.mitre.org/data/definitions/400.html) +/// - [Common Weakness Enumeration: CWE-776](https://cwe.mitre.org/data/definitions/776.html) #[violation] pub struct SuspiciousXMLExpatReaderUsage; @@ -139,6 +549,36 @@ impl Violation for SuspiciousXMLExpatReaderUsage { } } +/// ## What it does +/// Checks for uses of insecure XML parsers. +/// +/// ## Why is this bad? +/// Many XML parsers are vulnerable to XML attacks (such as entity expansion), +/// which cause excessive memory and CPU usage by exploiting recursion. An +/// attacker could use such methods to access unauthorized resources. +/// +/// Consider using the `defusedxml` package when parsing untrusted XML data, +/// to protect against XML attacks. +/// +/// ## Example +/// ```python +/// from xml.dom.expatbuilder import parse +/// +/// parse("untrusted.xml") +/// ``` +/// +/// Use instead: +/// ```python +/// from defusedxml.expatbuilder import parse +/// +/// tree = parse("untrusted.xml") +/// ``` +/// +/// ## References +/// - [Python documentation: `xml` — XML processing modules](https://docs.python.org/3/library/xml.html) +/// - [PyPI: `defusedxml`](https://pypi.org/project/defusedxml/) +/// - [Common Weakness Enumeration: CWE-400](https://cwe.mitre.org/data/definitions/400.html) +/// - [Common Weakness Enumeration: CWE-776](https://cwe.mitre.org/data/definitions/776.html) #[violation] pub struct SuspiciousXMLExpatBuilderUsage; @@ -149,6 +589,36 @@ impl Violation for SuspiciousXMLExpatBuilderUsage { } } +/// ## What it does +/// Checks for uses of insecure XML parsers. +/// +/// ## Why is this bad? +/// Many XML parsers are vulnerable to XML attacks (such as entity expansion), +/// which cause excessive memory and CPU usage by exploiting recursion. An +/// attacker could use such methods to access unauthorized resources. +/// +/// Consider using the `defusedxml` package when parsing untrusted XML data, +/// to protect against XML attacks. +/// +/// ## Example +/// ```python +/// from xml.sax import make_parser +/// +/// make_parser() +/// ``` +/// +/// Use instead: +/// ```python +/// from defusedxml.sax import make_parser +/// +/// make_parser() +/// ``` +/// +/// ## References +/// - [Python documentation: `xml` — XML processing modules](https://docs.python.org/3/library/xml.html) +/// - [PyPI: `defusedxml`](https://pypi.org/project/defusedxml/) +/// - [Common Weakness Enumeration: CWE-400](https://cwe.mitre.org/data/definitions/400.html) +/// - [Common Weakness Enumeration: CWE-776](https://cwe.mitre.org/data/definitions/776.html) #[violation] pub struct SuspiciousXMLSaxUsage; @@ -159,6 +629,36 @@ impl Violation for SuspiciousXMLSaxUsage { } } +/// ## What it does +/// Checks for uses of insecure XML parsers. +/// +/// ## Why is this bad? +/// Many XML parsers are vulnerable to XML attacks (such as entity expansion), +/// which cause excessive memory and CPU usage by exploiting recursion. An +/// attacker could use such methods to access unauthorized resources. +/// +/// Consider using the `defusedxml` package when parsing untrusted XML data, +/// to protect against XML attacks. +/// +/// ## Example +/// ```python +/// from xml.dom.minidom import parse +/// +/// content = parse("untrusted.xml") +/// ``` +/// +/// Use instead: +/// ```python +/// from defusedxml.minidom import parse +/// +/// content = parse("untrusted.xml") +/// ``` +/// +/// ## References +/// - [Python documentation: `xml` — XML processing modules](https://docs.python.org/3/library/xml.html) +/// - [PyPI: `defusedxml`](https://pypi.org/project/defusedxml/) +/// - [Common Weakness Enumeration: CWE-400](https://cwe.mitre.org/data/definitions/400.html) +/// - [Common Weakness Enumeration: CWE-776](https://cwe.mitre.org/data/definitions/776.html) #[violation] pub struct SuspiciousXMLMiniDOMUsage; @@ -169,6 +669,36 @@ impl Violation for SuspiciousXMLMiniDOMUsage { } } +/// ## What it does +/// Checks for uses of insecure XML parsers. +/// +/// ## Why is this bad? +/// Many XML parsers are vulnerable to XML attacks (such as entity expansion), +/// which cause excessive memory and CPU usage by exploiting recursion. An +/// attacker could use such methods to access unauthorized resources. +/// +/// Consider using the `defusedxml` package when parsing untrusted XML data, +/// to protect against XML attacks. +/// +/// ## Example +/// ```python +/// from xml.dom.pulldom import parse +/// +/// content = parse("untrusted.xml") +/// ``` +/// +/// Use instead: +/// ```python +/// from defusedxml.pulldom import parse +/// +/// content = parse("untrusted.xml") +/// ``` +/// +/// ## References +/// - [Python documentation: `xml` — XML processing modules](https://docs.python.org/3/library/xml.html) +/// - [PyPI: `defusedxml`](https://pypi.org/project/defusedxml/) +/// - [Common Weakness Enumeration: CWE-400](https://cwe.mitre.org/data/definitions/400.html) +/// - [Common Weakness Enumeration: CWE-776](https://cwe.mitre.org/data/definitions/776.html) #[violation] pub struct SuspiciousXMLPullDOMUsage; @@ -179,16 +709,68 @@ impl Violation for SuspiciousXMLPullDOMUsage { } } +/// ## What it does +/// Checks for uses of insecure XML parsers. +/// +/// ## Why is this bad? +/// Many XML parsers are vulnerable to XML attacks (such as entity expansion), +/// which cause excessive memory and CPU usage by exploiting recursion. An +/// attacker could use such methods to access unauthorized resources. +/// +/// ## Example +/// ```python +/// from lxml import etree +/// +/// content = etree.parse("untrusted.xml") +/// ``` +/// +/// ## References +/// - [PyPI: `lxml`](https://pypi.org/project/lxml/) +/// - [Common Weakness Enumeration: CWE-400](https://cwe.mitre.org/data/definitions/400.html) +/// - [Common Weakness Enumeration: CWE-776](https://cwe.mitre.org/data/definitions/776.html) #[violation] pub struct SuspiciousXMLETreeUsage; impl Violation for SuspiciousXMLETreeUsage { #[derive_message_formats] fn message(&self) -> String { - format!("Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; use `defusedxml` equivalents") + format!("Using `lxml` to parse untrusted data is known to be vulnerable to XML attacks") } } +/// ## What it does +/// Checks for uses of `ssl._create_unverified_context`. +/// +/// ## Why is this bad? +/// [PEP 476] enabled certificate and hostname validation by default in Python +/// standard library HTTP clients. Previously, Python did not validate +/// certificates by default, which could allow an attacker to perform a "man in +/// the middle" attack by intercepting and modifying traffic between client and +/// server. +/// +/// To support legacy environments, `ssl._create_unverified_context` reverts to +/// the previous behavior that does perform verification. Otherwise, use +/// `ssl.create_default_context` to create a secure context. +/// +/// ## Example +/// ```python +/// import ssl +/// +/// context = ssl._create_unverified_context() +/// ``` +/// +/// Use instead: +/// ```python +/// import ssl +/// +/// context = ssl.create_default_context() +/// ``` +/// +/// ## References +/// - [PEP 476 – Enabling certificate verification by default for stdlib http clients: Opting out](https://peps.python.org/pep-0476/#opting-out) +/// - [Python documentation: `ssl` — TLS/SSL wrapper for socket objects](https://docs.python.org/3/library/ssl.html) +/// +/// [PEP 476]: https://peps.python.org/pep-0476/ #[violation] pub struct SuspiciousUnverifiedContextUsage; @@ -199,6 +781,17 @@ impl Violation for SuspiciousUnverifiedContextUsage { } } +/// ## What it does +/// Checks for the use of Telnet-related functions. +/// +/// ## Why is this bad? +/// Telnet is considered insecure because it does not encrypt data sent over +/// the connection and is vulnerable to numerous attacks. +/// +/// Instead, consider using a more secure protocol such as SSH. +/// +/// ## References +/// - [Python documentation: `telnetlib` — Telnet client](https://docs.python.org/3/library/telnetlib.html) #[violation] pub struct SuspiciousTelnetUsage; @@ -209,6 +802,17 @@ impl Violation for SuspiciousTelnetUsage { } } +/// ## What it does +/// Checks for the use of FTP-related functions. +/// +/// ## Why is this bad? +/// FTP is considered insecure as it does not encrypt data sent over the +/// connection and is thus vulnerable to numerous attacks. +/// +/// Instead, consider using FTPS (which secures FTP using SSL/TLS) or SFTP. +/// +/// ## References +/// - [Python documentation: `ftplib` — FTP protocol client](https://docs.python.org/3/library/ftplib.html) #[violation] pub struct SuspiciousFTPLibUsage; @@ -219,298 +823,70 @@ impl Violation for SuspiciousFTPLibUsage { } } -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub(crate) enum Reason { - Pickle, - Marshal, - InsecureHash, - InsecureCipher, - InsecureCipherMode, - Mktemp, - Eval, - MarkSafe, - URLOpen, - NonCryptographicRandom, - XMLCElementTree, - XMLElementTree, - XMLExpatReader, - XMLExpatBuilder, - XMLSax, - XMLMiniDOM, - XMLPullDOM, - XMLETree, - UnverifiedContext, - Telnet, - FTPLib, -} - -struct SuspiciousMembers<'a> { - members: &'a [&'a [&'a str]], - reason: Reason, -} - -impl<'a> SuspiciousMembers<'a> { - pub(crate) const fn new(members: &'a [&'a [&'a str]], reason: Reason) -> Self { - Self { members, reason } - } -} - -struct SuspiciousModule<'a> { - name: &'a str, - reason: Reason, -} - -impl<'a> SuspiciousModule<'a> { - pub(crate) const fn new(name: &'a str, reason: Reason) -> Self { - Self { name, reason } - } -} - -const SUSPICIOUS_MEMBERS: &[SuspiciousMembers] = &[ - SuspiciousMembers::new( - &[ - &["pickle", "loads"], - &["pickle", "load"], - &["pickle", "Unpickler"], - &["dill", "loads"], - &["dill", "load"], - &["dill", "Unpickler"], - &["shelve", "open"], - &["shelve", "DbfilenameShelf"], - &["jsonpickle", "decode"], - &["jsonpickle", "unpickler", "decode"], - &["pandas", "read_pickle"], - ], - Reason::Pickle, - ), - SuspiciousMembers::new( - &[&["marshal", "loads"], &["marshal", "load"]], - Reason::Marshal, - ), - SuspiciousMembers::new( - &[ - &["Crypto", "Hash", "MD5", "new"], - &["Crypto", "Hash", "MD4", "new"], - &["Crypto", "Hash", "MD3", "new"], - &["Crypto", "Hash", "MD2", "new"], - &["Crypto", "Hash", "SHA", "new"], - &["Cryptodome", "Hash", "MD5", "new"], - &["Cryptodome", "Hash", "MD4", "new"], - &["Cryptodome", "Hash", "MD3", "new"], - &["Cryptodome", "Hash", "MD2", "new"], - &["Cryptodome", "Hash", "SHA", "new"], - &["cryptography", "hazmat", "primitives", "hashes", "MD5"], - &["cryptography", "hazmat", "primitives", "hashes", "SHA1"], - ], - Reason::InsecureHash, - ), - SuspiciousMembers::new( - &[ - &["Crypto", "Cipher", "ARC2", "new"], - &["Crypto", "Cipher", "ARC2", "new"], - &["Crypto", "Cipher", "Blowfish", "new"], - &["Crypto", "Cipher", "DES", "new"], - &["Crypto", "Cipher", "XOR", "new"], - &["Cryptodome", "Cipher", "ARC2", "new"], - &["Cryptodome", "Cipher", "ARC2", "new"], - &["Cryptodome", "Cipher", "Blowfish", "new"], - &["Cryptodome", "Cipher", "DES", "new"], - &["Cryptodome", "Cipher", "XOR", "new"], - &[ - "cryptography", - "hazmat", - "primitives", - "ciphers", - "algorithms", - "ARC4", - ], - &[ - "cryptography", - "hazmat", - "primitives", - "ciphers", - "algorithms", - "Blowfish", - ], - &[ - "cryptography", - "hazmat", - "primitives", - "ciphers", - "algorithms", - "IDEA", - ], - ], - Reason::InsecureCipher, - ), - SuspiciousMembers::new( - &[&[ - "cryptography", - "hazmat", - "primitives", - "ciphers", - "modes", - "ECB", - ]], - Reason::InsecureCipherMode, - ), - SuspiciousMembers::new(&[&["tempfile", "mktemp"]], Reason::Mktemp), - SuspiciousMembers::new(&[&["eval"]], Reason::Eval), - SuspiciousMembers::new( - &[&["django", "utils", "safestring", "mark_safe"]], - Reason::MarkSafe, - ), - SuspiciousMembers::new( - &[ - &["urllib", "urlopen"], - &["urllib", "request", "urlopen"], - &["urllib", "urlretrieve"], - &["urllib", "request", "urlretrieve"], - &["urllib", "URLopener"], - &["urllib", "request", "URLopener"], - &["urllib", "FancyURLopener"], - &["urllib", "request", "FancyURLopener"], - &["urllib2", "urlopen"], - &["urllib2", "Request"], - &["six", "moves", "urllib", "request", "urlopen"], - &["six", "moves", "urllib", "request", "urlretrieve"], - &["six", "moves", "urllib", "request", "URLopener"], - &["six", "moves", "urllib", "request", "FancyURLopener"], - ], - Reason::URLOpen, - ), - SuspiciousMembers::new( - &[ - &["random", "random"], - &["random", "randrange"], - &["random", "randint"], - &["random", "choice"], - &["random", "choices"], - &["random", "uniform"], - &["random", "triangular"], - ], - Reason::NonCryptographicRandom, - ), - SuspiciousMembers::new( - &[&["ssl", "_create_unverified_context"]], - Reason::UnverifiedContext, - ), - SuspiciousMembers::new( - &[ - &["xml", "etree", "cElementTree", "parse"], - &["xml", "etree", "cElementTree", "iterparse"], - &["xml", "etree", "cElementTree", "fromstring"], - &["xml", "etree", "cElementTree", "XMLParser"], - ], - Reason::XMLCElementTree, - ), - SuspiciousMembers::new( - &[ - &["xml", "etree", "ElementTree", "parse"], - &["xml", "etree", "ElementTree", "iterparse"], - &["xml", "etree", "ElementTree", "fromstring"], - &["xml", "etree", "ElementTree", "XMLParser"], - ], - Reason::XMLElementTree, - ), - SuspiciousMembers::new( - &[&["xml", "sax", "expatreader", "create_parser"]], - Reason::XMLExpatReader, - ), - SuspiciousMembers::new( - &[ - &["xml", "dom", "expatbuilder", "parse"], - &["xml", "dom", "expatbuilder", "parseString"], - ], - Reason::XMLExpatBuilder, - ), - SuspiciousMembers::new( - &[ - &["xml", "sax", "parse"], - &["xml", "sax", "parseString"], - &["xml", "sax", "make_parser"], - ], - Reason::XMLSax, - ), - SuspiciousMembers::new( - &[ - &["xml", "dom", "minidom", "parse"], - &["xml", "dom", "minidom", "parseString"], - ], - Reason::XMLMiniDOM, - ), - SuspiciousMembers::new( - &[ - &["xml", "dom", "pulldom", "parse"], - &["xml", "dom", "pulldom", "parseString"], - ], - Reason::XMLPullDOM, - ), - SuspiciousMembers::new( - &[ - &["lxml", "etree", "parse"], - &["lxml", "etree", "fromstring"], - &["lxml", "etree", "RestrictedElement"], - &["lxml", "etree", "GlobalParserTLS"], - &["lxml", "etree", "getDefaultParser"], - &["lxml", "etree", "check_docinfo"], - ], - Reason::XMLETree, - ), -]; - -const SUSPICIOUS_MODULES: &[SuspiciousModule] = &[ - SuspiciousModule::new("telnetlib", Reason::Telnet), - SuspiciousModule::new("ftplib", Reason::FTPLib), -]; - -/// S001 +/// S301, S302, S303, S304, S305, S306, S307, S308, S310, S311, S312, S313, S314, S315, S316, S317, S318, S319, S320, S321, S323 pub(crate) fn suspicious_function_call(checker: &mut Checker, expr: &Expr) { let Expr::Call(ast::ExprCall { func, .. }) = expr else { return; }; - let Some(reason) = checker.semantic_model().resolve_call_path(func).and_then(|call_path| { - for module in SUSPICIOUS_MEMBERS { - for member in module.members { - if call_path.as_slice() == *member { - return Some(module.reason); - } - } + let Some(diagnostic_kind) = checker.semantic().resolve_call_path(func).and_then(|call_path| { + match call_path.as_slice() { + // Pickle + ["pickle" | "dill", "load" | "loads" | "Unpickler"] | + ["shelve", "open" | "DbfilenameShelf"] | + ["jsonpickle", "decode"] | + ["jsonpickle", "unpickler", "decode"] | + ["pandas", "read_pickle"] => Some(SuspiciousPickleUsage.into()), + // Marshal + ["marshal", "load" | "loads"] => Some(SuspiciousMarshalUsage.into()), + // InsecureHash + ["Crypto" | "Cryptodome", "Hash", "SHA" | "MD2" | "MD3" | "MD4" | "MD5", "new"] | + ["cryptography", "hazmat", "primitives", "hashes", "SHA1" | "MD5"] => Some(SuspiciousInsecureHashUsage.into()), + // InsecureCipher + ["Crypto" | "Cryptodome", "Cipher", "ARC2" | "Blowfish" | "DES" | "XOR", "new"] | + ["cryptography", "hazmat", "primitives", "ciphers", "algorithms", "ARC4" | "Blowfish" | "IDEA" ] => Some(SuspiciousInsecureCipherUsage.into()), + // InsecureCipherMode + ["cryptography", "hazmat", "primitives", "ciphers", "modes", "ECB"] => Some(SuspiciousInsecureCipherModeUsage.into()), + // Mktemp + ["tempfile", "mktemp"] => Some(SuspiciousMktempUsage.into()), + // Eval + ["" | "builtins", "eval"] => Some(SuspiciousEvalUsage.into()), + // MarkSafe + ["django", "utils", "safestring", "mark_safe"] => Some(SuspiciousMarkSafeUsage.into()), + // URLOpen + ["urllib", "urlopen" | "urlretrieve" | "URLopener" | "FancyURLopener" | "Request"] | + ["urllib", "request", "urlopen" | "urlretrieve" | "URLopener" | "FancyURLopener"] | + ["six", "moves", "urllib", "request", "urlopen" | "urlretrieve" | "URLopener" | "FancyURLopener"] => Some(SuspiciousURLOpenUsage.into()), + // NonCryptographicRandom + ["random", "random" | "randrange" | "randint" | "choice" | "choices" | "uniform" | "triangular"] => Some(SuspiciousNonCryptographicRandomUsage.into()), + // UnverifiedContext + ["ssl", "_create_unverified_context"] => Some(SuspiciousUnverifiedContextUsage.into()), + // XMLCElementTree + ["xml", "etree", "cElementTree", "parse" | "iterparse" | "fromstring" | "XMLParser"] => Some(SuspiciousXMLCElementTreeUsage.into()), + // XMLElementTree + ["xml", "etree", "ElementTree", "parse" | "iterparse" | "fromstring" | "XMLParser"] => Some(SuspiciousXMLElementTreeUsage.into()), + // XMLExpatReader + ["xml", "sax", "expatreader", "create_parser"] => Some(SuspiciousXMLExpatReaderUsage.into()), + // XMLExpatBuilder + ["xml", "dom", "expatbuilder", "parse" | "parseString"] => Some(SuspiciousXMLExpatBuilderUsage.into()), + // XMLSax + ["xml", "sax", "parse" | "parseString" | "make_parser"] => Some(SuspiciousXMLSaxUsage.into()), + // XMLMiniDOM + ["xml", "dom", "minidom", "parse" | "parseString"] => Some(SuspiciousXMLMiniDOMUsage.into()), + // XMLPullDOM + ["xml", "dom", "pulldom", "parse" | "parseString"] => Some(SuspiciousXMLPullDOMUsage.into()), + // XMLETree + ["lxml", "etree", "parse" | "fromstring" | "RestrictedElement" | "GlobalParserTLS" | "getDefaultParser" | "check_docinfo"] => Some(SuspiciousXMLETreeUsage.into()), + // Telnet + ["telnetlib", ..] => Some(SuspiciousTelnetUsage.into()), + // FTPLib + ["ftplib", ..] => Some(SuspiciousFTPLibUsage.into()), + _ => None } - for module in SUSPICIOUS_MODULES { - if call_path.first() == Some(&module.name) { - return Some(module.reason); - } - } - None }) else { return; }; - let diagnostic_kind = match reason { - Reason::Pickle => SuspiciousPickleUsage.into(), - Reason::Marshal => SuspiciousMarshalUsage.into(), - Reason::InsecureHash => SuspiciousInsecureHashUsage.into(), - Reason::InsecureCipher => SuspiciousInsecureCipherUsage.into(), - Reason::InsecureCipherMode => SuspiciousInsecureCipherModeUsage.into(), - Reason::Mktemp => SuspiciousMktempUsage.into(), - Reason::Eval => SuspiciousEvalUsage.into(), - Reason::MarkSafe => SuspiciousMarkSafeUsage.into(), - Reason::URLOpen => SuspiciousURLOpenUsage.into(), - Reason::NonCryptographicRandom => SuspiciousNonCryptographicRandomUsage.into(), - Reason::XMLCElementTree => SuspiciousXMLCElementTreeUsage.into(), - Reason::XMLElementTree => SuspiciousXMLElementTreeUsage.into(), - Reason::XMLExpatReader => SuspiciousXMLExpatReaderUsage.into(), - Reason::XMLExpatBuilder => SuspiciousXMLExpatBuilderUsage.into(), - Reason::XMLSax => SuspiciousXMLSaxUsage.into(), - Reason::XMLMiniDOM => SuspiciousXMLMiniDOMUsage.into(), - Reason::XMLPullDOM => SuspiciousXMLPullDOMUsage.into(), - Reason::XMLETree => SuspiciousXMLETreeUsage.into(), - Reason::UnverifiedContext => SuspiciousUnverifiedContextUsage.into(), - Reason::Telnet => SuspiciousTelnetUsage.into(), - Reason::FTPLib => SuspiciousFTPLibUsage.into(), - }; let diagnostic = Diagnostic::new::(diagnostic_kind, expr.range()); if checker.enabled(diagnostic.kind.rule()) { checker.diagnostics.push(diagnostic); diff --git a/crates/ruff/src/rules/flake8_bandit/rules/try_except_continue.rs b/crates/ruff/src/rules/flake8_bandit/rules/try_except_continue.rs index a64c33bc39..82cc379cfb 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/try_except_continue.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/try_except_continue.rs @@ -1,4 +1,4 @@ -use rustpython_parser::ast::{Excepthandler, Expr, Ranged, Stmt}; +use rustpython_parser::ast::{ExceptHandler, Expr, Ranged, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -6,6 +6,40 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; use crate::rules::flake8_bandit::helpers::is_untyped_exception; +/// ## What it does +/// Checks for uses of the `try`-`except`-`continue` pattern. +/// +/// ## Why is this bad? +/// The `try`-`except`-`continue` pattern suppresses all exceptions. +/// Suppressing exceptions may hide errors that could otherwise reveal +/// unexpected behavior, security vulnerabilities, or malicious activity. +/// Instead, consider logging the exception. +/// +/// ## Example +/// ```python +/// import logging +/// +/// while predicate: +/// try: +/// ... +/// except Exception: +/// continue +/// ``` +/// +/// Use instead: +/// ```python +/// import logging +/// +/// while predicate: +/// try: +/// ... +/// except Exception as exc: +/// logging.exception("Error occurred") +/// ``` +/// +/// ## References +/// - [Common Weakness Enumeration: CWE-703](https://cwe.mitre.org/data/definitions/703.html) +/// - [Python documentation: `logging`](https://docs.python.org/3/library/logging.html) #[violation] pub struct TryExceptContinue; @@ -19,7 +53,7 @@ impl Violation for TryExceptContinue { /// S112 pub(crate) fn try_except_continue( checker: &mut Checker, - excepthandler: &Excepthandler, + except_handler: &ExceptHandler, type_: Option<&Expr>, _name: Option<&str>, body: &[Stmt], @@ -27,10 +61,10 @@ pub(crate) fn try_except_continue( ) { if body.len() == 1 && body[0].is_continue_stmt() - && (check_typed_exception || is_untyped_exception(type_, checker.semantic_model())) + && (check_typed_exception || is_untyped_exception(type_, checker.semantic())) { checker .diagnostics - .push(Diagnostic::new(TryExceptContinue, excepthandler.range())); + .push(Diagnostic::new(TryExceptContinue, except_handler.range())); } } diff --git a/crates/ruff/src/rules/flake8_bandit/rules/try_except_pass.rs b/crates/ruff/src/rules/flake8_bandit/rules/try_except_pass.rs index c740399349..ba4e1d713d 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/try_except_pass.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/try_except_pass.rs @@ -1,4 +1,4 @@ -use rustpython_parser::ast::{Excepthandler, Expr, Ranged, Stmt}; +use rustpython_parser::ast::{ExceptHandler, Expr, Ranged, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -6,6 +6,36 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; use crate::rules::flake8_bandit::helpers::is_untyped_exception; +/// ## What it does +/// Checks for uses of the `try`-`except`-`pass` pattern. +/// +/// ## Why is this bad? +/// The `try`-`except`-`pass` pattern suppresses all exceptions. Suppressing +/// exceptions may hide errors that could otherwise reveal unexpected behavior, +/// security vulnerabilities, or malicious activity. Instead, consider logging +/// the exception. +/// +/// ## Example +/// ```python +/// try: +/// ... +/// except Exception: +/// pass +/// ``` +/// +/// Use instead: +/// ```python +/// import logging +/// +/// try: +/// ... +/// except Exception as exc: +/// logging.exception("Exception occurred") +/// ``` +/// +/// ## References +/// - [Common Weakness Enumeration: CWE-703](https://cwe.mitre.org/data/definitions/703.html) +/// - [Python documentation: `logging`](https://docs.python.org/3/library/logging.html) #[violation] pub struct TryExceptPass; @@ -19,7 +49,7 @@ impl Violation for TryExceptPass { /// S110 pub(crate) fn try_except_pass( checker: &mut Checker, - excepthandler: &Excepthandler, + except_handler: &ExceptHandler, type_: Option<&Expr>, _name: Option<&str>, body: &[Stmt], @@ -27,10 +57,10 @@ pub(crate) fn try_except_pass( ) { if body.len() == 1 && body[0].is_pass_stmt() - && (check_typed_exception || is_untyped_exception(type_, checker.semantic_model())) + && (check_typed_exception || is_untyped_exception(type_, checker.semantic())) { checker .diagnostics - .push(Diagnostic::new(TryExceptPass, excepthandler.range())); + .push(Diagnostic::new(TryExceptPass, except_handler.range())); } } diff --git a/crates/ruff/src/rules/flake8_bandit/rules/unsafe_yaml_load.rs b/crates/ruff/src/rules/flake8_bandit/rules/unsafe_yaml_load.rs index e5225eb0d4..9d5274e523 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/unsafe_yaml_load.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/unsafe_yaml_load.rs @@ -38,18 +38,19 @@ pub(crate) fn unsafe_yaml_load( keywords: &[Keyword], ) { if checker - .semantic_model() + .semantic() .resolve_call_path(func) - .map_or(false, |call_path| call_path.as_slice() == ["yaml", "load"]) + .map_or(false, |call_path| { + matches!(call_path.as_slice(), ["yaml", "load"]) + }) { let call_args = SimpleCallArgs::new(args, keywords); if let Some(loader_arg) = call_args.argument("Loader", 1) { if !checker - .semantic_model() + .semantic() .resolve_call_path(loader_arg) .map_or(false, |call_path| { - call_path.as_slice() == ["yaml", "SafeLoader"] - || call_path.as_slice() == ["yaml", "CSafeLoader"] + matches!(call_path.as_slice(), ["yaml", "SafeLoader" | "CSafeLoader"]) }) { let loader = match loader_arg { diff --git a/crates/ruff/src/rules/flake8_bandit/settings.rs b/crates/ruff/src/rules/flake8_bandit/settings.rs index d43bd48700..168feeec14 100644 --- a/crates/ruff/src/rules/flake8_bandit/settings.rs +++ b/crates/ruff/src/rules/flake8_bandit/settings.rs @@ -59,12 +59,7 @@ impl From for Settings { .hardcoded_tmp_directory .unwrap_or_else(default_tmp_dirs) .into_iter() - .chain( - options - .hardcoded_tmp_directory_extend - .unwrap_or_default() - .into_iter(), - ) + .chain(options.hardcoded_tmp_directory_extend.unwrap_or_default()) .collect(), check_typed_exception: options.check_typed_exception.unwrap_or(false), } diff --git a/crates/ruff/src/rules/flake8_bandit/snapshots/ruff__rules__flake8_bandit__tests__S102_S102.py.snap b/crates/ruff/src/rules/flake8_bandit/snapshots/ruff__rules__flake8_bandit__tests__S102_S102.py.snap index ccf9572377..075092ceda 100644 --- a/crates/ruff/src/rules/flake8_bandit/snapshots/ruff__rules__flake8_bandit__tests__S102_S102.py.snap +++ b/crates/ruff/src/rules/flake8_bandit/snapshots/ruff__rules__flake8_bandit__tests__S102_S102.py.snap @@ -6,7 +6,7 @@ S102.py:3:5: S102 Use of `exec` detected 1 | def fn(): 2 | # Error 3 | exec('x = 2') - | ^^^^^^^^^^^^^ S102 + | ^^^^ S102 4 | 5 | exec('y = 3') | @@ -16,7 +16,7 @@ S102.py:5:1: S102 Use of `exec` detected 3 | exec('x = 2') 4 | 5 | exec('y = 3') - | ^^^^^^^^^^^^^ S102 + | ^^^^ S102 | diff --git a/crates/ruff/src/rules/flake8_bandit/snapshots/ruff__rules__flake8_bandit__tests__S307_S307.py.snap b/crates/ruff/src/rules/flake8_bandit/snapshots/ruff__rules__flake8_bandit__tests__S307_S307.py.snap new file mode 100644 index 0000000000..f5c6ac82d8 --- /dev/null +++ b/crates/ruff/src/rules/flake8_bandit/snapshots/ruff__rules__flake8_bandit__tests__S307_S307.py.snap @@ -0,0 +1,20 @@ +--- +source: crates/ruff/src/rules/flake8_bandit/mod.rs +--- +S307.py:3:7: S307 Use of possibly insecure function; consider using `ast.literal_eval` + | +1 | import os +2 | +3 | print(eval("1+1")) # S307 + | ^^^^^^^^^^^ S307 +4 | print(eval("os.getcwd()")) # S307 + | + +S307.py:4:7: S307 Use of possibly insecure function; consider using `ast.literal_eval` + | +3 | print(eval("1+1")) # S307 +4 | print(eval("os.getcwd()")) # S307 + | ^^^^^^^^^^^^^^^^^^^ S307 + | + + diff --git a/crates/ruff/src/rules/flake8_blind_except/rules/blind_except.rs b/crates/ruff/src/rules/flake8_blind_except/rules/blind_except.rs index ef4f52524e..f0650afdda 100644 --- a/crates/ruff/src/rules/flake8_blind_except/rules/blind_except.rs +++ b/crates/ruff/src/rules/flake8_blind_except/rules/blind_except.rs @@ -7,6 +7,36 @@ use ruff_python_semantic::analyze::logging; use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for `except` clauses that catch all exceptions. +/// +/// ## Why is this bad? +/// Overly broad `except` clauses can lead to unexpected behavior, such as +/// catching `KeyboardInterrupt` or `SystemExit` exceptions that prevent the +/// user from exiting the program. +/// +/// Instead of catching all exceptions, catch only those that are expected to +/// be raised in the `try` block. +/// +/// ## Example +/// ```python +/// try: +/// foo() +/// except BaseException: +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// try: +/// foo() +/// except FileNotFoundError: +/// ... +/// ``` +/// +/// ## References +/// - [Python documentation: The `try` statement](https://docs.python.org/3/reference/compound_stmts.html#the-try-statement) +/// - [Python documentation: Exception hierarchy](https://docs.python.org/3/library/exceptions.html#exception-hierarchy) #[violation] pub struct BlindExcept { name: String, @@ -33,59 +63,64 @@ pub(crate) fn blind_except( let Expr::Name(ast::ExprName { id, .. }) = &type_ else { return; }; - for exception in ["BaseException", "Exception"] { - if id == exception && checker.semantic_model().is_builtin(exception) { - // If the exception is re-raised, don't flag an error. - if body.iter().any(|stmt| { - if let Stmt::Raise(ast::StmtRaise { exc, .. }) = stmt { - if let Some(exc) = exc { - if let Expr::Name(ast::ExprName { id, .. }) = exc.as_ref() { - name.map_or(false, |name| id == name) - } else { - false - } - } else { - true - } + + if !matches!(id.as_str(), "BaseException" | "Exception") { + return; + } + + if !checker.semantic().is_builtin(id) { + return; + } + + // If the exception is re-raised, don't flag an error. + if body.iter().any(|stmt| { + if let Stmt::Raise(ast::StmtRaise { exc, .. }) = stmt { + if let Some(exc) = exc { + if let Expr::Name(ast::ExprName { id, .. }) = exc.as_ref() { + name.map_or(false, |name| id == name) } else { false } - }) { - continue; + } else { + true } + } else { + false + } + }) { + return; + } - // If the exception is logged, don't flag an error. - if body.iter().any(|stmt| { - if let Stmt::Expr(ast::StmtExpr { value, range: _ }) = stmt { - if let Expr::Call(ast::ExprCall { func, keywords, .. }) = value.as_ref() { - if logging::is_logger_candidate(func, checker.semantic_model()) { - if let Some(attribute) = func.as_attribute_expr() { - let attr = attribute.attr.as_str(); - if attr == "exception" { + // If the exception is logged, don't flag an error. + if body.iter().any(|stmt| { + if let Stmt::Expr(ast::StmtExpr { value, range: _ }) = stmt { + if let Expr::Call(ast::ExprCall { func, keywords, .. }) = value.as_ref() { + if logging::is_logger_candidate(func, checker.semantic()) { + if let Some(attribute) = func.as_attribute_expr() { + let attr = attribute.attr.as_str(); + if attr == "exception" { + return true; + } + if attr == "error" { + if let Some(keyword) = find_keyword(keywords, "exc_info") { + if is_const_true(&keyword.value) { return true; } - if attr == "error" { - if let Some(keyword) = find_keyword(keywords, "exc_info") { - if is_const_true(&keyword.value) { - return true; - } - } - } } } } } - false - }) { - continue; } - - checker.diagnostics.push(Diagnostic::new( - BlindExcept { - name: id.to_string(), - }, - type_.range(), - )); } + false + }) { + return; } + + checker.diagnostics.push(Diagnostic::new( + BlindExcept { + name: id.to_string(), + }, + type_.range(), + )); } diff --git a/crates/ruff/src/rules/flake8_blind_except/rules/mod.rs b/crates/ruff/src/rules/flake8_blind_except/rules/mod.rs index 520b3ece06..555850e95a 100644 --- a/crates/ruff/src/rules/flake8_blind_except/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_blind_except/rules/mod.rs @@ -1,3 +1,3 @@ -pub(crate) use blind_except::{blind_except, BlindExcept}; +pub(crate) use blind_except::*; mod blind_except; diff --git a/crates/ruff/src/rules/flake8_boolean_trap/helpers.rs b/crates/ruff/src/rules/flake8_boolean_trap/helpers.rs index 2c397470b7..01cc630de6 100644 --- a/crates/ruff/src/rules/flake8_boolean_trap/helpers.rs +++ b/crates/ruff/src/rules/flake8_boolean_trap/helpers.rs @@ -4,50 +4,57 @@ use ruff_diagnostics::{Diagnostic, DiagnosticKind}; use crate::checkers::ast::Checker; -pub(super) const FUNC_CALL_NAME_ALLOWLIST: &[&str] = &[ - "append", - "assertEqual", - "assertEquals", - "assertNotEqual", - "assertNotEquals", - "bool", - "bytes", - "count", - "failIfEqual", - "failUnlessEqual", - "float", - "fromkeys", - "get", - "getattr", - "getboolean", - "getfloat", - "getint", - "index", - "insert", - "int", - "param", - "pop", - "remove", - "set_blocking", - "set_enabled", - "setattr", - "__setattr__", - "setdefault", - "str", -]; +/// Returns `true` if a function call is allowed to use a boolean trap. +pub(super) fn is_allowed_func_call(name: &str) -> bool { + matches!( + name, + "append" + | "assertEqual" + | "assertEquals" + | "assertNotEqual" + | "assertNotEquals" + | "bool" + | "bytes" + | "count" + | "failIfEqual" + | "failUnlessEqual" + | "float" + | "fromkeys" + | "get" + | "getattr" + | "getboolean" + | "getfloat" + | "getint" + | "index" + | "insert" + | "int" + | "param" + | "pop" + | "remove" + | "set_blocking" + | "set_enabled" + | "setattr" + | "__setattr__" + | "setdefault" + | "str" + ) +} -pub(super) const FUNC_DEF_NAME_ALLOWLIST: &[&str] = &["__setitem__"]; +/// Returns `true` if a function definition is allowed to use a boolean trap. +pub(super) fn is_allowed_func_def(name: &str) -> bool { + matches!(name, "__setitem__") +} /// Returns `true` if an argument is allowed to use a boolean trap. To return /// `true`, the function name must be explicitly allowed, and the argument must /// be either the first or second argument in the call. pub(super) fn allow_boolean_trap(func: &Expr) -> bool { if let Expr::Attribute(ast::ExprAttribute { attr, .. }) = func { - return FUNC_CALL_NAME_ALLOWLIST.contains(&attr.as_ref()); + return is_allowed_func_call(attr); } if let Expr::Name(ast::ExprName { id, .. }) = func { - return FUNC_CALL_NAME_ALLOWLIST.contains(&id.as_ref()); + return is_allowed_func_call(id); } false diff --git a/crates/ruff/src/rules/flake8_boolean_trap/rules/check_boolean_default_value_in_function_definition.rs b/crates/ruff/src/rules/flake8_boolean_trap/rules/check_boolean_default_value_in_function_definition.rs index 4295b60d54..1e853abb87 100644 --- a/crates/ruff/src/rules/flake8_boolean_trap/rules/check_boolean_default_value_in_function_definition.rs +++ b/crates/ruff/src/rules/flake8_boolean_trap/rules/check_boolean_default_value_in_function_definition.rs @@ -1,14 +1,11 @@ -use rustpython_parser::ast::{Arguments, Decorator}; +use rustpython_parser::ast::{ArgWithDefault, Arguments, Decorator}; use ruff_diagnostics::Violation; - use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::call_path::collect_call_path; use crate::checkers::ast::Checker; -use crate::rules::flake8_boolean_trap::helpers::add_if_boolean; - -use super::super::helpers::FUNC_DEF_NAME_ALLOWLIST; +use crate::rules::flake8_boolean_trap::helpers::{add_if_boolean, is_allowed_func_def}; /// ## What it does /// Checks for the use of booleans as default values in function definitions. @@ -46,7 +43,7 @@ use super::super::helpers::FUNC_DEF_NAME_ALLOWLIST; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/reference/expressions.html#calls) +/// - [Python documentation: Calls](https://docs.python.org/3/reference/expressions.html#calls) /// - [_How to Avoid “The Boolean Trap”_ by Adam Johnson](https://adamj.eu/tech/2021/07/10/python-type-hints-how-to-avoid-the-boolean-trap/) #[violation] pub struct BooleanDefaultValueInFunctionDefinition; @@ -64,7 +61,7 @@ pub(crate) fn check_boolean_default_value_in_function_definition( decorator_list: &[Decorator], arguments: &Arguments, ) { - if FUNC_DEF_NAME_ALLOWLIST.contains(&name) { + if is_allowed_func_def(name) { return; } @@ -75,7 +72,19 @@ pub(crate) fn check_boolean_default_value_in_function_definition( return; } - for arg in &arguments.defaults { - add_if_boolean(checker, arg, BooleanDefaultValueInFunctionDefinition.into()); + for ArgWithDefault { + def: _, + default, + range: _, + } in arguments.args.iter().chain(&arguments.posonlyargs) + { + let Some(default) = default else { + continue; + }; + add_if_boolean( + checker, + default, + BooleanDefaultValueInFunctionDefinition.into(), + ); } } diff --git a/crates/ruff/src/rules/flake8_boolean_trap/rules/check_boolean_positional_value_in_function_call.rs b/crates/ruff/src/rules/flake8_boolean_trap/rules/check_boolean_positional_value_in_function_call.rs index 5b39f0f24a..95b84c8b9c 100644 --- a/crates/ruff/src/rules/flake8_boolean_trap/rules/check_boolean_positional_value_in_function_call.rs +++ b/crates/ruff/src/rules/flake8_boolean_trap/rules/check_boolean_positional_value_in_function_call.rs @@ -34,7 +34,7 @@ use crate::rules::flake8_boolean_trap::helpers::{add_if_boolean, allow_boolean_t /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/reference/expressions.html#calls) +/// - [Python documentation: Calls](https://docs.python.org/3/reference/expressions.html#calls) /// - [_How to Avoid “The Boolean Trap”_ by Adam Johnson](https://adamj.eu/tech/2021/07/10/python-type-hints-how-to-avoid-the-boolean-trap/) #[violation] pub struct BooleanPositionalValueInFunctionCall; diff --git a/crates/ruff/src/rules/flake8_boolean_trap/rules/check_positional_boolean_in_def.rs b/crates/ruff/src/rules/flake8_boolean_trap/rules/check_positional_boolean_in_def.rs index 7d8ed6f574..68dc43238f 100644 --- a/crates/ruff/src/rules/flake8_boolean_trap/rules/check_positional_boolean_in_def.rs +++ b/crates/ruff/src/rules/flake8_boolean_trap/rules/check_positional_boolean_in_def.rs @@ -1,4 +1,4 @@ -use rustpython_parser::ast::{self, Arguments, Constant, Decorator, Expr, Ranged}; +use rustpython_parser::ast::{self, ArgWithDefault, Arguments, Constant, Decorator, Expr, Ranged}; use ruff_diagnostics::Diagnostic; use ruff_diagnostics::Violation; @@ -6,7 +6,7 @@ use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::call_path::collect_call_path; use crate::checkers::ast::Checker; -use crate::rules::flake8_boolean_trap::helpers::FUNC_DEF_NAME_ALLOWLIST; +use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def; /// ## What it does /// Checks for boolean positional arguments in function definitions. @@ -64,7 +64,7 @@ use crate::rules::flake8_boolean_trap::helpers::FUNC_DEF_NAME_ALLOWLIST; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/reference/expressions.html#calls) +/// - [Python documentation: Calls](https://docs.python.org/3/reference/expressions.html#calls) /// - [_How to Avoid “The Boolean Trap”_ by Adam Johnson](https://adamj.eu/tech/2021/07/10/python-type-hints-how-to-avoid-the-boolean-trap/) #[violation] pub struct BooleanPositionalArgInFunctionDefinition; @@ -82,7 +82,7 @@ pub(crate) fn check_positional_boolean_in_def( decorator_list: &[Decorator], arguments: &Arguments, ) { - if FUNC_DEF_NAME_ALLOWLIST.contains(&name) { + if is_allowed_func_def(name) { return; } @@ -93,11 +93,16 @@ pub(crate) fn check_positional_boolean_in_def( return; } - for arg in arguments.posonlyargs.iter().chain(arguments.args.iter()) { - if arg.annotation.is_none() { + for ArgWithDefault { + def, + default: _, + range: _, + } in arguments.posonlyargs.iter().chain(&arguments.args) + { + if def.annotation.is_none() { continue; } - let Some(expr) = &arg.annotation else { + let Some(expr) = &def.annotation else { continue; }; @@ -115,7 +120,7 @@ pub(crate) fn check_positional_boolean_in_def( } checker.diagnostics.push(Diagnostic::new( BooleanPositionalArgInFunctionDefinition, - arg.range(), + def.range(), )); } } diff --git a/crates/ruff/src/rules/flake8_boolean_trap/rules/mod.rs b/crates/ruff/src/rules/flake8_boolean_trap/rules/mod.rs index a0e9b8bd66..b40aa58dc5 100644 --- a/crates/ruff/src/rules/flake8_boolean_trap/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_boolean_trap/rules/mod.rs @@ -1,12 +1,6 @@ -pub(crate) use check_boolean_default_value_in_function_definition::{ - check_boolean_default_value_in_function_definition, BooleanDefaultValueInFunctionDefinition, -}; -pub(crate) use check_boolean_positional_value_in_function_call::{ - check_boolean_positional_value_in_function_call, BooleanPositionalValueInFunctionCall, -}; -pub(crate) use check_positional_boolean_in_def::{ - check_positional_boolean_in_def, BooleanPositionalArgInFunctionDefinition, -}; +pub(crate) use check_boolean_default_value_in_function_definition::*; +pub(crate) use check_boolean_positional_value_in_function_call::*; +pub(crate) use check_positional_boolean_in_def::*; mod check_boolean_default_value_in_function_definition; mod check_boolean_positional_value_in_function_call; diff --git a/crates/ruff/src/rules/flake8_bugbear/mod.rs b/crates/ruff/src/rules/flake8_bugbear/mod.rs index 2bf5676306..df1c463250 100644 --- a/crates/ruff/src/rules/flake8_bugbear/mod.rs +++ b/crates/ruff/src/rules/flake8_bugbear/mod.rs @@ -19,7 +19,6 @@ mod tests { #[test_case(Rule::AssertRaisesException, Path::new("B017.py"))] #[test_case(Rule::AssignmentToOsEnviron, Path::new("B003.py"))] #[test_case(Rule::CachedInstanceMethod, Path::new("B019.py"))] - #[test_case(Rule::CannotRaiseLiteral, Path::new("B016.py"))] #[test_case(Rule::DuplicateHandlerException, Path::new("B014.py"))] #[test_case(Rule::DuplicateTryBlockException, Path::new("B025.py"))] #[test_case(Rule::DuplicateValue, Path::new("B033.py"))] @@ -35,7 +34,9 @@ mod tests { #[test_case(Rule::LoopVariableOverridesIterator, Path::new("B020.py"))] #[test_case(Rule::MutableArgumentDefault, Path::new("B006_B008.py"))] #[test_case(Rule::NoExplicitStacklevel, Path::new("B028.py"))] + #[test_case(Rule::RaiseLiteral, Path::new("B016.py"))] #[test_case(Rule::RaiseWithoutFromInsideExcept, Path::new("B904.py"))] + #[test_case(Rule::ReSubPositionalArgs, Path::new("B034.py"))] #[test_case(Rule::RedundantTupleInExceptionHandler, Path::new("B013.py"))] #[test_case(Rule::ReuseOfGroupbyGenerator, Path::new("B031.py"))] #[test_case(Rule::SetAttrWithConstant, Path::new("B009_B010.py"))] diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/abstract_base_class.rs b/crates/ruff/src/rules/flake8_bugbear/rules/abstract_base_class.rs index 59ea9cc665..4c16954baa 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/abstract_base_class.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/abstract_base_class.rs @@ -2,13 +2,47 @@ use rustpython_parser::ast::{self, Constant, Expr, Keyword, Ranged, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::identifier_range; +use ruff_python_ast::identifier::Identifier; use ruff_python_semantic::analyze::visibility::{is_abstract, is_overload}; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; use crate::checkers::ast::Checker; use crate::registry::Rule; +/// ## What it does +/// Checks for abstract classes without abstract methods. +/// +/// ## Why is this bad? +/// Abstract base classes are used to define interfaces. If they have no abstract +/// methods, they are not useful. +/// +/// If the class is not meant to be used as an interface, it should not be an +/// abstract base class. Remove the `ABC` base class from the class definition, +/// or add an abstract method to the class. +/// +/// ## Example +/// ```python +/// from abc import ABC +/// +/// +/// class Foo(ABC): +/// def method(self): +/// bar() +/// ``` +/// +/// Use instead: +/// ```python +/// from abc import ABC, abstractmethod +/// +/// +/// class Foo(ABC): +/// @abstractmethod +/// def method(self): +/// bar() +/// ``` +/// +/// ## References +/// - [Python documentation: `abc`](https://docs.python.org/3/library/abc.html) #[violation] pub struct AbstractBaseClassWithoutAbstractMethod { name: String, @@ -21,6 +55,40 @@ impl Violation for AbstractBaseClassWithoutAbstractMethod { format!("`{name}` is an abstract base class, but it has no abstract methods") } } +/// ## What it does +/// Checks for empty methods in abstract base classes without an abstract +/// decorator. +/// +/// ## Why is this bad? +/// Empty methods in abstract base classes without an abstract decorator are +/// indicative of unfinished code or a mistake. +/// +/// Instead, add an abstract method decorated to indicate that it is abstract, +/// or implement the method. +/// +/// ## Example +/// ```python +/// from abc import ABC +/// +/// +/// class Foo(ABC): +/// def method(self): +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// from abc import ABC, abstractmethod +/// +/// +/// class Foo(ABC): +/// @abstractmethod +/// def method(self): +/// ... +/// ``` +/// +/// ## References +/// - [Python documentation: abc](https://docs.python.org/3/library/abc.html) #[violation] pub struct EmptyMethodWithoutAbstractDecorator { name: String, @@ -36,18 +104,18 @@ impl Violation for EmptyMethodWithoutAbstractDecorator { } } -fn is_abc_class(model: &SemanticModel, bases: &[Expr], keywords: &[Keyword]) -> bool { +fn is_abc_class(bases: &[Expr], keywords: &[Keyword], semantic: &SemanticModel) -> bool { keywords.iter().any(|keyword| { keyword.arg.as_ref().map_or(false, |arg| arg == "metaclass") - && model + && semantic .resolve_call_path(&keyword.value) .map_or(false, |call_path| { - call_path.as_slice() == ["abc", "ABCMeta"] + matches!(call_path.as_slice(), ["abc", "ABCMeta"]) }) }) || bases.iter().any(|base| { - model - .resolve_call_path(base) - .map_or(false, |call_path| call_path.as_slice() == ["abc", "ABC"]) + semantic.resolve_call_path(base).map_or(false, |call_path| { + matches!(call_path.as_slice(), ["abc", "ABC"]) + }) }) } @@ -80,7 +148,7 @@ pub(crate) fn abstract_base_class( if bases.len() + keywords.len() != 1 { return; } - if !is_abc_class(checker.semantic_model(), bases, keywords) { + if !is_abc_class(bases, keywords, checker.semantic()) { return; } @@ -93,23 +161,23 @@ pub(crate) fn abstract_base_class( continue; } - let ( - Stmt::FunctionDef(ast::StmtFunctionDef { - decorator_list, - body, - name: method_name, - .. - }) | Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { - decorator_list, - body, - name: method_name, - .. - }) - ) = stmt else { + let (Stmt::FunctionDef(ast::StmtFunctionDef { + decorator_list, + body, + name: method_name, + .. + }) + | Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { + decorator_list, + body, + name: method_name, + .. + })) = stmt + else { continue; }; - let has_abstract_decorator = is_abstract(checker.semantic_model(), decorator_list); + let has_abstract_decorator = is_abstract(decorator_list, checker.semantic()); has_abstract_method |= has_abstract_decorator; if !checker.enabled(Rule::EmptyMethodWithoutAbstractDecorator) { @@ -118,7 +186,7 @@ pub(crate) fn abstract_base_class( if !has_abstract_decorator && is_empty_body(body) - && !is_overload(checker.semantic_model(), decorator_list) + && !is_overload(decorator_list, checker.semantic()) { checker.diagnostics.push(Diagnostic::new( EmptyMethodWithoutAbstractDecorator { @@ -134,7 +202,7 @@ pub(crate) fn abstract_base_class( AbstractBaseClassWithoutAbstractMethod { name: name.to_string(), }, - identifier_range(stmt, checker.locator), + stmt.identifier(), )); } } diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/assert_false.rs b/crates/ruff/src/rules/flake8_bugbear/rules/assert_false.rs index 817483b00e..56b43f77df 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/assert_false.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/assert_false.rs @@ -1,12 +1,35 @@ use ruff_text_size::TextRange; -use rustpython_parser::ast::{self, Constant, Expr, ExprContext, Ranged, Stmt}; +use rustpython_parser::ast::{self, Expr, ExprContext, Ranged, Stmt}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers::is_const_false; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Checks for uses of `assert False`. +/// +/// ## Why is this bad? +/// Python removes `assert` statements when running in optimized mode +/// (`python -O`), making `assert False` an unreliable means of +/// raising an `AssertionError`. +/// +/// Instead, raise an `AssertionError` directly. +/// +/// ## Example +/// ```python +/// assert False +/// ``` +/// +/// Use instead: +/// ```python +/// raise AssertionError +/// ``` +/// +/// ## References +/// - [Python documentation: `assert`](https://docs.python.org/3/reference/simple_stmts.html#the-assert-statement) #[violation] pub struct AssertFalse; @@ -44,16 +67,12 @@ fn assertion_error(msg: Option<&Expr>) -> Stmt { /// B011 pub(crate) fn assert_false(checker: &mut Checker, stmt: &Stmt, test: &Expr, msg: Option<&Expr>) { - let Expr::Constant(ast::ExprConstant { - value: Constant::Bool(false), - .. - } )= &test else { + if !is_const_false(test) { return; - }; + } let mut diagnostic = Diagnostic::new(AssertFalse, test.range()); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] diagnostic.set_fix(Fix::suggested(Edit::range_replacement( checker.generator().stmt(&assertion_error(msg)), stmt.range(), diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/assert_raises_exception.rs b/crates/ruff/src/rules/flake8_bugbear/rules/assert_raises_exception.rs index be7a22f54e..00c44bf0ee 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/assert_raises_exception.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/assert_raises_exception.rs @@ -1,22 +1,20 @@ -use rustpython_parser::ast::{self, Expr, Ranged, Stmt, Withitem}; +use std::fmt; + +use rustpython_parser::ast::{self, Expr, Ranged, WithItem}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; -#[derive(Debug, PartialEq, Eq)] -pub(crate) enum AssertionKind { - AssertRaises, - PytestRaises, -} - /// ## What it does -/// Checks for `self.assertRaises(Exception)` or `pytest.raises(Exception)`. +/// Checks for `assertRaises` and `pytest.raises` context managers that catch +/// `Exception` or `BaseException`. /// /// ## Why is this bad? /// These forms catch every `Exception`, which can lead to tests passing even -/// if, e.g., the code being tested is never executed due to a typo. +/// if, e.g., the code under consideration raises a `SyntaxError` or +/// `IndentationError`. /// /// Either assert for a more specific exception (builtin or custom), or use /// `assertRaisesRegex` or `pytest.raises(..., match=)` respectively. @@ -32,56 +30,90 @@ pub(crate) enum AssertionKind { /// ``` #[violation] pub struct AssertRaisesException { - kind: AssertionKind, + assertion: AssertionKind, + exception: ExceptionKind, } impl Violation for AssertRaisesException { #[derive_message_formats] fn message(&self) -> String { - match self.kind { - AssertionKind::AssertRaises => { - format!("`assertRaises(Exception)` should be considered evil") - } - AssertionKind::PytestRaises => { - format!("`pytest.raises(Exception)` should be considered evil") - } + let AssertRaisesException { + assertion, + exception, + } = self; + format!("`{assertion}({exception})` should be considered evil") + } +} + +#[derive(Debug, PartialEq, Eq)] +enum AssertionKind { + AssertRaises, + PytestRaises, +} + +impl fmt::Display for AssertionKind { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match self { + AssertionKind::AssertRaises => fmt.write_str("assertRaises"), + AssertionKind::PytestRaises => fmt.write_str("pytest.raises"), + } + } +} + +#[derive(Debug, PartialEq, Eq)] +enum ExceptionKind { + BaseException, + Exception, +} + +impl fmt::Display for ExceptionKind { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match self { + ExceptionKind::BaseException => fmt.write_str("BaseException"), + ExceptionKind::Exception => fmt.write_str("Exception"), } } } /// B017 -pub(crate) fn assert_raises_exception(checker: &mut Checker, stmt: &Stmt, items: &[Withitem]) { - let Some(item) = items.first() else { - return; - }; - let item_context = &item.context_expr; - let Expr::Call(ast::ExprCall { func, args, keywords, range: _ }) = &item_context else { - return; - }; - if args.len() != 1 { - return; - } - if item.optional_vars.is_some() { - return; - } +pub(crate) fn assert_raises_exception(checker: &mut Checker, items: &[WithItem]) { + for item in items { + let Expr::Call(ast::ExprCall { + func, + args, + keywords, + range: _, + }) = &item.context_expr + else { + return; + }; + if args.len() != 1 { + return; + } + if item.optional_vars.is_some() { + return; + } - if !checker - .semantic_model() - .resolve_call_path(args.first().unwrap()) - .map_or(false, |call_path| call_path.as_slice() == ["", "Exception"]) - { - return; - } + let Some(exception) = checker + .semantic() + .resolve_call_path(args.first().unwrap()) + .and_then(|call_path| match call_path.as_slice() { + ["", "Exception"] => Some(ExceptionKind::Exception), + ["", "BaseException"] => Some(ExceptionKind::BaseException), + _ => None, + }) + else { + return; + }; - let kind = { - if matches!(func.as_ref(), Expr::Attribute(ast::ExprAttribute { attr, .. }) if attr == "assertRaises") + let assertion = if matches!(func.as_ref(), Expr::Attribute(ast::ExprAttribute { attr, .. }) if attr == "assertRaises") { AssertionKind::AssertRaises } else if checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["pytest", "raises"] + matches!(call_path.as_slice(), ["pytest", "raises"]) }) && !keywords .iter() @@ -90,11 +122,14 @@ pub(crate) fn assert_raises_exception(checker: &mut Checker, stmt: &Stmt, items: AssertionKind::PytestRaises } else { return; - } - }; + }; - checker.diagnostics.push(Diagnostic::new( - AssertRaisesException { kind }, - stmt.range(), - )); + checker.diagnostics.push(Diagnostic::new( + AssertRaisesException { + assertion, + exception, + }, + item.range(), + )); + } } diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/assignment_to_os_environ.rs b/crates/ruff/src/rules/flake8_bugbear/rules/assignment_to_os_environ.rs index fcaa3d6373..50f3f32922 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/assignment_to_os_environ.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/assignment_to_os_environ.rs @@ -5,6 +5,39 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for assignments to `os.environ`. +/// +/// ## Why is this bad? +/// In Python, `os.environ` is a mapping that represents the environment of the +/// current process. +/// +/// However, reassigning to `os.environ` does not clear the environment. Instead, +/// it merely updates the `os.environ` for the current process. This can lead to +/// unexpected behavior, especially when running the program in a subprocess. +/// +/// Instead, use `os.environ.clear()` to clear the environment, or use the +/// `env` argument of `subprocess.Popen` to pass a custom environment to +/// a subprocess. +/// +/// ## Example +/// ```python +/// import os +/// +/// os.environ = {"foo": "bar"} +/// ``` +/// +/// Use instead: +/// ```python +/// import os +/// +/// os.environ.clear() +/// os.environ["foo"] = "bar" +/// ``` +/// +/// ## References +/// - [Python documentation: `os.environ`](https://docs.python.org/3/library/os.html#os.environ) +/// - [Python documentation: `subprocess.Popen`](https://docs.python.org/3/library/subprocess.html#subprocess.Popen) #[violation] pub struct AssignmentToOsEnviron; @@ -26,7 +59,7 @@ pub(crate) fn assignment_to_os_environ(checker: &mut Checker, targets: &[Expr]) if attr != "environ" { return; } - let Expr::Name(ast::ExprName { id, .. } )= value.as_ref() else { + let Expr::Name(ast::ExprName { id, .. }) = value.as_ref() else { return; }; if id != "os" { diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/cached_instance_method.rs b/crates/ruff/src/rules/flake8_bugbear/rules/cached_instance_method.rs index d16bfabf1f..b12482599d 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/cached_instance_method.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/cached_instance_method.rs @@ -2,10 +2,61 @@ use rustpython_parser::ast::{self, Decorator, Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for uses of the `functools.lru_cache` and `functools.cache` +/// decorators on methods. +/// +/// ## Why is this bad? +/// Using the `functools.lru_cache` and `functools.cache` decorators on methods +/// can lead to memory leaks, as the global cache will retain a reference to +/// the instance, preventing it from being garbage collected. +/// +/// Instead, refactor the method to depend only on its arguments and not on the +/// instance of the class, or use the `@lru_cache` decorator on a function +/// outside of the class. +/// +/// ## Example +/// ```python +/// from functools import lru_cache +/// +/// +/// def square(x: int) -> int: +/// return x * x +/// +/// +/// class Number: +/// value: int +/// +/// @lru_cache +/// def squared(self): +/// return square(self.value) +/// ``` +/// +/// Use instead: +/// ```python +/// from functools import lru_cache +/// +/// +/// @lru_cache +/// def square(x: int) -> int: +/// return x * x +/// +/// +/// class Number: +/// value: int +/// +/// def squared(self): +/// return square(self.value) +/// ``` +/// +/// ## References +/// - [Python documentation: `functools.lru_cache`](https://docs.python.org/3/library/functools.html#functools.lru_cache) +/// - [Python documentation: `functools.cache`](https://docs.python.org/3/library/functools.html#functools.cache) +/// - [don't lru_cache methods!](https://www.youtube.com/watch?v=sVjtp6tGo0g) #[violation] pub struct CachedInstanceMethod; @@ -18,16 +69,15 @@ impl Violation for CachedInstanceMethod { } } -fn is_cache_func(model: &SemanticModel, expr: &Expr) -> bool { - model.resolve_call_path(expr).map_or(false, |call_path| { - call_path.as_slice() == ["functools", "lru_cache"] - || call_path.as_slice() == ["functools", "cache"] +fn is_cache_func(expr: &Expr, semantic: &SemanticModel) -> bool { + semantic.resolve_call_path(expr).map_or(false, |call_path| { + matches!(call_path.as_slice(), ["functools", "lru_cache" | "cache"]) }) } /// B019 pub(crate) fn cached_instance_method(checker: &mut Checker, decorator_list: &[Decorator]) { - if !checker.semantic_model().scope().kind.is_class() { + if !checker.semantic().scope().kind.is_class() { return; } for decorator in decorator_list { @@ -41,11 +91,11 @@ pub(crate) fn cached_instance_method(checker: &mut Checker, decorator_list: &[De } for decorator in decorator_list { if is_cache_func( - checker.semantic_model(), match &decorator.expression { Expr::Call(ast::ExprCall { func, .. }) => func, _ => &decorator.expression, }, + checker.semantic(), ) { checker .diagnostics diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/cannot_raise_literal.rs b/crates/ruff/src/rules/flake8_bugbear/rules/cannot_raise_literal.rs deleted file mode 100644 index 6b2df20d5b..0000000000 --- a/crates/ruff/src/rules/flake8_bugbear/rules/cannot_raise_literal.rs +++ /dev/null @@ -1,26 +0,0 @@ -use rustpython_parser::ast::{Expr, Ranged}; - -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, violation}; - -use crate::checkers::ast::Checker; - -#[violation] -pub struct CannotRaiseLiteral; - -impl Violation for CannotRaiseLiteral { - #[derive_message_formats] - fn message(&self) -> String { - format!("Cannot raise a literal. Did you intend to return it or raise an Exception?") - } -} - -/// B016 -pub(crate) fn cannot_raise_literal(checker: &mut Checker, expr: &Expr) { - let Expr::Constant ( _) = expr else { - return; - }; - checker - .diagnostics - .push(Diagnostic::new(CannotRaiseLiteral, expr.range())); -} diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/duplicate_exceptions.rs b/crates/ruff/src/rules/flake8_bugbear/rules/duplicate_exceptions.rs index 95eed78d56..388d4a0128 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/duplicate_exceptions.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/duplicate_exceptions.rs @@ -1,7 +1,7 @@ use itertools::Itertools; use ruff_text_size::TextRange; use rustc_hash::{FxHashMap, FxHashSet}; -use rustpython_parser::ast::{self, Excepthandler, Expr, ExprContext, Ranged}; +use rustpython_parser::ast::{self, ExceptHandler, Expr, ExprContext, Ranged}; use ruff_diagnostics::{AlwaysAutofixableViolation, Violation}; use ruff_diagnostics::{Diagnostic, Edit, Fix}; @@ -12,6 +12,33 @@ use ruff_python_ast::call_path::CallPath; use crate::checkers::ast::Checker; use crate::registry::{AsRule, Rule}; +/// ## What it does +/// Checks for `try-except` blocks with duplicate exception handlers. +/// +/// ## Why is this bad? +/// Duplicate exception handlers are redundant, as the first handler will catch +/// the exception, making the second handler unreachable. +/// +/// ## Example +/// ```python +/// try: +/// ... +/// except ValueError: +/// ... +/// except ValueError: +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// try: +/// ... +/// except ValueError: +/// ... +/// ``` +/// +/// ## References +/// - [Python documentation: `except` clause](https://docs.python.org/3/reference/compound_stmts.html#except-clause) #[violation] pub struct DuplicateTryBlockException { name: String, @@ -24,6 +51,36 @@ impl Violation for DuplicateTryBlockException { format!("try-except block with duplicate exception `{name}`") } } + +/// ## What it does +/// Checks for exception handlers that catch duplicate exceptions. +/// +/// ## Why is this bad? +/// Including the same exception multiple times in the same handler is redundant, +/// as the first exception will catch the exception, making the second exception +/// unreachable. The same applies to exception hierarchies, as a handler for a +/// parent exception (like `Exception`) will also catch child exceptions (like +/// `ValueError`). +/// +/// ## Example +/// ```python +/// try: +/// ... +/// except (Exception, ValueError): # `Exception` includes `ValueError`. +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// try: +/// ... +/// except Exception: +/// ... +/// ``` +/// +/// ## References +/// - [Python documentation: `except` clause](https://docs.python.org/3/reference/compound_stmts.html#except-clause) +/// - [Python documentation: Exception hierarchy](https://docs.python.org/3/library/exceptions.html#exception-hierarchy) #[violation] pub struct DuplicateHandlerException { pub names: Vec, @@ -89,8 +146,7 @@ fn duplicate_handler_exceptions<'a>( expr.range(), ); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::suggested(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( if unique_elts.len() == 1 { checker.generator().expr(unique_elts[0]) } else { @@ -106,11 +162,15 @@ fn duplicate_handler_exceptions<'a>( seen } -pub(crate) fn duplicate_exceptions(checker: &mut Checker, handlers: &[Excepthandler]) { +pub(crate) fn duplicate_exceptions(checker: &mut Checker, handlers: &[ExceptHandler]) { let mut seen: FxHashSet = FxHashSet::default(); let mut duplicates: FxHashMap> = FxHashMap::default(); for handler in handlers { - let Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { type_: Some(type_), .. }) = handler else { + let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { + type_: Some(type_), + .. + }) = handler + else { continue; }; match type_.as_ref() { diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/except_with_empty_tuple.rs b/crates/ruff/src/rules/flake8_bugbear/rules/except_with_empty_tuple.rs index 9ee4188a46..1ee0f711ee 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/except_with_empty_tuple.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/except_with_empty_tuple.rs @@ -1,11 +1,37 @@ use rustpython_parser::ast::{self, Ranged}; -use rustpython_parser::ast::{Excepthandler, Expr}; +use rustpython_parser::ast::{ExceptHandler, Expr}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for exception handlers that catch an empty tuple. +/// +/// ## Why is this bad? +/// An exception handler that catches an empty tuple will not catch anything, +/// and is indicative of a mistake. Instead, add exceptions to the `except` +/// clause. +/// +/// ## Example +/// ```python +/// try: +/// 1 / 0 +/// except (): +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// try: +/// 1 / 0 +/// except ZeroDivisionError: +/// ... +/// ``` +/// +/// ## References +/// - [Python documentation: `except` clause](https://docs.python.org/3/reference/compound_stmts.html#except-clause) #[violation] pub struct ExceptWithEmptyTuple; @@ -17,8 +43,9 @@ impl Violation for ExceptWithEmptyTuple { } /// B029 -pub(crate) fn except_with_empty_tuple(checker: &mut Checker, excepthandler: &Excepthandler) { - let Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { type_, .. }) = excepthandler; +pub(crate) fn except_with_empty_tuple(checker: &mut Checker, except_handler: &ExceptHandler) { + let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { type_, .. }) = + except_handler; let Some(type_) = type_ else { return; }; @@ -26,8 +53,9 @@ pub(crate) fn except_with_empty_tuple(checker: &mut Checker, excepthandler: &Exc return; }; if elts.is_empty() { - checker - .diagnostics - .push(Diagnostic::new(ExceptWithEmptyTuple, excepthandler.range())); + checker.diagnostics.push(Diagnostic::new( + ExceptWithEmptyTuple, + except_handler.range(), + )); } } diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/except_with_non_exception_classes.rs b/crates/ruff/src/rules/flake8_bugbear/rules/except_with_non_exception_classes.rs index 4bfab07f94..cd195a8635 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/except_with_non_exception_classes.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/except_with_non_exception_classes.rs @@ -1,12 +1,38 @@ use std::collections::VecDeque; -use rustpython_parser::ast::{self, Excepthandler, Expr, Ranged}; +use rustpython_parser::ast::{self, ExceptHandler, Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for exception handlers that catch non-exception classes. +/// +/// ## Why is this bad? +/// Catching classes that do not inherit from `BaseException` will raise a +/// `TypeError`. +/// +/// ## Example +/// ```python +/// try: +/// 1 / 0 +/// except 1: +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// try: +/// 1 / 0 +/// except ZeroDivisionError: +/// ... +/// ``` +/// +/// ## References +/// - [Python documentation: `except` clause](https://docs.python.org/3/reference/compound_stmts.html#except-clause) +/// - [Python documentation: Built-in Exceptions](https://docs.python.org/3/library/exceptions.html#built-in-exceptions) #[violation] pub struct ExceptWithNonExceptionClasses; @@ -21,7 +47,7 @@ impl Violation for ExceptWithNonExceptionClasses { /// This should leave any unstarred iterables alone (subsequently raising a /// warning for B029). fn flatten_starred_iterables(expr: &Expr) -> Vec<&Expr> { - let Expr::Tuple(ast::ExprTuple { elts, .. } )= expr else { + let Expr::Tuple(ast::ExprTuple { elts, .. }) = expr else { return vec![expr]; }; let mut flattened_exprs: Vec<&Expr> = Vec::with_capacity(elts.len()); @@ -44,9 +70,10 @@ fn flatten_starred_iterables(expr: &Expr) -> Vec<&Expr> { /// B030 pub(crate) fn except_with_non_exception_classes( checker: &mut Checker, - excepthandler: &Excepthandler, + except_handler: &ExceptHandler, ) { - let Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { type_, .. }) = excepthandler; + let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { type_, .. }) = + except_handler; let Some(type_) = type_ else { return; }; diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/f_string_docstring.rs b/crates/ruff/src/rules/flake8_bugbear/rules/f_string_docstring.rs index 5453b71569..1a5bd2e011 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/f_string_docstring.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/f_string_docstring.rs @@ -1,11 +1,35 @@ -use rustpython_parser::ast::{self, Expr, Stmt}; +use rustpython_parser::ast::{self, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers; +use ruff_python_ast::identifier::Identifier; use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for docstrings that are written via f-strings. +/// +/// ## Why is this bad? +/// Python will interpret the f-string as a joined string, rather than as a +/// docstring. As such, the "docstring" will not be accessible via the +/// `__doc__` attribute, nor will it be picked up by any automated +/// documentation tooling. +/// +/// ## Example +/// ```python +/// def foo(): +/// f"""Not a docstring.""" +/// ``` +/// +/// Use instead: +/// ```python +/// def foo(): +/// """A docstring.""" +/// ``` +/// +/// ## References +/// - [PEP 257](https://peps.python.org/pep-0257/) +/// - [Python documentation: Formatted string literals](https://docs.python.org/3/reference/lexical_analysis.html#f-strings) #[violation] pub struct FStringDocstring; @@ -26,11 +50,10 @@ pub(crate) fn f_string_docstring(checker: &mut Checker, body: &[Stmt]) { let Stmt::Expr(ast::StmtExpr { value, range: _ }) = stmt else { return; }; - let Expr::JoinedStr ( _) = value.as_ref() else { + if !value.is_joined_str_expr() { return; - }; - checker.diagnostics.push(Diagnostic::new( - FStringDocstring, - helpers::identifier_range(stmt, checker.locator), - )); + } + checker + .diagnostics + .push(Diagnostic::new(FStringDocstring, stmt.identifier())); } diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/function_call_argument_default.rs b/crates/ruff/src/rules/flake8_bugbear/rules/function_call_argument_default.rs index a36b571c12..d85d0d19e5 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/function_call_argument_default.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/function_call_argument_default.rs @@ -1,5 +1,5 @@ use ruff_text_size::TextRange; -use rustpython_parser::ast::{self, Arguments, Expr, Ranged}; +use rustpython_parser::ast::{self, ArgWithDefault, Arguments, Expr, Ranged}; use ruff_diagnostics::Violation; use ruff_diagnostics::{Diagnostic, DiagnosticKind}; @@ -7,11 +7,10 @@ use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::call_path::{compose_call_path, from_qualified_name, CallPath}; use ruff_python_ast::visitor; use ruff_python_ast::visitor::Visitor; -use ruff_python_semantic::analyze::typing::is_immutable_func; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::analyze::typing::{is_immutable_func, is_mutable_func}; +use ruff_python_semantic::SemanticModel; use crate::checkers::ast::Checker; -use crate::rules::flake8_bugbear::rules::mutable_argument_default::is_mutable_func; /// ## What it does /// Checks for function calls in default function arguments. @@ -21,9 +20,6 @@ use crate::rules::flake8_bugbear::rules::mutable_argument_default::is_mutable_fu /// once, at definition time. The returned value will then be reused by all /// calls to the function, which can lead to unexpected behaviour. /// -/// ## Options -/// - `flake8-bugbear.extend-immutable-calls` -/// /// ## Example /// ```python /// def create_list() -> list[int]: @@ -45,16 +41,8 @@ use crate::rules::flake8_bugbear::rules::mutable_argument_default::is_mutable_fu /// return arg /// ``` /// -/// Alternatively, if shared behavior is desirable, clarify the intent by -/// assigning to a module-level variable: -/// ```python -/// I_KNOW_THIS_IS_SHARED_STATE = create_list() -/// -/// -/// def mutable_default(arg: list[int] = I_KNOW_THIS_IS_SHARED_STATE) -> list[int]: -/// arg.append(4) -/// return arg -/// ``` +/// ## Options +/// - `flake8-bugbear.extend-immutable-calls` #[violation] pub struct FunctionCallInDefaultArgument { pub name: Option, @@ -73,7 +61,7 @@ impl Violation for FunctionCallInDefaultArgument { } struct ArgumentDefaultVisitor<'a> { - model: &'a SemanticModel<'a>, + semantic: &'a SemanticModel<'a>, extend_immutable_calls: Vec>, diagnostics: Vec<(DiagnosticKind, TextRange)>, } @@ -81,7 +69,7 @@ struct ArgumentDefaultVisitor<'a> { impl<'a> ArgumentDefaultVisitor<'a> { fn new(model: &'a SemanticModel<'a>, extend_immutable_calls: Vec>) -> Self { Self { - model, + semantic: model, extend_immutable_calls, diagnostics: Vec::new(), } @@ -95,8 +83,8 @@ where fn visit_expr(&mut self, expr: &'b Expr) { match expr { Expr::Call(ast::ExprCall { func, .. }) => { - if !is_mutable_func(self.model, func) - && !is_immutable_func(self.model, func, &self.extend_immutable_calls) + if !is_mutable_func(func, self.semantic) + && !is_immutable_func(func, self.semantic, &self.extend_immutable_calls) { self.diagnostics.push(( FunctionCallInDefaultArgument { @@ -125,14 +113,20 @@ pub(crate) fn function_call_argument_default(checker: &mut Checker, arguments: & .map(|target| from_qualified_name(target)) .collect(); let diagnostics = { - let mut visitor = - ArgumentDefaultVisitor::new(checker.semantic_model(), extend_immutable_calls); - for expr in arguments - .defaults + let mut visitor = ArgumentDefaultVisitor::new(checker.semantic(), extend_immutable_calls); + for ArgWithDefault { + default, + def: _, + range: _, + } in arguments + .posonlyargs .iter() - .chain(arguments.kw_defaults.iter()) + .chain(&arguments.args) + .chain(&arguments.kwonlyargs) { - visitor.visit_expr(expr); + if let Some(expr) = &default { + visitor.visit_expr(expr); + } } visitor.diagnostics }; diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/function_uses_loop_variable.rs b/crates/ruff/src/rules/flake8_bugbear/rules/function_uses_loop_variable.rs index d6d061534f..edba6aa901 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/function_uses_loop_variable.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/function_uses_loop_variable.rs @@ -1,15 +1,47 @@ -use rustc_hash::FxHashSet; use rustpython_parser::ast::{self, Comprehension, Expr, ExprContext, Ranged, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::collect_arg_names; +use ruff_python_ast::helpers::includes_arg_name; use ruff_python_ast::types::Node; use ruff_python_ast::visitor; use ruff_python_ast::visitor::Visitor; use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for function definitions that use a loop variable. +/// +/// ## Why is this bad? +/// The loop variable is not bound in the function definition, so it will always +/// have the value it had in the last iteration when the function is called. +/// +/// Instead, consider using a default argument to bind the loop variable at +/// function definition time. Or, use `functools.partial`. +/// +/// ## Example +/// ```python +/// adders = [lambda x: x + i for i in range(3)] +/// values = [adder(1) for adder in adders] # [3, 3, 3] +/// ``` +/// +/// Use instead: +/// ```python +/// adders = [lambda x, i=i: x + i for i in range(3)] +/// values = [adder(1) for adder in adders] # [1, 2, 3] +/// ``` +/// +/// Or: +/// ```python +/// from functools import partial +/// +/// adders = [partial(lambda x, i: x + i, i) for i in range(3)] +/// values = [adder(1) for adder in adders] # [1, 2, 3] +/// ``` +/// +/// ## References +/// - [The Hitchhiker's Guide to Python: Late Binding Closures](https://docs.python-guide.org/writing/gotchas/#late-binding-closures) +/// - [Python documentation: functools.partial](https://docs.python.org/3/library/functools.html#functools.partial) #[violation] pub struct FunctionUsesLoopVariable { name: String, @@ -25,19 +57,17 @@ impl Violation for FunctionUsesLoopVariable { #[derive(Default)] struct LoadedNamesVisitor<'a> { - // Tuple of: name, defining expression, and defining range. - loaded: Vec<(&'a str, &'a Expr)>, - // Tuple of: name, defining expression, and defining range. - stored: Vec<(&'a str, &'a Expr)>, + loaded: Vec<&'a ast::ExprName>, + stored: Vec<&'a ast::ExprName>, } /// `Visitor` to collect all used identifiers in a statement. impl<'a> Visitor<'a> for LoadedNamesVisitor<'a> { fn visit_expr(&mut self, expr: &'a Expr) { match expr { - Expr::Name(ast::ExprName { id, ctx, range: _ }) => match ctx { - ExprContext::Load => self.loaded.push((id, expr)), - ExprContext::Store => self.stored.push((id, expr)), + Expr::Name(name) => match &name.ctx { + ExprContext::Load => self.loaded.push(name), + ExprContext::Store => self.stored.push(name), ExprContext::Del => {} }, _ => visitor::walk_expr(self, expr), @@ -47,7 +77,7 @@ impl<'a> Visitor<'a> for LoadedNamesVisitor<'a> { #[derive(Default)] struct SuspiciousVariablesVisitor<'a> { - names: Vec<(&'a str, &'a Expr)>, + names: Vec<&'a ast::ExprName>, safe_functions: Vec<&'a Expr>, } @@ -62,17 +92,20 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> { let mut visitor = LoadedNamesVisitor::default(); visitor.visit_body(body); - // Collect all argument names. - let mut arg_names = collect_arg_names(args); - arg_names.extend(visitor.stored.iter().map(|(id, ..)| id)); - // Treat any non-arguments as "suspicious". - self.names.extend( - visitor - .loaded - .into_iter() - .filter(|(id, ..)| !arg_names.contains(id)), - ); + self.names + .extend(visitor.loaded.into_iter().filter(|loaded| { + if visitor.stored.iter().any(|stored| stored.id == loaded.id) { + return false; + } + + if includes_arg_name(&loaded.id, args) { + return false; + } + + true + })); + return; } Stmt::Return(ast::StmtReturn { @@ -99,10 +132,9 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> { }) => { match func.as_ref() { Expr::Name(ast::ExprName { id, .. }) => { - let id = id.as_str(); - if id == "filter" || id == "reduce" || id == "map" { + if matches!(id.as_str(), "filter" | "reduce" | "map") { for arg in args { - if matches!(arg, Expr::Lambda(_)) { + if arg.is_lambda_expr() { self.safe_functions.push(arg); } } @@ -126,7 +158,7 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> { for keyword in keywords { if keyword.arg.as_ref().map_or(false, |arg| arg == "key") - && matches!(keyword.value, Expr::Lambda(_)) + && keyword.value.is_lambda_expr() { self.safe_functions.push(&keyword.value); } @@ -142,17 +174,19 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> { let mut visitor = LoadedNamesVisitor::default(); visitor.visit_expr(body); - // Collect all argument names. - let mut arg_names = collect_arg_names(args); - arg_names.extend(visitor.stored.iter().map(|(id, ..)| id)); - // Treat any non-arguments as "suspicious". - self.names.extend( - visitor - .loaded - .iter() - .filter(|(id, ..)| !arg_names.contains(id)), - ); + self.names + .extend(visitor.loaded.into_iter().filter(|loaded| { + if visitor.stored.iter().any(|stored| stored.id == loaded.id) { + return false; + } + + if includes_arg_name(&loaded.id, args) { + return false; + } + + true + })); return; } @@ -165,7 +199,7 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> { #[derive(Default)] struct NamesFromAssignmentsVisitor<'a> { - names: FxHashSet<&'a str>, + names: Vec<&'a str>, } /// `Visitor` to collect all names used in an assignment expression. @@ -173,7 +207,7 @@ impl<'a> Visitor<'a> for NamesFromAssignmentsVisitor<'a> { fn visit_expr(&mut self, expr: &'a Expr) { match expr { Expr::Name(ast::ExprName { id, .. }) => { - self.names.insert(id.as_str()); + self.names.push(id.as_str()); } Expr::Starred(ast::ExprStarred { value, .. }) => { self.visit_expr(value); @@ -190,7 +224,7 @@ impl<'a> Visitor<'a> for NamesFromAssignmentsVisitor<'a> { #[derive(Default)] struct AssignedNamesVisitor<'a> { - names: FxHashSet<&'a str>, + names: Vec<&'a str>, } /// `Visitor` to collect all used identifiers in a statement. @@ -224,7 +258,7 @@ impl<'a> Visitor<'a> for AssignedNamesVisitor<'a> { } fn visit_expr(&mut self, expr: &'a Expr) { - if matches!(expr, Expr::Lambda(_)) { + if expr.is_lambda_expr() { // Don't recurse. return; } @@ -267,15 +301,15 @@ pub(crate) fn function_uses_loop_variable<'a>(checker: &mut Checker<'a>, node: & // If a variable was used in a function or lambda body, and assigned in the // loop, flag it. - for (name, expr) in suspicious_variables { - if reassigned_in_loop.contains(name) { - if !checker.flake8_bugbear_seen.contains(&expr) { - checker.flake8_bugbear_seen.push(expr); + for name in suspicious_variables { + if reassigned_in_loop.contains(&name.id.as_str()) { + if !checker.flake8_bugbear_seen.contains(&name) { + checker.flake8_bugbear_seen.push(name); checker.diagnostics.push(Diagnostic::new( FunctionUsesLoopVariable { - name: name.to_string(), + name: name.id.to_string(), }, - expr.range(), + name.range(), )); } } diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/getattr_with_constant.rs b/crates/ruff/src/rules/flake8_bugbear/rules/getattr_with_constant.rs index ff57ce600f..3abcea7013 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/getattr_with_constant.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/getattr_with_constant.rs @@ -1,5 +1,5 @@ use ruff_text_size::TextRange; -use rustpython_parser::ast::{self, Constant, Expr, ExprContext, Ranged}; +use rustpython_parser::ast::{self, Constant, Expr, ExprContext, Identifier, Ranged}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; @@ -8,6 +8,29 @@ use ruff_python_stdlib::identifiers::{is_identifier, is_mangled_private}; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Checks for uses of `getattr` that take a constant attribute value as an +/// argument (e.g., `getattr(obj, "foo")`). +/// +/// ## Why is this bad? +/// `getattr` is used to access attributes dynamically. If the attribute is +/// defined as a constant, it is no safer than a typical property access. When +/// possible, prefer property access over `getattr` calls, as the former is +/// more concise and idiomatic. +/// +/// +/// ## Example +/// ```python +/// getattr(obj, "foo") +/// ``` +/// +/// Use instead: +/// ```python +/// obj.foo +/// ``` +/// +/// ## References +/// - [Python documentation: `getattr`](https://docs.python.org/3/library/functions.html#getattr) #[violation] pub struct GetAttrWithConstant; @@ -27,7 +50,7 @@ impl AlwaysAutofixableViolation for GetAttrWithConstant { fn attribute(value: &Expr, attr: &str) -> Expr { ast::ExprAttribute { value: Box::new(value.clone()), - attr: attr.into(), + attr: Identifier::new(attr.to_string(), TextRange::default()), ctx: ExprContext::Load, range: TextRange::default(), } @@ -41,7 +64,7 @@ pub(crate) fn getattr_with_constant( func: &Expr, args: &[Expr], ) { - let Expr::Name(ast::ExprName { id, .. } )= func else { + let Expr::Name(ast::ExprName { id, .. }) = func else { return; }; if id != "getattr" { @@ -53,7 +76,8 @@ pub(crate) fn getattr_with_constant( let Expr::Constant(ast::ExprConstant { value: Constant::Str(value), .. - } )= arg else { + }) = arg + else { return; }; if !is_identifier(value) { diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/jump_statement_in_finally.rs b/crates/ruff/src/rules/flake8_bugbear/rules/jump_statement_in_finally.rs index df656cafed..15b8aca374 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/jump_statement_in_finally.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/jump_statement_in_finally.rs @@ -5,6 +5,40 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for `break`, `continue`, and `return` statements in `finally` +/// blocks. +/// +/// ## Why is this bad? +/// The use of `break`, `continue`, and `return` statements in `finally` blocks +/// can cause exceptions to be silenced. +/// +/// `finally` blocks execute regardless of whether an exception is raised. If a +/// `break`, `continue`, or `return` statement is reached in a `finally` block, +/// any exception raised in the `try` or `except` blocks will be silenced. +/// +/// ## Example +/// ```python +/// def speed(distance, time): +/// try: +/// return distance / time +/// except ZeroDivisionError: +/// raise ValueError("Time cannot be zero") +/// finally: +/// return 299792458 # `ValueError` is silenced +/// ``` +/// +/// Use instead: +/// ```python +/// def speed(distance, time): +/// try: +/// return distance / time +/// except ZeroDivisionError: +/// raise ValueError("Time cannot be zero") +/// ``` +/// +/// ## References +/// - [Python documentation: The `try` statement](https://docs.python.org/3/reference/compound_stmts.html#the-try-statement) #[violation] pub struct JumpStatementInFinally { name: String, diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/loop_variable_overrides_iterator.rs b/crates/ruff/src/rules/flake8_bugbear/rules/loop_variable_overrides_iterator.rs index f8d37590dd..8b7efb83ec 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/loop_variable_overrides_iterator.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/loop_variable_overrides_iterator.rs @@ -1,5 +1,5 @@ use rustc_hash::FxHashMap; -use rustpython_parser::ast::{self, Expr, Ranged}; +use rustpython_parser::ast::{self, ArgWithDefault, Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -8,6 +8,33 @@ use ruff_python_ast::visitor::Visitor; use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for loop control variables that override the loop iterable. +/// +/// ## Why is this bad? +/// Loop control variables should not override the loop iterable, as this can +/// lead to confusing behavior. +/// +/// Instead, use a distinct variable name for any loop control variables. +/// +/// ## Example +/// ```python +/// items = [1, 2, 3] +/// +/// for items in items: +/// print(items) +/// ``` +/// +/// Use instead: +/// ```python +/// items = [1, 2, 3] +/// +/// for item in items: +/// print(item) +/// ``` +/// +/// ## References +/// - [Python documentation: The `for` statement](https://docs.python.org/3/reference/compound_stmts.html#the-for-statement) #[violation] pub struct LoopVariableOverridesIterator { name: String, @@ -49,8 +76,17 @@ where range: _, }) => { visitor::walk_expr(self, body); - for arg in &args.args { - self.names.remove(arg.arg.as_str()); + for ArgWithDefault { + def, + default: _, + range: _, + } in args + .posonlyargs + .iter() + .chain(&args.args) + .chain(&args.kwonlyargs) + { + self.names.remove(def.arg.as_str()); } } _ => visitor::walk_expr(self, expr), diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/mod.rs b/crates/ruff/src/rules/flake8_bugbear/rules/mod.rs index f350160a57..0cb4402221 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/mod.rs @@ -1,71 +1,42 @@ -pub(crate) use abstract_base_class::{ - abstract_base_class, AbstractBaseClassWithoutAbstractMethod, - EmptyMethodWithoutAbstractDecorator, -}; -pub(crate) use assert_false::{assert_false, AssertFalse}; -pub(crate) use assert_raises_exception::{assert_raises_exception, AssertRaisesException}; -pub(crate) use assignment_to_os_environ::{assignment_to_os_environ, AssignmentToOsEnviron}; -pub(crate) use cached_instance_method::{cached_instance_method, CachedInstanceMethod}; -pub(crate) use cannot_raise_literal::{cannot_raise_literal, CannotRaiseLiteral}; -pub(crate) use duplicate_exceptions::{ - duplicate_exceptions, DuplicateHandlerException, DuplicateTryBlockException, -}; -pub(crate) use duplicate_value::{duplicate_value, DuplicateValue}; -pub(crate) use except_with_empty_tuple::{except_with_empty_tuple, ExceptWithEmptyTuple}; -pub(crate) use except_with_non_exception_classes::{ - except_with_non_exception_classes, ExceptWithNonExceptionClasses, -}; -pub(crate) use f_string_docstring::{f_string_docstring, FStringDocstring}; -pub(crate) use function_call_argument_default::{ - function_call_argument_default, FunctionCallInDefaultArgument, -}; -pub(crate) use function_uses_loop_variable::{ - function_uses_loop_variable, FunctionUsesLoopVariable, -}; -pub(crate) use getattr_with_constant::{getattr_with_constant, GetAttrWithConstant}; -pub(crate) use jump_statement_in_finally::{jump_statement_in_finally, JumpStatementInFinally}; -pub(crate) use loop_variable_overrides_iterator::{ - loop_variable_overrides_iterator, LoopVariableOverridesIterator, -}; -pub(crate) use mutable_argument_default::{mutable_argument_default, MutableArgumentDefault}; -pub(crate) use no_explicit_stacklevel::{no_explicit_stacklevel, NoExplicitStacklevel}; -pub(crate) use raise_without_from_inside_except::{ - raise_without_from_inside_except, RaiseWithoutFromInsideExcept, -}; -pub(crate) use redundant_tuple_in_exception_handler::{ - redundant_tuple_in_exception_handler, RedundantTupleInExceptionHandler, -}; -pub(crate) use reuse_of_groupby_generator::{reuse_of_groupby_generator, ReuseOfGroupbyGenerator}; -pub(crate) use setattr_with_constant::{setattr_with_constant, SetAttrWithConstant}; -pub(crate) use star_arg_unpacking_after_keyword_arg::{ - star_arg_unpacking_after_keyword_arg, StarArgUnpackingAfterKeywordArg, -}; -pub(crate) use strip_with_multi_characters::{ - strip_with_multi_characters, StripWithMultiCharacters, -}; -pub(crate) use unary_prefix_increment::{unary_prefix_increment, UnaryPrefixIncrement}; -pub(crate) use unintentional_type_annotation::{ - unintentional_type_annotation, UnintentionalTypeAnnotation, -}; -pub(crate) use unreliable_callable_check::{unreliable_callable_check, UnreliableCallableCheck}; -pub(crate) use unused_loop_control_variable::{ - unused_loop_control_variable, UnusedLoopControlVariable, -}; -pub(crate) use useless_comparison::{useless_comparison, UselessComparison}; -pub(crate) use useless_contextlib_suppress::{ - useless_contextlib_suppress, UselessContextlibSuppress, -}; -pub(crate) use useless_expression::{useless_expression, UselessExpression}; -pub(crate) use zip_without_explicit_strict::{ - zip_without_explicit_strict, ZipWithoutExplicitStrict, -}; +pub(crate) use abstract_base_class::*; +pub(crate) use assert_false::*; +pub(crate) use assert_raises_exception::*; +pub(crate) use assignment_to_os_environ::*; +pub(crate) use cached_instance_method::*; +pub(crate) use duplicate_exceptions::*; +pub(crate) use duplicate_value::*; +pub(crate) use except_with_empty_tuple::*; +pub(crate) use except_with_non_exception_classes::*; +pub(crate) use f_string_docstring::*; +pub(crate) use function_call_argument_default::*; +pub(crate) use function_uses_loop_variable::*; +pub(crate) use getattr_with_constant::*; +pub(crate) use jump_statement_in_finally::*; +pub(crate) use loop_variable_overrides_iterator::*; +pub(crate) use mutable_argument_default::*; +pub(crate) use no_explicit_stacklevel::*; +pub(crate) use raise_literal::*; +pub(crate) use raise_without_from_inside_except::*; +pub(crate) use re_sub_positional_args::*; +pub(crate) use redundant_tuple_in_exception_handler::*; +pub(crate) use reuse_of_groupby_generator::*; +pub(crate) use setattr_with_constant::*; +pub(crate) use star_arg_unpacking_after_keyword_arg::*; +pub(crate) use strip_with_multi_characters::*; +pub(crate) use unary_prefix_increment::*; +pub(crate) use unintentional_type_annotation::*; +pub(crate) use unreliable_callable_check::*; +pub(crate) use unused_loop_control_variable::*; +pub(crate) use useless_comparison::*; +pub(crate) use useless_contextlib_suppress::*; +pub(crate) use useless_expression::*; +pub(crate) use zip_without_explicit_strict::*; mod abstract_base_class; mod assert_false; mod assert_raises_exception; mod assignment_to_os_environ; mod cached_instance_method; -mod cannot_raise_literal; mod duplicate_exceptions; mod duplicate_value; mod except_with_empty_tuple; @@ -78,7 +49,9 @@ mod jump_statement_in_finally; mod loop_variable_overrides_iterator; mod mutable_argument_default; mod no_explicit_stacklevel; +mod raise_literal; mod raise_without_from_inside_except; +mod re_sub_positional_args; mod redundant_tuple_in_exception_handler; mod reuse_of_groupby_generator; mod setattr_with_constant; diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/mutable_argument_default.rs b/crates/ruff/src/rules/flake8_bugbear/rules/mutable_argument_default.rs index 19698f8120..4af5133187 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/mutable_argument_default.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/mutable_argument_default.rs @@ -1,12 +1,51 @@ -use rustpython_parser::ast::{self, Arguments, Expr, Ranged}; +use rustpython_parser::ast::{ArgWithDefault, Arguments, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::analyze::typing::is_immutable_annotation; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::analyze::typing::{is_immutable_annotation, is_mutable_expr}; use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for uses of mutable objects as function argument defaults. +/// +/// ## Why is this bad? +/// Function defaults are evaluated once, when the function is defined. +/// +/// The same mutable object is then shared across all calls to the function. +/// If the object is modified, those modifications will persist across calls, +/// which can lead to unexpected behavior. +/// +/// Instead, prefer to use immutable data structures, or take `None` as a +/// default, and initialize a new mutable object inside the function body +/// for each call. +/// +/// ## Example +/// ```python +/// def add_to_list(item, some_list=[]): +/// some_list.append(item) +/// return some_list +/// +/// +/// l1 = add_to_list(0) # [0] +/// l2 = add_to_list(1) # [0, 1] +/// ``` +/// +/// Use instead: +/// ```python +/// def add_to_list(item, some_list=None): +/// if some_list is None: +/// some_list = [] +/// some_list.append(item) +/// return some_list +/// +/// +/// l1 = add_to_list(0) # [0] +/// l2 = add_to_list(1) # [1] +/// ``` +/// +/// ## References +/// - [Python documentation: Default Argument Values](https://docs.python.org/3/tutorial/controlflow.html#default-argument-values) #[violation] pub struct MutableArgumentDefault; @@ -16,57 +55,27 @@ impl Violation for MutableArgumentDefault { format!("Do not use mutable data structures for argument defaults") } } -const MUTABLE_FUNCS: &[&[&str]] = &[ - &["", "dict"], - &["", "list"], - &["", "set"], - &["collections", "Counter"], - &["collections", "OrderedDict"], - &["collections", "defaultdict"], - &["collections", "deque"], -]; - -pub(crate) fn is_mutable_func(model: &SemanticModel, func: &Expr) -> bool { - model.resolve_call_path(func).map_or(false, |call_path| { - MUTABLE_FUNCS - .iter() - .any(|target| call_path.as_slice() == *target) - }) -} - -fn is_mutable_expr(model: &SemanticModel, expr: &Expr) -> bool { - match expr { - Expr::List(_) - | Expr::Dict(_) - | Expr::Set(_) - | Expr::ListComp(_) - | Expr::DictComp(_) - | Expr::SetComp(_) => true, - Expr::Call(ast::ExprCall { func, .. }) => is_mutable_func(model, func), - _ => false, - } -} /// B006 pub(crate) fn mutable_argument_default(checker: &mut Checker, arguments: &Arguments) { // Scan in reverse order to right-align zip(). - for (arg, default) in arguments - .kwonlyargs + for ArgWithDefault { + def, + default, + range: _, + } in arguments + .posonlyargs .iter() - .rev() - .zip(arguments.kw_defaults.iter().rev()) - .chain( - arguments - .args - .iter() - .rev() - .chain(arguments.posonlyargs.iter().rev()) - .zip(arguments.defaults.iter().rev()), - ) + .chain(&arguments.args) + .chain(&arguments.kwonlyargs) { - if is_mutable_expr(checker.semantic_model(), default) - && !arg.annotation.as_ref().map_or(false, |expr| { - is_immutable_annotation(checker.semantic_model(), expr) + let Some(default) = default else { + continue; + }; + + if is_mutable_expr(default, checker.semantic()) + && !def.annotation.as_ref().map_or(false, |expr| { + is_immutable_annotation(expr, checker.semantic()) }) { checker diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs b/crates/ruff/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs index a7d20c2801..4d034dce4a 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs @@ -45,10 +45,10 @@ pub(crate) fn no_explicit_stacklevel( keywords: &[Keyword], ) { if !checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["warnings", "warn"] + matches!(call_path.as_slice(), ["warnings", "warn"]) }) { return; diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/raise_literal.rs b/crates/ruff/src/rules/flake8_bugbear/rules/raise_literal.rs new file mode 100644 index 0000000000..8adc4f0150 --- /dev/null +++ b/crates/ruff/src/rules/flake8_bugbear/rules/raise_literal.rs @@ -0,0 +1,45 @@ +use rustpython_parser::ast::{Expr, Ranged}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; + +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for `raise` statements that raise a literal value. +/// +/// ## Why is this bad? +/// `raise` must be followed by an exception instance or an exception class, +/// and exceptions must be instances of `BaseException` or a subclass thereof. +/// Raising a literal will raise a `TypeError` at runtime. +/// +/// ## Example +/// ```python +/// raise "foo" +/// ``` +/// +/// Use instead: +/// ```python +/// raise Exception("foo") +/// ``` +/// +/// ## References +/// - [Python documentation: `raise` statement](https://docs.python.org/3/reference/simple_stmts.html#the-raise-statement) +#[violation] +pub struct RaiseLiteral; + +impl Violation for RaiseLiteral { + #[derive_message_formats] + fn message(&self) -> String { + format!("Cannot raise a literal. Did you intend to return it or raise an Exception?") + } +} + +/// B016 +pub(crate) fn raise_literal(checker: &mut Checker, expr: &Expr) { + if expr.is_constant_expr() { + checker + .diagnostics + .push(Diagnostic::new(RaiseLiteral, expr.range())); + } +} diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/raise_without_from_inside_except.rs b/crates/ruff/src/rules/flake8_bugbear/rules/raise_without_from_inside_except.rs index ccb0d84c55..3495c16065 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/raise_without_from_inside_except.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/raise_without_from_inside_except.rs @@ -4,10 +4,48 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::RaiseStatementVisitor; use ruff_python_ast::statement_visitor::StatementVisitor; -use ruff_python_stdlib::str::is_lower; +use ruff_python_stdlib::str::is_cased_lowercase; use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for `raise` statements in exception handlers that lack a `from` +/// clause. +/// +/// ## Why is this bad? +/// In Python, `raise` can be used with or without an exception from which the +/// current exception is derived. This is known as exception chaining. When +/// printing the stack trace, chained exceptions are displayed in such a way +/// so as make it easier to trace the exception back to its root cause. +/// +/// When raising an exception from within an `except` clause, always include a +/// `from` clause to facilitate exception chaining. If the exception is not +/// chained, it will be difficult to trace the exception back to its root cause. +/// +/// ## Example +/// ```python +/// try: +/// ... +/// except FileNotFoundError: +/// if ...: +/// raise RuntimeError("...") +/// else: +/// raise UserWarning("...") +/// ``` +/// +/// Use instead: +/// ```python +/// try: +/// ... +/// except FileNotFoundError as exc: +/// if ...: +/// raise RuntimeError("...") from None +/// else: +/// raise UserWarning("...") from exc +/// ``` +/// +/// ## References +/// - [Python documentation: `raise` statement](https://docs.python.org/3/reference/simple_stmts.html#the-raise-statement) #[violation] pub struct RaiseWithoutFromInsideExcept; @@ -33,7 +71,7 @@ pub(crate) fn raise_without_from_inside_except(checker: &mut Checker, body: &[St if cause.is_none() { if let Some(exc) = exc { match exc { - Expr::Name(ast::ExprName { id, .. }) if is_lower(id) => {} + Expr::Name(ast::ExprName { id, .. }) if is_cased_lowercase(id) => {} _ => { checker .diagnostics diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/re_sub_positional_args.rs b/crates/ruff/src/rules/flake8_bugbear/rules/re_sub_positional_args.rs new file mode 100644 index 0000000000..5e9cdddb4c --- /dev/null +++ b/crates/ruff/src/rules/flake8_bugbear/rules/re_sub_positional_args.rs @@ -0,0 +1,112 @@ +use std::fmt; + +use rustpython_parser::ast::{self, Ranged}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; + +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for calls to `re.sub`, `re.subn`, and `re.split` that pass `count`, +/// `maxsplit`, or `flags` as positional arguments. +/// +/// ## Why is this bad? +/// Passing `count`, `maxsplit`, or `flags` as positional arguments to +/// `re.sub`, re.subn`, or `re.split` can lead to confusion, as most methods in +/// the `re` module accepts `flags` as the third positional argument, while +/// `re.sub`, `re.subn`, and `re.split` have different signatures. +/// +/// Instead, pass `count`, `maxsplit`, and `flags` as keyword arguments. +/// +/// ## Example +/// ```python +/// import re +/// +/// re.split("pattern", "replacement", 1) +/// ``` +/// +/// Use instead: +/// ```python +/// import re +/// +/// re.split("pattern", "replacement", maxsplit=1) +/// ``` +/// +/// ## References +/// - [Python documentation: `re.sub`](https://docs.python.org/3/library/re.html#re.sub) +/// - [Python documentation: `re.subn`](https://docs.python.org/3/library/re.html#re.subn) +/// - [Python documentation: `re.split`](https://docs.python.org/3/library/re.html#re.split) +#[violation] +pub struct ReSubPositionalArgs { + method: Method, +} + +impl Violation for ReSubPositionalArgs { + #[derive_message_formats] + fn message(&self) -> String { + let ReSubPositionalArgs { method } = self; + let param_name = method.param_name(); + format!( + "`{method}` should pass `{param_name}` and `flags` as keyword arguments to avoid confusion due to unintuitive argument positions" + ) + } +} + +/// B034 +pub(crate) fn re_sub_positional_args(checker: &mut Checker, call: &ast::ExprCall) { + let Some(method) = checker + .semantic() + .resolve_call_path(&call.func) + .and_then(|call_path| match call_path.as_slice() { + ["re", "sub"] => Some(Method::Sub), + ["re", "subn"] => Some(Method::Subn), + ["re", "split"] => Some(Method::Split), + _ => None, + }) + else { + return; + }; + + if call.args.len() > method.num_args() { + checker.diagnostics.push(Diagnostic::new( + ReSubPositionalArgs { method }, + call.range(), + )); + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +enum Method { + Sub, + Subn, + Split, +} + +impl fmt::Display for Method { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Sub => fmt.write_str("re.sub"), + Self::Subn => fmt.write_str("re.subn"), + Self::Split => fmt.write_str("re.split"), + } + } +} + +impl Method { + const fn param_name(self) -> &'static str { + match self { + Self::Sub => "count", + Self::Subn => "count", + Self::Split => "maxsplit", + } + } + + const fn num_args(self) -> usize { + match self { + Self::Sub => 3, + Self::Subn => 3, + Self::Split => 2, + } + } +} diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/redundant_tuple_in_exception_handler.rs b/crates/ruff/src/rules/flake8_bugbear/rules/redundant_tuple_in_exception_handler.rs index 9b0bbe6f29..865456d3f2 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/redundant_tuple_in_exception_handler.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/redundant_tuple_in_exception_handler.rs @@ -1,4 +1,4 @@ -use rustpython_parser::ast::{self, Excepthandler, Expr, Ranged}; +use rustpython_parser::ast::{self, ExceptHandler, Expr, Ranged}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; @@ -6,6 +6,32 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Checks for single-element tuples in exception handlers (e.g., +/// `except (ValueError,):`). +/// +/// ## Why is this bad? +/// A tuple with a single element can be more concisely and idiomatically +/// expressed as a single value. +/// +/// ## Example +/// ```python +/// try: +/// ... +/// except (ValueError,): +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// try: +/// ... +/// except ValueError: +/// ... +/// ``` +/// +/// ## References +/// - [Python documentation: `except` clause](https://docs.python.org/3/reference/compound_stmts.html#except-clause) #[violation] pub struct RedundantTupleInExceptionHandler { name: String, @@ -30,10 +56,14 @@ impl AlwaysAutofixableViolation for RedundantTupleInExceptionHandler { /// B013 pub(crate) fn redundant_tuple_in_exception_handler( checker: &mut Checker, - handlers: &[Excepthandler], + handlers: &[ExceptHandler], ) { for handler in handlers { - let Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { type_: Some(type_), .. }) = handler else { + let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { + type_: Some(type_), + .. + }) = handler + else { continue; }; let Expr::Tuple(ast::ExprTuple { elts, .. }) = type_.as_ref() else { diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/reuse_of_groupby_generator.rs b/crates/ruff/src/rules/flake8_bugbear/rules/reuse_of_groupby_generator.rs index 618af80195..1618f0f9b2 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/reuse_of_groupby_generator.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/reuse_of_groupby_generator.rs @@ -342,16 +342,16 @@ pub(crate) fn reuse_of_groupby_generator( }; // Check if the function call is `itertools.groupby` if !checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["itertools", "groupby"] + matches!(call_path.as_slice(), ["itertools", "groupby"]) }) { return; } let mut finder = GroupNameFinder::new(group_name); - for stmt in body.iter() { + for stmt in body { finder.visit_stmt(stmt); } for expr in finder.exprs { diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/setattr_with_constant.rs b/crates/ruff/src/rules/flake8_bugbear/rules/setattr_with_constant.rs index 9229339bf3..fe107f5fc0 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/setattr_with_constant.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/setattr_with_constant.rs @@ -1,5 +1,5 @@ use ruff_text_size::TextRange; -use rustpython_parser::ast::{self, Constant, Expr, ExprContext, Ranged, Stmt}; +use rustpython_parser::ast::{self, Constant, Expr, ExprContext, Identifier, Ranged, Stmt}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; @@ -9,6 +9,28 @@ use ruff_python_stdlib::identifiers::{is_identifier, is_mangled_private}; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Checks for uses of `setattr` that take a constant attribute value as an +/// argument (e.g., `setattr(obj, "foo", 42)`). +/// +/// ## Why is this bad? +/// `setattr` is used to set attributes dynamically. If the attribute is +/// defined as a constant, it is no safer than a typical property access. When +/// possible, prefer property access over `setattr` calls, as the former is +/// more concise and idiomatic. +/// +/// ## Example +/// ```python +/// setattr(obj, "foo", 42) +/// ``` +/// +/// Use instead: +/// ```python +/// obj.foo = 42 +/// ``` +/// +/// ## References +/// - [Python documentation: `setattr`](https://docs.python.org/3/library/functions.html#setattr) #[violation] pub struct SetAttrWithConstant; @@ -30,7 +52,7 @@ fn assignment(obj: &Expr, name: &str, value: &Expr, generator: Generator) -> Str let stmt = Stmt::Assign(ast::StmtAssign { targets: vec![Expr::Attribute(ast::ExprAttribute { value: Box::new(obj.clone()), - attr: name.into(), + attr: Identifier::new(name.to_string(), TextRange::default()), ctx: ExprContext::Store, range: TextRange::default(), })], @@ -60,7 +82,8 @@ pub(crate) fn setattr_with_constant( let Expr::Constant(ast::ExprConstant { value: Constant::Str(name), .. - } )= name else { + }) = name + else { return; }; if !is_identifier(name) { @@ -75,7 +98,7 @@ pub(crate) fn setattr_with_constant( if let Stmt::Expr(ast::StmtExpr { value: child, range: _, - }) = checker.semantic_model().stmt() + }) = checker.semantic().stmt() { if expr == child.as_ref() { let mut diagnostic = Diagnostic::new(SetAttrWithConstant, expr.range()); diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/star_arg_unpacking_after_keyword_arg.rs b/crates/ruff/src/rules/flake8_bugbear/rules/star_arg_unpacking_after_keyword_arg.rs index 287f67035f..dae1e2168e 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/star_arg_unpacking_after_keyword_arg.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/star_arg_unpacking_after_keyword_arg.rs @@ -1,12 +1,3 @@ -//! Checks for `f(x=0, *(1, 2))`. -//! -//! ## Why is this bad? -//! -//! Star-arg unpacking after a keyword argument is strongly discouraged. It only -//! works when the keyword parameter is declared after all parameters supplied -//! by the unpacked sequence, and this change of ordering can surprise and -//! mislead readers. - use rustpython_parser::ast::{Expr, Keyword, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; @@ -14,6 +5,45 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for function calls that use star-argument unpacking after providing a +/// keyword argument +/// +/// ## Why is this bad? +/// In Python, you can use star-argument unpacking to pass a list or tuple of +/// arguments to a function. +/// +/// Providing a star-argument after a keyword argument can lead to confusing +/// behavior, and is only supported for backwards compatibility. +/// +/// ## Example +/// ```python +/// def foo(x, y, z): +/// return x, y, z +/// +/// +/// foo(1, 2, 3) # (1, 2, 3) +/// foo(1, *[2, 3]) # (1, 2, 3) +/// # foo(x=1, *[2, 3]) # TypeError +/// # foo(y=2, *[1, 3]) # TypeError +/// foo(z=3, *[1, 2]) # (1, 2, 3) # No error, but confusing! +/// ``` +/// +/// Use instead: +/// ```python +/// def foo(x, y, z): +/// return x, y, z +/// +/// +/// foo(1, 2, 3) # (1, 2, 3) +/// foo(x=1, y=2, z=3) # (1, 2, 3) +/// foo(*[1, 2, 3]) # (1, 2, 3) +/// foo(*[1, 2], 3) # (1, 2, 3) +/// ``` +/// +/// ## References +/// - [Python documentation: Calls](https://docs.python.org/3/reference/expressions.html#calls) +/// - [Disallow iterable argument unpacking after a keyword argument?](https://github.com/python/cpython/issues/82741) #[violation] pub struct StarArgUnpackingAfterKeywordArg; @@ -34,7 +64,7 @@ pub(crate) fn star_arg_unpacking_after_keyword_arg( return; }; for arg in args { - let Expr::Starred (_) = arg else { + let Expr::Starred(_) = arg else { continue; }; if arg.start() <= keyword.start() { diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/strip_with_multi_characters.rs b/crates/ruff/src/rules/flake8_bugbear/rules/strip_with_multi_characters.rs index 1fd78729c9..2098a2a796 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/strip_with_multi_characters.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/strip_with_multi_characters.rs @@ -6,6 +6,32 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for uses of multi-character strings in `.strip()`, `.lstrip()`, and +/// `.rstrip()` calls. +/// +/// ## Why is this bad? +/// All characters in the call to `.strip()`, `.lstrip()`, or `.rstrip()` are +/// removed from the leading and trailing ends of the string. If the string +/// contains multiple characters, the reader may be misled into thinking that +/// a prefix or suffix is being removed, rather than a set of characters. +/// +/// In Python 3.9 and later, you can use `str#removeprefix` and +/// `str#removesuffix` to remove an exact prefix or suffix from a string, +/// respectively, which should be preferred when possible. +/// +/// ## Example +/// ```python +/// "abcba".strip("ab") # "c" +/// ``` +/// +/// Use instead: +/// ```python +/// "abcba".removeprefix("ab").removesuffix("ba") # "c" +/// ``` +/// +/// ## References +/// - [Python documentation: `str.strip`](https://docs.python.org/3/library/stdtypes.html#str.strip) #[violation] pub struct StripWithMultiCharacters; @@ -36,7 +62,8 @@ pub(crate) fn strip_with_multi_characters( let Expr::Constant(ast::ExprConstant { value: Constant::Str(value), .. - } )= &args[0] else { + }) = &args[0] + else { return; }; diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/unary_prefix_increment.rs b/crates/ruff/src/rules/flake8_bugbear/rules/unary_prefix_increment.rs index 12f70b9044..e9a096aacc 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/unary_prefix_increment.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/unary_prefix_increment.rs @@ -1,29 +1,30 @@ -//! Checks for `++n`. -//! -//! ## Why is this bad? -//! -//! Python does not support the unary prefix increment. Writing `++n` is -//! equivalent to `+(+(n))`, which equals `n`. -//! -//! ## Example -//! -//! ```python -//! ++n; -//! ``` -//! -//! Use instead: -//! -//! ```python -//! n += 1 -//! ``` - -use rustpython_parser::ast::{self, Expr, Ranged, Unaryop}; +use rustpython_parser::ast::{self, Expr, Ranged, UnaryOp}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for uses of the unary prefix increment operator (e.g., `++n`). +/// +/// ## Why is this bad? +/// Python does not support the unary prefix increment operator. Writing `++n` +/// is equivalent to `+(+(n))`, which is equivalent to `n`. +/// +/// ## Example +/// ```python +/// ++n +/// ``` +/// +/// Use instead: +/// ```python +/// n += 1 +/// ``` +/// +/// ## References +/// - [Python documentation: Unary arithmetic and bitwise operations](https://docs.python.org/3/reference/expressions.html#unary-arithmetic-and-bitwise-operations) +/// - [Python documentation: Augmented assignment statements](https://docs.python.org/3/reference/simple_stmts.html#augmented-assignment-statements) #[violation] pub struct UnaryPrefixIncrement; @@ -38,16 +39,16 @@ impl Violation for UnaryPrefixIncrement { pub(crate) fn unary_prefix_increment( checker: &mut Checker, expr: &Expr, - op: Unaryop, + op: UnaryOp, operand: &Expr, ) { - if !matches!(op, Unaryop::UAdd) { + if !matches!(op, UnaryOp::UAdd) { return; } - let Expr::UnaryOp(ast::ExprUnaryOp { op, .. })= operand else { - return; - }; - if !matches!(op, Unaryop::UAdd) { + let Expr::UnaryOp(ast::ExprUnaryOp { op, .. }) = operand else { + return; + }; + if !matches!(op, UnaryOp::UAdd) { return; } checker diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/unreliable_callable_check.rs b/crates/ruff/src/rules/flake8_bugbear/rules/unreliable_callable_check.rs index ec1cff4635..34e209ee48 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/unreliable_callable_check.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/unreliable_callable_check.rs @@ -5,6 +5,32 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for uses of `hasattr` to test if an object is callable (e.g., +/// `hasattr(obj, "__call__")`). +/// +/// ## Why is this bad? +/// Using `hasattr` is an unreliable mechanism for testing if an object is +/// callable. If `obj` implements a custom `__getattr__`, or if its `__call__` +/// is itself not callable, you may get misleading results. +/// +/// Instead, use `callable(obj)` to test if `obj` is callable. +/// +/// ## Example +/// ```python +/// hasattr(obj, "__call__") +/// ``` +/// +/// Use instead: +/// ```python +/// callable(obj) +/// ``` +/// +/// ## References +/// - [Python documentation: `callable`](https://docs.python.org/3/library/functions.html#callable) +/// - [Python documentation: `hasattr`](https://docs.python.org/3/library/functions.html#hasattr) +/// - [Python documentation: `__getattr__`](https://docs.python.org/3/reference/datamodel.html#object.__getattr__) +/// - [Python documentation: `__call__`](https://docs.python.org/3/reference/datamodel.html#object.__call__) #[violation] pub struct UnreliableCallableCheck; @@ -37,8 +63,8 @@ pub(crate) fn unreliable_callable_check( let Expr::Constant(ast::ExprConstant { value: Constant::Str(s), .. - }) = &args[1] else - { + }) = &args[1] + else { return; }; if s != "__call__" { diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/unused_loop_control_variable.rs b/crates/ruff/src/rules/flake8_bugbear/rules/unused_loop_control_variable.rs index ad64d880b6..5eb68d0119 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/unused_loop_control_variable.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/unused_loop_control_variable.rs @@ -1,26 +1,5 @@ -//! Checks for unused loop variables. -//! -//! ## Why is this bad? -//! -//! Unused variables may signal a mistake or unfinished code. -//! -//! ## Example -//! -//! ```python -//! for x in range(10): -//! method() -//! ``` -//! -//! Prefix the variable with an underscore: -//! -//! ```python -//! for _x in range(10): -//! method() -//! ``` - use rustc_hash::FxHashMap; use rustpython_parser::ast::{self, Expr, Ranged, Stmt}; -use serde::{Deserialize, Serialize}; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -30,12 +9,37 @@ use ruff_python_ast::{helpers, visitor}; use crate::checkers::ast::Checker; use crate::registry::AsRule; -#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, result_like::BoolLike)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, result_like::BoolLike)] enum Certainty { Certain, Uncertain, } +/// ## What it does +/// Checks for unused variables in loops (e.g., `for` and `while` statements). +/// +/// ## Why is this bad? +/// Defining a variable in a loop statement that is never used can confuse +/// readers. +/// +/// If the variable is intended to be unused (e.g., to facilitate +/// destructuring of a tuple or other object), prefix it with an underscore +/// to indicate the intent. Otherwise, remove the variable entirely. +/// +/// ## Example +/// ```python +/// for i, j in foo: +/// bar(i) +/// ``` +/// +/// Use instead: +/// ```python +/// for i, _j in foo: +/// bar(i) +/// ``` +/// +/// ## References +/// - [PEP 8: Naming Conventions](https://peps.python.org/pep-0008/#naming-conventions) #[violation] pub struct UnusedLoopControlVariable { /// The name of the loop control variable. @@ -129,7 +133,7 @@ pub(crate) fn unused_loop_control_variable(checker: &mut Checker, target: &Expr, // Avoid fixing any variables that _may_ be used, but undetectably so. let certainty = Certainty::from(!helpers::uses_magic_variable_access(body, |id| { - checker.semantic_model().is_builtin(id) + checker.semantic().is_builtin(id) })); // Attempt to rename the variable by prepending an underscore, but avoid @@ -154,10 +158,10 @@ pub(crate) fn unused_loop_control_variable(checker: &mut Checker, target: &Expr, if certainty.into() && checker.patch(diagnostic.kind.rule()) { // Avoid fixing if the variable, or any future bindings to the variable, are // used _after_ the loop. - let scope = checker.semantic_model().scope(); + let scope = checker.semantic().scope(); if scope - .bindings_for_name(name) - .map(|binding_id| &checker.semantic_model().bindings[binding_id]) + .get_all(name) + .map(|binding_id| checker.semantic().binding(binding_id)) .all(|binding| !binding.is_used()) { diagnostic.set_fix(Fix::suggested(Edit::range_replacement( diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/useless_comparison.rs b/crates/ruff/src/rules/flake8_bugbear/rules/useless_comparison.rs index 746acc8ec9..1654ef13ce 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/useless_comparison.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/useless_comparison.rs @@ -5,6 +5,26 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for useless comparisons. +/// +/// ## Why is this bad? +/// Useless comparisons have no effect on the program, and are often included +/// by mistake. If the comparison is intended to enforce an invariant, prepend +/// the comparison with an `assert`. Otherwise, remove it entirely. +/// +/// ## Example +/// ```python +/// foo == bar +/// ``` +/// +/// Use instead: +/// ```python +/// assert foo == bar, "`foo` and `bar` should be equal." +/// ``` +/// +/// ## References +/// - [Python documentation: `assert` statement](https://docs.python.org/3/reference/simple_stmts.html#the-assert-statement) #[violation] pub struct UselessComparison; diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/useless_contextlib_suppress.rs b/crates/ruff/src/rules/flake8_bugbear/rules/useless_contextlib_suppress.rs index 456d47af04..8e734d70cb 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/useless_contextlib_suppress.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/useless_contextlib_suppress.rs @@ -5,6 +5,36 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for `contextlib.suppress` without arguments. +/// +/// ## Why is this bad? +/// `contextlib.suppress` is a context manager that suppresses exceptions. It takes, +/// as arguments, the exceptions to suppress within the enclosed block. If no +/// exceptions are specified, then the context manager won't suppress any +/// exceptions, and is thus redundant. +/// +/// Consider adding exceptions to the `contextlib.suppress` call, or removing the +/// context manager entirely. +/// +/// ## Example +/// ```python +/// import contextlib +/// +/// with contextlib.suppress(): +/// foo() +/// ``` +/// +/// Use instead: +/// ```python +/// import contextlib +/// +/// with contextlib.suppress(Exception): +/// foo() +/// ``` +/// +/// ## References +/// - [Python documentation: contextlib.suppress](https://docs.python.org/3/library/contextlib.html#contextlib.suppress) #[violation] pub struct UselessContextlibSuppress; @@ -27,10 +57,10 @@ pub(crate) fn useless_contextlib_suppress( ) { if args.is_empty() && checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["contextlib", "suppress"] + matches!(call_path.as_slice(), ["contextlib", "suppress"]) }) { checker diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/useless_expression.rs b/crates/ruff/src/rules/flake8_bugbear/rules/useless_expression.rs index 00376dc8b0..24361c1895 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/useless_expression.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/useless_expression.rs @@ -6,12 +6,23 @@ use ruff_python_ast::helpers::contains_effect; use crate::checkers::ast::Checker; -#[derive(Debug, PartialEq, Eq, Copy, Clone)] -pub(crate) enum Kind { - Expression, - Attribute, -} - +/// ## What it does +/// Checks for useless expressions. +/// +/// ## Why is this bad? +/// Useless expressions have no effect on the program, and are often included +/// by mistake. Assign a useless expression to a variable, or remove it +/// entirely. +/// +/// ## Example +/// ```python +/// 1 + 1 +/// ``` +/// +/// Use instead: +/// ```python +/// foo = 1 + 1 +/// ``` #[violation] pub struct UselessExpression { kind: Kind, @@ -53,7 +64,7 @@ pub(crate) fn useless_expression(checker: &mut Checker, value: &Expr) { } // Ignore statements that have side effects. - if contains_effect(value, |id| checker.semantic_model().is_builtin(id)) { + if contains_effect(value, |id| checker.semantic().is_builtin(id)) { // Flag attributes as useless expressions, even if they're attached to calls or other // expressions. if matches!(value, Expr::Attribute(_)) { @@ -74,3 +85,9 @@ pub(crate) fn useless_expression(checker: &mut Checker, value: &Expr) { value.range(), )); } + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +enum Kind { + Expression, + Attribute, +} diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs b/crates/ruff/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs index 3bbc2efbac..4a4efd4e3c 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs @@ -3,10 +3,33 @@ use rustpython_parser::ast::{self, Expr, Keyword, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::is_const_none; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for `zip` calls without an explicit `strict` parameter. +/// +/// ## Why is this bad? +/// By default, if the iterables passed to `zip` are of different lengths, the +/// resulting iterator will be silently truncated to the length of the shortest +/// iterable. This can lead to subtle bugs. +/// +/// Use the `strict` parameter to raise a `ValueError` if the iterables are of +/// non-uniform length. +/// +/// ## Example +/// ```python +/// zip(a, b) +/// ``` +/// +/// Use instead: +/// ```python +/// zip(a, b, strict=True) +/// ``` +/// +/// ## References +/// - [Python documentation: `zip`](https://docs.python.org/3/library/functions.html#zip) #[violation] pub struct ZipWithoutExplicitStrict; @@ -27,13 +50,13 @@ pub(crate) fn zip_without_explicit_strict( ) { if let Expr::Name(ast::ExprName { id, .. }) = func { if id == "zip" - && checker.semantic_model().is_builtin("zip") + && checker.semantic().is_builtin("zip") && !kwargs .iter() .any(|keyword| keyword.arg.as_ref().map_or(false, |name| name == "strict")) && !args .iter() - .any(|arg| is_infinite_iterator(arg, checker.semantic_model())) + .any(|arg| is_infinite_iterator(arg, checker.semantic())) { checker .diagnostics @@ -44,14 +67,19 @@ pub(crate) fn zip_without_explicit_strict( /// Return `true` if the [`Expr`] appears to be an infinite iterator (e.g., a call to /// `itertools.cycle` or similar). -fn is_infinite_iterator(arg: &Expr, model: &SemanticModel) -> bool { - let Expr::Call(ast::ExprCall { func, args, keywords, .. }) = &arg else { +fn is_infinite_iterator(arg: &Expr, semantic: &SemanticModel) -> bool { + let Expr::Call(ast::ExprCall { + func, + args, + keywords, + .. + }) = &arg + else { return false; }; - return model - .resolve_call_path(func) - .map_or(false, |call_path| match call_path.as_slice() { + semantic.resolve_call_path(func).map_or(false, |call_path| { + match call_path.as_slice() { ["itertools", "cycle" | "count"] => true, ["itertools", "repeat"] => { // Ex) `itertools.repeat(1)` @@ -76,5 +104,6 @@ fn is_infinite_iterator(arg: &Expr, model: &SemanticModel) -> bool { false } _ => false, - }); + } + }) } diff --git a/crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B014_B014.py.snap b/crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B014_B014.py.snap index 50c0748163..bb8fb6c3c2 100644 --- a/crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B014_B014.py.snap +++ b/crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B014_B014.py.snap @@ -12,7 +12,7 @@ B014.py:17:8: B014 [*] Exception handler with duplicate exception: `OSError` | = help: De-duplicate exceptions -ℹ Suggested fix +ℹ Fix 14 14 | 15 15 | try: 16 16 | pass @@ -33,7 +33,7 @@ B014.py:28:8: B014 [*] Exception handler with duplicate exception: `MyError` | = help: De-duplicate exceptions -ℹ Suggested fix +ℹ Fix 25 25 | 26 26 | try: 27 27 | pass @@ -54,7 +54,7 @@ B014.py:49:8: B014 [*] Exception handler with duplicate exception: `re.error` | = help: De-duplicate exceptions -ℹ Suggested fix +ℹ Fix 46 46 | 47 47 | try: 48 48 | pass diff --git a/crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B017_B017.py.snap b/crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B017_B017.py.snap index f46fd7d74e..d6a332dfc2 100644 --- a/crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B017_B017.py.snap +++ b/crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B017_B017.py.snap @@ -1,27 +1,38 @@ --- source: crates/ruff/src/rules/flake8_bugbear/mod.rs --- -B017.py:23:9: B017 `assertRaises(Exception)` should be considered evil +B017.py:23:14: B017 `assertRaises(Exception)` should be considered evil | -21 | class Foobar(unittest.TestCase): -22 | def evil_raises(self) -> None: -23 | with self.assertRaises(Exception): - | _________^ -24 | | raise Exception("Evil I say!") - | |__________________________________________^ B017 -25 | -26 | def context_manager_raises(self) -> None: +21 | class Foobar(unittest.TestCase): +22 | def evil_raises(self) -> None: +23 | with self.assertRaises(Exception): + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B017 +24 | raise Exception("Evil I say!") | -B017.py:41:5: B017 `pytest.raises(Exception)` should be considered evil +B017.py:27:14: B017 `assertRaises(BaseException)` should be considered evil | -40 | def test_pytest_raises(): -41 | with pytest.raises(Exception): - | _____^ -42 | | raise ValueError("Hello") - | |_________________________________^ B017 -43 | -44 | with pytest.raises(Exception, "hello"): +26 | def also_evil_raises(self) -> None: +27 | with self.assertRaises(BaseException): + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B017 +28 | raise Exception("Evil I say!") + | + +B017.py:45:10: B017 `pytest.raises(Exception)` should be considered evil + | +44 | def test_pytest_raises(): +45 | with pytest.raises(Exception): + | ^^^^^^^^^^^^^^^^^^^^^^^^ B017 +46 | raise ValueError("Hello") + | + +B017.py:48:10: B017 `pytest.raises(Exception)` should be considered evil + | +46 | raise ValueError("Hello") +47 | +48 | with pytest.raises(Exception), pytest.raises(ValueError): + | ^^^^^^^^^^^^^^^^^^^^^^^^ B017 +49 | raise ValueError("Hello") | diff --git a/crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B034_B034.py.snap b/crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B034_B034.py.snap new file mode 100644 index 0000000000..44efbb7d97 --- /dev/null +++ b/crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B034_B034.py.snap @@ -0,0 +1,102 @@ +--- +source: crates/ruff/src/rules/flake8_bugbear/mod.rs +--- +B034.py:5:1: B034 `re.sub` should pass `count` and `flags` as keyword arguments to avoid confusion due to unintuitive argument positions + | +4 | # B034 +5 | re.sub("a", "b", "aaa", re.IGNORECASE) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B034 +6 | re.sub("a", "b", "aaa", 5) +7 | re.sub("a", "b", "aaa", 5, re.IGNORECASE) + | + +B034.py:6:1: B034 `re.sub` should pass `count` and `flags` as keyword arguments to avoid confusion due to unintuitive argument positions + | +4 | # B034 +5 | re.sub("a", "b", "aaa", re.IGNORECASE) +6 | re.sub("a", "b", "aaa", 5) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ B034 +7 | re.sub("a", "b", "aaa", 5, re.IGNORECASE) +8 | re.subn("a", "b", "aaa", re.IGNORECASE) + | + +B034.py:7:1: B034 `re.sub` should pass `count` and `flags` as keyword arguments to avoid confusion due to unintuitive argument positions + | +5 | re.sub("a", "b", "aaa", re.IGNORECASE) +6 | re.sub("a", "b", "aaa", 5) +7 | re.sub("a", "b", "aaa", 5, re.IGNORECASE) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B034 +8 | re.subn("a", "b", "aaa", re.IGNORECASE) +9 | re.subn("a", "b", "aaa", 5) + | + +B034.py:8:1: B034 `re.subn` should pass `count` and `flags` as keyword arguments to avoid confusion due to unintuitive argument positions + | + 6 | re.sub("a", "b", "aaa", 5) + 7 | re.sub("a", "b", "aaa", 5, re.IGNORECASE) + 8 | re.subn("a", "b", "aaa", re.IGNORECASE) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B034 + 9 | re.subn("a", "b", "aaa", 5) +10 | re.subn("a", "b", "aaa", 5, re.IGNORECASE) + | + +B034.py:9:1: B034 `re.subn` should pass `count` and `flags` as keyword arguments to avoid confusion due to unintuitive argument positions + | + 7 | re.sub("a", "b", "aaa", 5, re.IGNORECASE) + 8 | re.subn("a", "b", "aaa", re.IGNORECASE) + 9 | re.subn("a", "b", "aaa", 5) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ B034 +10 | re.subn("a", "b", "aaa", 5, re.IGNORECASE) +11 | re.split(" ", "a a a a", re.I) + | + +B034.py:10:1: B034 `re.subn` should pass `count` and `flags` as keyword arguments to avoid confusion due to unintuitive argument positions + | + 8 | re.subn("a", "b", "aaa", re.IGNORECASE) + 9 | re.subn("a", "b", "aaa", 5) +10 | re.subn("a", "b", "aaa", 5, re.IGNORECASE) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B034 +11 | re.split(" ", "a a a a", re.I) +12 | re.split(" ", "a a a a", 2) + | + +B034.py:11:1: B034 `re.split` should pass `maxsplit` and `flags` as keyword arguments to avoid confusion due to unintuitive argument positions + | + 9 | re.subn("a", "b", "aaa", 5) +10 | re.subn("a", "b", "aaa", 5, re.IGNORECASE) +11 | re.split(" ", "a a a a", re.I) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B034 +12 | re.split(" ", "a a a a", 2) +13 | re.split(" ", "a a a a", 2, re.I) + | + +B034.py:12:1: B034 `re.split` should pass `maxsplit` and `flags` as keyword arguments to avoid confusion due to unintuitive argument positions + | +10 | re.subn("a", "b", "aaa", 5, re.IGNORECASE) +11 | re.split(" ", "a a a a", re.I) +12 | re.split(" ", "a a a a", 2) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ B034 +13 | re.split(" ", "a a a a", 2, re.I) +14 | sub("a", "b", "aaa", re.IGNORECASE) + | + +B034.py:13:1: B034 `re.split` should pass `maxsplit` and `flags` as keyword arguments to avoid confusion due to unintuitive argument positions + | +11 | re.split(" ", "a a a a", re.I) +12 | re.split(" ", "a a a a", 2) +13 | re.split(" ", "a a a a", 2, re.I) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B034 +14 | sub("a", "b", "aaa", re.IGNORECASE) + | + +B034.py:14:1: B034 `re.sub` should pass `count` and `flags` as keyword arguments to avoid confusion due to unintuitive argument positions + | +12 | re.split(" ", "a a a a", 2) +13 | re.split(" ", "a a a a", 2, re.I) +14 | sub("a", "b", "aaa", re.IGNORECASE) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B034 +15 | +16 | # OK + | + + diff --git a/crates/ruff/src/rules/flake8_builtins/helpers.rs b/crates/ruff/src/rules/flake8_builtins/helpers.rs index 1f1eb0f3ba..1fe69e0cb4 100644 --- a/crates/ruff/src/rules/flake8_builtins/helpers.rs +++ b/crates/ruff/src/rules/flake8_builtins/helpers.rs @@ -1,9 +1,8 @@ -use rustpython_parser::ast::{Excepthandler, Expr, Ranged, Stmt}; - -use ruff_python_ast::helpers::identifier_range; -use ruff_python_ast::source_code::Locator; -use ruff_python_stdlib::builtins::BUILTINS; use ruff_text_size::TextRange; +use rustpython_parser::ast::{ExceptHandler, Expr, Ranged, Stmt}; + +use ruff_python_ast::identifier::{Identifier, TryIdentifier}; +use ruff_python_stdlib::builtins::BUILTINS; pub(super) fn shadows_builtin(name: &str, ignorelist: &[String]) -> bool { BUILTINS.contains(&name) && ignorelist.iter().all(|ignore| ignore != name) @@ -13,15 +12,15 @@ pub(super) fn shadows_builtin(name: &str, ignorelist: &[String]) -> bool { pub(crate) enum AnyShadowing<'a> { Expression(&'a Expr), Statement(&'a Stmt), - ExceptHandler(&'a Excepthandler), + ExceptHandler(&'a ExceptHandler), } -impl AnyShadowing<'_> { - pub(crate) fn range(self, locator: &Locator) -> TextRange { +impl Identifier for AnyShadowing<'_> { + fn identifier(&self) -> TextRange { match self { AnyShadowing::Expression(expr) => expr.range(), - AnyShadowing::Statement(stmt) => identifier_range(stmt, locator), - AnyShadowing::ExceptHandler(handler) => handler.range(), + AnyShadowing::Statement(stmt) => stmt.identifier(), + AnyShadowing::ExceptHandler(handler) => handler.try_identifier().unwrap(), } } } @@ -38,8 +37,8 @@ impl<'a> From<&'a Expr> for AnyShadowing<'a> { } } -impl<'a> From<&'a Excepthandler> for AnyShadowing<'a> { - fn from(value: &'a Excepthandler) -> Self { +impl<'a> From<&'a ExceptHandler> for AnyShadowing<'a> { + fn from(value: &'a ExceptHandler) -> Self { AnyShadowing::ExceptHandler(value) } } diff --git a/crates/ruff/src/rules/flake8_builtins/rules/builtin_argument_shadowing.rs b/crates/ruff/src/rules/flake8_builtins/rules/builtin_argument_shadowing.rs index e6aea40d3d..f2e95545e5 100644 --- a/crates/ruff/src/rules/flake8_builtins/rules/builtin_argument_shadowing.rs +++ b/crates/ruff/src/rules/flake8_builtins/rules/builtin_argument_shadowing.rs @@ -20,10 +20,6 @@ use super::super::helpers::shadows_builtin; /// Builtins can be marked as exceptions to this rule via the /// [`flake8-builtins.builtins-ignorelist`] configuration option. /// -/// ## Options -/// -/// - `flake8-builtins.builtins-ignorelist` -/// /// ## Example /// ```python /// def remove_duplicates(list, list2): @@ -46,6 +42,9 @@ use super::super::helpers::shadows_builtin; /// return list(result) /// ``` /// +/// ## Options +/// - `flake8-builtins.builtins-ignorelist` +/// /// ## References /// - [_Is it bad practice to use a built-in function name as an attribute or method identifier?_](https://stackoverflow.com/questions/9109333/is-it-bad-practice-to-use-a-built-in-function-name-as-an-attribute-or-method-ide) /// - [_Why is it a bad idea to name a variable `id` in Python?_](https://stackoverflow.com/questions/77552/id-is-a-bad-variable-name-in-python) diff --git a/crates/ruff/src/rules/flake8_builtins/rules/builtin_attribute_shadowing.rs b/crates/ruff/src/rules/flake8_builtins/rules/builtin_attribute_shadowing.rs index 701fa4edd3..45edcb2ef7 100644 --- a/crates/ruff/src/rules/flake8_builtins/rules/builtin_attribute_shadowing.rs +++ b/crates/ruff/src/rules/flake8_builtins/rules/builtin_attribute_shadowing.rs @@ -1,6 +1,8 @@ use ruff_diagnostics::Diagnostic; use ruff_diagnostics::Violation; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::identifier::Identifier; +use rustpython_parser::ast; use crate::checkers::ast::Checker; @@ -19,10 +21,6 @@ use super::super::helpers::{shadows_builtin, AnyShadowing}; /// [`flake8-builtins.builtins-ignorelist`] configuration option, or /// converted to the appropriate dunder method. /// -/// ## Options -/// -/// - `flake8-builtins.builtins-ignorelist` -/// /// ## Example /// ```python /// class Shadow: @@ -45,6 +43,9 @@ use super::super::helpers::{shadows_builtin, AnyShadowing}; /// return 0 /// ``` /// +/// ## Options +/// - `flake8-builtins.builtins-ignorelist` +/// /// ## References /// - [_Is it bad practice to use a built-in function name as an attribute or method identifier?_](https://stackoverflow.com/questions/9109333/is-it-bad-practice-to-use-a-built-in-function-name-as-an-attribute-or-method-ide) /// - [_Why is it a bad idea to name a variable `id` in Python?_](https://stackoverflow.com/questions/77552/id-is-a-bad-variable-name-in-python) @@ -64,15 +65,26 @@ impl Violation for BuiltinAttributeShadowing { /// A003 pub(crate) fn builtin_attribute_shadowing( checker: &mut Checker, + class_def: &ast::StmtClassDef, name: &str, shadowing: AnyShadowing, ) { if shadows_builtin(name, &checker.settings.flake8_builtins.builtins_ignorelist) { + // Ignore shadowing within `TypedDict` definitions, since these are only accessible through + // subscripting and not through attribute access. + if class_def + .bases + .iter() + .any(|base| checker.semantic().match_typing_expr(base, "TypedDict")) + { + return; + } + checker.diagnostics.push(Diagnostic::new( BuiltinAttributeShadowing { name: name.to_string(), }, - shadowing.range(checker.locator), + shadowing.identifier(), )); } } diff --git a/crates/ruff/src/rules/flake8_builtins/rules/builtin_variable_shadowing.rs b/crates/ruff/src/rules/flake8_builtins/rules/builtin_variable_shadowing.rs index a965af53ca..d61aff2e57 100644 --- a/crates/ruff/src/rules/flake8_builtins/rules/builtin_variable_shadowing.rs +++ b/crates/ruff/src/rules/flake8_builtins/rules/builtin_variable_shadowing.rs @@ -1,6 +1,7 @@ use ruff_diagnostics::Diagnostic; use ruff_diagnostics::Violation; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::identifier::Identifier; use crate::checkers::ast::Checker; @@ -19,10 +20,6 @@ use super::super::helpers::{shadows_builtin, AnyShadowing}; /// Builtins can be marked as exceptions to this rule via the /// [`flake8-builtins.builtins-ignorelist`] configuration option. /// -/// ## Options -/// -/// - `flake8-builtins.builtins-ignorelist` -/// /// ## Example /// ```python /// def find_max(list_of_lists): @@ -43,6 +40,10 @@ use super::super::helpers::{shadows_builtin, AnyShadowing}; /// return result /// ``` /// +/// ## Options +/// - `flake8-builtins.builtins-ignorelist` +/// +/// ## References /// - [_Why is it a bad idea to name a variable `id` in Python?_](https://stackoverflow.com/questions/77552/id-is-a-bad-variable-name-in-python) #[violation] pub struct BuiltinVariableShadowing { @@ -68,7 +69,7 @@ pub(crate) fn builtin_variable_shadowing( BuiltinVariableShadowing { name: name.to_string(), }, - shadowing.range(checker.locator), + shadowing.identifier(), )); } } diff --git a/crates/ruff/src/rules/flake8_builtins/rules/mod.rs b/crates/ruff/src/rules/flake8_builtins/rules/mod.rs index f9b8c3c3d7..d81afec0d6 100644 --- a/crates/ruff/src/rules/flake8_builtins/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_builtins/rules/mod.rs @@ -1,8 +1,6 @@ -pub(crate) use builtin_argument_shadowing::{builtin_argument_shadowing, BuiltinArgumentShadowing}; -pub(crate) use builtin_attribute_shadowing::{ - builtin_attribute_shadowing, BuiltinAttributeShadowing, -}; -pub(crate) use builtin_variable_shadowing::{builtin_variable_shadowing, BuiltinVariableShadowing}; +pub(crate) use builtin_argument_shadowing::*; +pub(crate) use builtin_attribute_shadowing::*; +pub(crate) use builtin_variable_shadowing::*; mod builtin_argument_shadowing; mod builtin_attribute_shadowing; diff --git a/crates/ruff/src/rules/flake8_builtins/snapshots/ruff__rules__flake8_builtins__tests__A001_A001.py.snap b/crates/ruff/src/rules/flake8_builtins/snapshots/ruff__rules__flake8_builtins__tests__A001_A001.py.snap index f62577f0f3..4aa57c119f 100644 --- a/crates/ruff/src/rules/flake8_builtins/snapshots/ruff__rules__flake8_builtins__tests__A001_A001.py.snap +++ b/crates/ruff/src/rules/flake8_builtins/snapshots/ruff__rules__flake8_builtins__tests__A001_A001.py.snap @@ -122,15 +122,13 @@ A001.py:16:7: A001 Variable `slice` is shadowing a Python builtin 17 | pass | -A001.py:21:1: A001 Variable `ValueError` is shadowing a Python builtin +A001.py:21:23: A001 Variable `ValueError` is shadowing a Python builtin | -19 | try: -20 | ... -21 | / except ImportError as ValueError: -22 | | ... - | |_______^ A001 -23 | -24 | for memoryview, *bytearray in []: +19 | try: +20 | ... +21 | except ImportError as ValueError: + | ^^^^^^^^^^ A001 +22 | ... | A001.py:24:5: A001 Variable `memoryview` is shadowing a Python builtin diff --git a/crates/ruff/src/rules/flake8_builtins/snapshots/ruff__rules__flake8_builtins__tests__A001_A001.py_builtins_ignorelist.snap b/crates/ruff/src/rules/flake8_builtins/snapshots/ruff__rules__flake8_builtins__tests__A001_A001.py_builtins_ignorelist.snap index 4c47bb5f0c..591ee6781f 100644 --- a/crates/ruff/src/rules/flake8_builtins/snapshots/ruff__rules__flake8_builtins__tests__A001_A001.py_builtins_ignorelist.snap +++ b/crates/ruff/src/rules/flake8_builtins/snapshots/ruff__rules__flake8_builtins__tests__A001_A001.py_builtins_ignorelist.snap @@ -102,15 +102,13 @@ A001.py:16:7: A001 Variable `slice` is shadowing a Python builtin 17 | pass | -A001.py:21:1: A001 Variable `ValueError` is shadowing a Python builtin +A001.py:21:23: A001 Variable `ValueError` is shadowing a Python builtin | -19 | try: -20 | ... -21 | / except ImportError as ValueError: -22 | | ... - | |_______^ A001 -23 | -24 | for memoryview, *bytearray in []: +19 | try: +20 | ... +21 | except ImportError as ValueError: + | ^^^^^^^^^^ A001 +22 | ... | A001.py:24:5: A001 Variable `memoryview` is shadowing a Python builtin diff --git a/crates/ruff/src/rules/flake8_builtins/snapshots/ruff__rules__flake8_builtins__tests__A003_A003.py.snap b/crates/ruff/src/rules/flake8_builtins/snapshots/ruff__rules__flake8_builtins__tests__A003_A003.py.snap index 8c8a70b623..48c9dfa217 100644 --- a/crates/ruff/src/rules/flake8_builtins/snapshots/ruff__rules__flake8_builtins__tests__A003_A003.py.snap +++ b/crates/ruff/src/rules/flake8_builtins/snapshots/ruff__rules__flake8_builtins__tests__A003_A003.py.snap @@ -6,7 +6,7 @@ A003.py:2:5: A003 Class attribute `ImportError` is shadowing a Python builtin 1 | class MyClass: 2 | ImportError = 4 | ^^^^^^^^^^^ A003 -3 | id = 5 +3 | id: int 4 | dir = "/" | @@ -14,7 +14,7 @@ A003.py:3:5: A003 Class attribute `id` is shadowing a Python builtin | 1 | class MyClass: 2 | ImportError = 4 -3 | id = 5 +3 | id: int | ^^ A003 4 | dir = "/" | @@ -22,7 +22,7 @@ A003.py:3:5: A003 Class attribute `id` is shadowing a Python builtin A003.py:4:5: A003 Class attribute `dir` is shadowing a Python builtin | 2 | ImportError = 4 -3 | id = 5 +3 | id: int 4 | dir = "/" | ^^^ A003 5 | diff --git a/crates/ruff/src/rules/flake8_builtins/snapshots/ruff__rules__flake8_builtins__tests__A003_A003.py_builtins_ignorelist.snap b/crates/ruff/src/rules/flake8_builtins/snapshots/ruff__rules__flake8_builtins__tests__A003_A003.py_builtins_ignorelist.snap index 3c509b2fb8..342f14a9a1 100644 --- a/crates/ruff/src/rules/flake8_builtins/snapshots/ruff__rules__flake8_builtins__tests__A003_A003.py_builtins_ignorelist.snap +++ b/crates/ruff/src/rules/flake8_builtins/snapshots/ruff__rules__flake8_builtins__tests__A003_A003.py_builtins_ignorelist.snap @@ -6,7 +6,7 @@ A003.py:2:5: A003 Class attribute `ImportError` is shadowing a Python builtin 1 | class MyClass: 2 | ImportError = 4 | ^^^^^^^^^^^ A003 -3 | id = 5 +3 | id: int 4 | dir = "/" | diff --git a/crates/ruff/src/rules/flake8_commas/rules/mod.rs b/crates/ruff/src/rules/flake8_commas/rules/mod.rs index 0286278d8c..5dbfe3cb51 100644 --- a/crates/ruff/src/rules/flake8_commas/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_commas/rules/mod.rs @@ -1,5 +1,3 @@ -pub(crate) use trailing_commas::{ - trailing_commas, MissingTrailingComma, ProhibitedTrailingComma, TrailingCommaOnBareTuple, -}; +pub(crate) use trailing_commas::*; mod trailing_commas; diff --git a/crates/ruff/src/rules/flake8_commas/rules/trailing_commas.rs b/crates/ruff/src/rules/flake8_commas/rules/trailing_commas.rs index eaee333c3c..1335ff3bb4 100644 --- a/crates/ruff/src/rules/flake8_commas/rules/trailing_commas.rs +++ b/crates/ruff/src/rules/flake8_commas/rules/trailing_commas.rs @@ -222,12 +222,11 @@ impl AlwaysAutofixableViolation for ProhibitedTrailingComma { /// COM812, COM818, COM819 pub(crate) fn trailing_commas( + diagnostics: &mut Vec, tokens: &[LexResult], locator: &Locator, settings: &Settings, -) -> Vec { - let mut diagnostics = vec![]; - +) { let tokens = tokens .iter() .flatten() @@ -326,8 +325,7 @@ pub(crate) fn trailing_commas( let comma = prev.spanned.unwrap(); let mut diagnostic = Diagnostic::new(ProhibitedTrailingComma, comma.1); if settings.rules.should_fix(Rule::ProhibitedTrailingComma) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_deletion(diagnostic.range()))); + diagnostic.set_fix(Fix::automatic(Edit::range_deletion(diagnostic.range()))); } diagnostics.push(diagnostic); } @@ -367,8 +365,7 @@ pub(crate) fn trailing_commas( // removing any brackets in the same linter pass - doing both at the same time could // lead to a syntax error. let contents = locator.slice(missing_comma.1); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( format!("{contents},"), missing_comma.1, ))); @@ -389,6 +386,4 @@ pub(crate) fn trailing_commas( stack.pop(); } } - - diagnostics } diff --git a/crates/ruff/src/rules/flake8_commas/snapshots/ruff__rules__flake8_commas__tests__COM81.py.snap b/crates/ruff/src/rules/flake8_commas/snapshots/ruff__rules__flake8_commas__tests__COM81.py.snap index eb778af586..e4314c5b64 100644 --- a/crates/ruff/src/rules/flake8_commas/snapshots/ruff__rules__flake8_commas__tests__COM81.py.snap +++ b/crates/ruff/src/rules/flake8_commas/snapshots/ruff__rules__flake8_commas__tests__COM81.py.snap @@ -12,7 +12,7 @@ COM81.py:4:18: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 1 1 | # ==> bad_function_call.py <== 2 2 | bad_function_call( 3 3 | param1='test', @@ -32,7 +32,7 @@ COM81.py:10:6: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 7 7 | bad_list = [ 8 8 | 1, 9 9 | 2, @@ -53,7 +53,7 @@ COM81.py:16:6: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 13 13 | bad_list_with_comment = [ 14 14 | 1, 15 15 | 2, @@ -72,7 +72,7 @@ COM81.py:23:6: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 20 20 | bad_list_with_extra_empty = [ 21 21 | 1, 22 22 | 2, @@ -159,7 +159,7 @@ COM81.py:70:8: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 67 67 | pass 68 68 | 69 69 | {'foo': foo}['foo']( @@ -178,7 +178,7 @@ COM81.py:78:8: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 75 75 | ) 76 76 | 77 77 | (foo)( @@ -197,7 +197,7 @@ COM81.py:86:8: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 83 83 | ) 84 84 | 85 85 | [foo][0]( @@ -217,7 +217,7 @@ COM81.py:152:6: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 149 149 | 150 150 | # ==> keyword_before_parenth_form/base_bad.py <== 151 151 | from x import ( @@ -237,7 +237,7 @@ COM81.py:158:11: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 155 155 | assert( 156 156 | SyntaxWarning, 157 157 | ThrownHere, @@ -258,7 +258,7 @@ COM81.py:293:15: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 290 290 | 291 291 | # ==> multiline_bad_dict.py <== 292 292 | multiline_bad_dict = { @@ -279,7 +279,7 @@ COM81.py:304:14: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 301 301 | 302 302 | def func_bad( 303 303 | a = 3, @@ -300,7 +300,7 @@ COM81.py:310:14: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 307 307 | 308 308 | # ==> multiline_bad_function_one_param.py <== 309 309 | def func( @@ -319,7 +319,7 @@ COM81.py:316:10: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 313 313 | 314 314 | 315 315 | func( @@ -339,7 +339,7 @@ COM81.py:322:15: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 319 319 | # ==> multiline_bad_or_dict.py <== 320 320 | multiline_bad_or_dict = { 321 321 | "good": True or False, @@ -359,7 +359,7 @@ COM81.py:368:15: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 365 365 | 366 366 | multiline_index_access[ 367 367 | "probably fine", @@ -379,7 +379,7 @@ COM81.py:375:15: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 372 372 | "fine", 373 373 | "fine", 374 374 | : @@ -399,7 +399,7 @@ COM81.py:404:15: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 401 401 | "fine", 402 402 | "fine" 403 403 | : @@ -419,7 +419,7 @@ COM81.py:432:15: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 429 429 | "fine" 430 430 | : 431 431 | "fine", @@ -439,7 +439,7 @@ COM81.py:485:21: COM819 [*] Trailing comma prohibited | = help: Remove trailing comma -ℹ Suggested fix +ℹ Fix 482 482 | ) 483 483 | 484 484 | # ==> prohibited.py <== @@ -460,7 +460,7 @@ COM81.py:487:13: COM819 [*] Trailing comma prohibited | = help: Remove trailing comma -ℹ Suggested fix +ℹ Fix 484 484 | # ==> prohibited.py <== 485 485 | foo = ['a', 'b', 'c',] 486 486 | @@ -480,7 +480,7 @@ COM81.py:489:18: COM819 [*] Trailing comma prohibited | = help: Remove trailing comma -ℹ Suggested fix +ℹ Fix 486 486 | 487 487 | bar = { a: b,} 488 488 | @@ -501,7 +501,7 @@ COM81.py:494:6: COM819 [*] Trailing comma prohibited | = help: Remove trailing comma -ℹ Suggested fix +ℹ Fix 491 491 | 492 492 | (0,) 493 493 | @@ -522,7 +522,7 @@ COM81.py:496:21: COM819 [*] Trailing comma prohibited | = help: Remove trailing comma -ℹ Suggested fix +ℹ Fix 493 493 | 494 494 | (0, 1,) 495 495 | @@ -543,7 +543,7 @@ COM81.py:498:13: COM819 [*] Trailing comma prohibited | = help: Remove trailing comma -ℹ Suggested fix +ℹ Fix 495 495 | 496 496 | foo = ['a', 'b', 'c', ] 497 497 | @@ -563,7 +563,7 @@ COM81.py:500:18: COM819 [*] Trailing comma prohibited | = help: Remove trailing comma -ℹ Suggested fix +ℹ Fix 497 497 | 498 498 | bar = { a: b, } 499 499 | @@ -584,7 +584,7 @@ COM81.py:505:6: COM819 [*] Trailing comma prohibited | = help: Remove trailing comma -ℹ Suggested fix +ℹ Fix 502 502 | 503 503 | (0, ) 504 504 | @@ -605,7 +605,7 @@ COM81.py:511:10: COM819 [*] Trailing comma prohibited | = help: Remove trailing comma -ℹ Suggested fix +ℹ Fix 508 508 | 509 509 | image[:,] 510 510 | @@ -626,7 +626,7 @@ COM81.py:513:9: COM819 [*] Trailing comma prohibited | = help: Remove trailing comma -ℹ Suggested fix +ℹ Fix 510 510 | 511 511 | image[:,:,] 512 512 | @@ -647,7 +647,7 @@ COM81.py:519:13: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 516 516 | def function( 517 517 | foo, 518 518 | bar, @@ -668,7 +668,7 @@ COM81.py:526:10: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 523 523 | def function( 524 524 | foo, 525 525 | bar, @@ -689,7 +689,7 @@ COM81.py:534:16: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 531 531 | foo, 532 532 | bar, 533 533 | *args, @@ -709,7 +709,7 @@ COM81.py:541:13: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 538 538 | result = function( 539 539 | foo, 540 540 | bar, @@ -729,7 +729,7 @@ COM81.py:547:24: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 544 544 | result = function( 545 545 | foo, 546 546 | bar, @@ -750,7 +750,7 @@ COM81.py:554:15: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 551 551 | ham, 552 552 | spam, 553 553 | *args, @@ -769,7 +769,7 @@ COM81.py:561:13: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 558 558 | # In python 3.5 if it's not a function def, commas are mandatory. 559 559 | 560 560 | foo( @@ -788,7 +788,7 @@ COM81.py:565:13: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 562 562 | ) 563 563 | 564 564 | { @@ -807,7 +807,7 @@ COM81.py:573:10: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 570 570 | ) 571 571 | 572 572 | { @@ -826,7 +826,7 @@ COM81.py:577:10: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 574 574 | } 575 575 | 576 576 | [ @@ -847,7 +847,7 @@ COM81.py:583:10: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 580 580 | def foo( 581 581 | ham, 582 582 | spam, @@ -868,7 +868,7 @@ COM81.py:590:13: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 587 587 | def foo( 588 588 | ham, 589 589 | spam, @@ -889,7 +889,7 @@ COM81.py:598:15: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 595 595 | ham, 596 596 | spam, 597 597 | *args, @@ -909,7 +909,7 @@ COM81.py:627:20: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 624 624 | result = function( 625 625 | foo, 626 626 | bar, @@ -929,7 +929,7 @@ COM81.py:632:42: COM812 [*] Trailing comma missing | = help: Add trailing comma -ℹ Suggested fix +ℹ Fix 629 629 | 630 630 | # Make sure the COM812 and UP034 rules don't autofix simultaneously and cause a syntax error. 631 631 | the_first_one = next( diff --git a/crates/ruff/src/rules/flake8_comprehensions/fixes.rs b/crates/ruff/src/rules/flake8_comprehensions/fixes.rs index ed2ba3ada3..80260fb5f3 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/fixes.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/fixes.rs @@ -14,6 +14,7 @@ use ruff_diagnostics::{Edit, Fix}; use ruff_python_ast::source_code::{Locator, Stylist}; use crate::autofix::codemods::CodegenStylist; +use crate::rules::flake8_comprehensions::rules::ObjectType; use crate::{ checkers::ast::Checker, cst::matchers::{ @@ -109,7 +110,8 @@ pub(crate) fn fix_unnecessary_generator_dict( // Extract the (k, v) from `(k, v) for ...`. let generator_exp = match_generator_exp(&arg.value)?; let tuple = match_tuple(&generator_exp.elt)?; - let [Element::Simple { value: key, .. }, Element::Simple { value, .. }] = &tuple.elements[..] else { + let [Element::Simple { value: key, .. }, Element::Simple { value, .. }] = &tuple.elements[..] + else { bail!("Expected tuple to contain two elements"); }; @@ -188,9 +190,10 @@ pub(crate) fn fix_unnecessary_list_comprehension_dict( let tuple = match_tuple(&list_comp.elt)?; - let [Element::Simple { - value: key, .. - }, Element::Simple { value, .. }] = &tuple.elements[..] else { bail!("Expected tuple with two elements"); }; + let [Element::Simple { value: key, .. }, Element::Simple { value, .. }] = &tuple.elements[..] + else { + bail!("Expected tuple with two elements"); + }; tree = Expression::DictComp(Box::new(DictComp { key: Box::new(key.clone()), @@ -502,14 +505,14 @@ pub(crate) fn fix_unnecessary_collection_call( /// this method will pad the start and end of an expression as needed to /// avoid producing invalid syntax. fn pad_expression(content: String, range: TextRange, checker: &Checker) -> String { - if !checker.semantic_model().in_f_string() { + if !checker.semantic().in_f_string() { return content; } // If the expression is immediately preceded by an opening brace, then // we need to add a space before the expression. let prefix = checker.locator.up_to(range.start()); - let left_pad = matches!(prefix.chars().rev().next(), Some('{')); + let left_pad = matches!(prefix.chars().next_back(), Some('{')); // If the expression is immediately preceded by an opening brace, then // we need to add a space before the expression. @@ -886,7 +889,7 @@ pub(crate) fn fix_unnecessary_map( stylist: &Stylist, expr: &rustpython_parser::ast::Expr, parent: Option<&rustpython_parser::ast::Expr>, - kind: &str, + object_type: ObjectType, ) -> Result { let module_text = locator.slice(expr.range()); let mut tree = match_expression(module_text)?; @@ -946,8 +949,8 @@ pub(crate) fn fix_unnecessary_map( whitespace_after_in: ParenthesizableWhitespace::SimpleWhitespace(SimpleWhitespace(" ")), }); - match kind { - "generator" => { + match object_type { + ObjectType::Generator => { tree = Expression::GeneratorExp(Box::new(GeneratorExp { elt: func_body.body.clone(), for_in: compfor, @@ -955,7 +958,7 @@ pub(crate) fn fix_unnecessary_map( rpar: vec![RightParen::default()], })); } - "list" => { + ObjectType::List => { tree = Expression::ListComp(Box::new(ListComp { elt: func_body.body.clone(), for_in: compfor, @@ -965,7 +968,7 @@ pub(crate) fn fix_unnecessary_map( rpar: vec![], })); } - "set" => { + ObjectType::Set => { tree = Expression::SetComp(Box::new(SetComp { elt: func_body.body.clone(), for_in: compfor, @@ -975,21 +978,17 @@ pub(crate) fn fix_unnecessary_map( rbrace: RightCurlyBrace::default(), })); } - "dict" => { + ObjectType::Dict => { let (key, value) = if let Expression::Tuple(tuple) = func_body.body.as_ref() { if tuple.elements.len() != 2 { bail!("Expected two elements") } let Some(Element::Simple { value: key, .. }) = &tuple.elements.get(0) else { - bail!( - "Expected tuple to contain a key as the first element" - ); + bail!("Expected tuple to contain a key as the first element"); }; let Some(Element::Simple { value, .. }) = &tuple.elements.get(1) else { - bail!( - "Expected tuple to contain a key as the second element" - ); + bail!("Expected tuple to contain a key as the second element"); }; (key, value) @@ -1011,17 +1010,14 @@ pub(crate) fn fix_unnecessary_map( ), })); } - _ => { - bail!("Expected generator, list, set or dict"); - } } let mut content = tree.codegen_stylist(stylist); // If the expression is embedded in an f-string, surround it with spaces to avoid // syntax errors. - if kind == "set" || kind == "dict" { - if let Some(rustpython_parser::ast::Expr::FormattedValue(_)) = parent { + if matches!(object_type, ObjectType::Set | ObjectType::Dict) { + if parent.map_or(false, rustpython_parser::ast::Expr::is_formatted_value_expr) { content = format!(" {content} "); } } @@ -1063,9 +1059,7 @@ pub(crate) fn fix_unnecessary_comprehension_any_all( let call = match_call_mut(&mut tree)?; let Expression::ListComp(list_comp) = &call.args[0].value else { - bail!( - "Expected Expression::ListComp" - ); + bail!("Expected Expression::ListComp"); }; let mut new_empty_lines = vec![]; diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/mod.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/mod.rs index 0cb1aaacfe..2c24ecfc2f 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/mod.rs @@ -1,43 +1,21 @@ -pub(crate) use unnecessary_call_around_sorted::{ - unnecessary_call_around_sorted, UnnecessaryCallAroundSorted, -}; -pub(crate) use unnecessary_collection_call::{ - unnecessary_collection_call, UnnecessaryCollectionCall, -}; -pub(crate) use unnecessary_comprehension::{ - unnecessary_dict_comprehension, unnecessary_list_set_comprehension, UnnecessaryComprehension, -}; -pub(crate) use unnecessary_comprehension_any_all::{ - unnecessary_comprehension_any_all, UnnecessaryComprehensionAnyAll, -}; -pub(crate) use unnecessary_double_cast_or_process::{ - unnecessary_double_cast_or_process, UnnecessaryDoubleCastOrProcess, -}; -pub(crate) use unnecessary_generator_dict::{unnecessary_generator_dict, UnnecessaryGeneratorDict}; -pub(crate) use unnecessary_generator_list::{unnecessary_generator_list, UnnecessaryGeneratorList}; -pub(crate) use unnecessary_generator_set::{unnecessary_generator_set, UnnecessaryGeneratorSet}; -pub(crate) use unnecessary_list_call::{unnecessary_list_call, UnnecessaryListCall}; -pub(crate) use unnecessary_list_comprehension_dict::{ - unnecessary_list_comprehension_dict, UnnecessaryListComprehensionDict, -}; -pub(crate) use unnecessary_list_comprehension_set::{ - unnecessary_list_comprehension_set, UnnecessaryListComprehensionSet, -}; -pub(crate) use unnecessary_literal_dict::{unnecessary_literal_dict, UnnecessaryLiteralDict}; -pub(crate) use unnecessary_literal_set::{unnecessary_literal_set, UnnecessaryLiteralSet}; -pub(crate) use unnecessary_literal_within_dict_call::{ - unnecessary_literal_within_dict_call, UnnecessaryLiteralWithinDictCall, -}; -pub(crate) use unnecessary_literal_within_list_call::{ - unnecessary_literal_within_list_call, UnnecessaryLiteralWithinListCall, -}; -pub(crate) use unnecessary_literal_within_tuple_call::{ - unnecessary_literal_within_tuple_call, UnnecessaryLiteralWithinTupleCall, -}; -pub(crate) use unnecessary_map::{unnecessary_map, UnnecessaryMap}; -pub(crate) use unnecessary_subscript_reversal::{ - unnecessary_subscript_reversal, UnnecessarySubscriptReversal, -}; +pub(crate) use unnecessary_call_around_sorted::*; +pub(crate) use unnecessary_collection_call::*; +pub(crate) use unnecessary_comprehension::*; +pub(crate) use unnecessary_comprehension_any_all::*; +pub(crate) use unnecessary_double_cast_or_process::*; +pub(crate) use unnecessary_generator_dict::*; +pub(crate) use unnecessary_generator_list::*; +pub(crate) use unnecessary_generator_set::*; +pub(crate) use unnecessary_list_call::*; +pub(crate) use unnecessary_list_comprehension_dict::*; +pub(crate) use unnecessary_list_comprehension_set::*; +pub(crate) use unnecessary_literal_dict::*; +pub(crate) use unnecessary_literal_set::*; +pub(crate) use unnecessary_literal_within_dict_call::*; +pub(crate) use unnecessary_literal_within_list_call::*; +pub(crate) use unnecessary_literal_within_tuple_call::*; +pub(crate) use unnecessary_map::*; +pub(crate) use unnecessary_subscript_reversal::*; mod helpers; mod unnecessary_call_around_sorted; diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_call_around_sorted.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_call_around_sorted.rs index 80be40cf4f..e8770e8ac3 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_call_around_sorted.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_call_around_sorted.rs @@ -75,7 +75,7 @@ pub(crate) fn unnecessary_call_around_sorted( if inner != "sorted" { return; } - if !checker.semantic_model().is_builtin(inner) || !checker.semantic_model().is_builtin(outer) { + if !checker.semantic().is_builtin(inner) || !checker.semantic().is_builtin(outer) { return; } let mut diagnostic = Diagnostic::new( diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_collection_call.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_collection_call.rs index 23ca342702..6a51574d45 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_collection_call.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_collection_call.rs @@ -79,7 +79,7 @@ pub(crate) fn unnecessary_collection_call( } _ => return, }; - if !checker.semantic_model().is_builtin(id) { + if !checker.semantic().is_builtin(id) { return; } let mut diagnostic = Diagnostic::new( diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_comprehension.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_comprehension.rs index 498e86edc4..56de6768ce 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_comprehension.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_comprehension.rs @@ -56,7 +56,7 @@ fn add_diagnostic(checker: &mut Checker, expr: &Expr) { Expr::DictComp(_) => "dict", _ => return, }; - if !checker.semantic_model().is_builtin(id) { + if !checker.semantic().is_builtin(id) { return; } let mut diagnostic = Diagnostic::new( diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_comprehension_any_all.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_comprehension_any_all.rs index c3aac344dc..71b6432f2a 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_comprehension_any_all.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_comprehension_any_all.rs @@ -66,17 +66,19 @@ pub(crate) fn unnecessary_comprehension_any_all( if !keywords.is_empty() { return; } - let Expr::Name(ast::ExprName { id, .. } )= func else { + let Expr::Name(ast::ExprName { id, .. }) = func else { return; }; if (matches!(id.as_str(), "all" | "any")) && args.len() == 1 { - let (Expr::ListComp(ast::ExprListComp { elt, .. } )| Expr::SetComp(ast::ExprSetComp { elt, .. })) = &args[0] else { + let (Expr::ListComp(ast::ExprListComp { elt, .. }) + | Expr::SetComp(ast::ExprSetComp { elt, .. })) = &args[0] + else { return; }; - if is_async_generator(elt) { + if contains_await(elt) { return; } - if !checker.semantic_model().is_builtin(id) { + if !checker.semantic().is_builtin(id) { return; } let mut diagnostic = Diagnostic::new(UnnecessaryComprehensionAnyAll, args[0].range()); @@ -89,7 +91,7 @@ pub(crate) fn unnecessary_comprehension_any_all( } } -/// Return `true` if the `Expr` contains an `await` expression. -fn is_async_generator(expr: &Expr) -> bool { - any_over_expr(expr, &|expr| matches!(expr, Expr::Await(_))) +/// Return `true` if the [`Expr`] contains an `await` expression. +fn contains_await(expr: &Expr) -> bool { + any_over_expr(expr, &Expr::is_await_expr) } diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_double_cast_or_process.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_double_cast_or_process.rs index 96cec22cf0..2c71d2d11a 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_double_cast_or_process.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_double_cast_or_process.rs @@ -84,13 +84,13 @@ pub(crate) fn unnecessary_double_cast_or_process( let Some(arg) = args.first() else { return; }; - let Expr::Call(ast::ExprCall { func, ..} )= arg else { + let Expr::Call(ast::ExprCall { func, .. }) = arg else { return; }; let Some(inner) = helpers::expr_name(func) else { return; }; - if !checker.semantic_model().is_builtin(inner) || !checker.semantic_model().is_builtin(outer) { + if !checker.semantic().is_builtin(inner) || !checker.semantic().is_builtin(outer) { return; } diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_dict.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_dict.rs index 030eff86d3..fe8c8afb0e 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_dict.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_dict.rs @@ -49,7 +49,9 @@ pub(crate) fn unnecessary_generator_dict( args: &[Expr], keywords: &[Keyword], ) { - let Some(argument) = helpers::exactly_one_argument_with_matching_function("dict", func, args, keywords) else { + let Some(argument) = + helpers::exactly_one_argument_with_matching_function("dict", func, args, keywords) + else { return; }; if let Expr::GeneratorExp(ast::ExprGeneratorExp { elt, .. }) = argument { diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_list.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_list.rs index 4cdd85512d..470595f806 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_list.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_list.rs @@ -49,10 +49,12 @@ pub(crate) fn unnecessary_generator_list( args: &[Expr], keywords: &[Keyword], ) { - let Some(argument) = helpers::exactly_one_argument_with_matching_function("list", func, args, keywords) else { + let Some(argument) = + helpers::exactly_one_argument_with_matching_function("list", func, args, keywords) + else { return; }; - if !checker.semantic_model().is_builtin("list") { + if !checker.semantic().is_builtin("list") { return; } if let Expr::GeneratorExp(_) = argument { diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_set.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_set.rs index 345a6c0f91..44fa61ced3 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_set.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_set.rs @@ -49,10 +49,12 @@ pub(crate) fn unnecessary_generator_set( args: &[Expr], keywords: &[Keyword], ) { - let Some(argument) = helpers::exactly_one_argument_with_matching_function("set", func, args, keywords) else { + let Some(argument) = + helpers::exactly_one_argument_with_matching_function("set", func, args, keywords) + else { return; }; - if !checker.semantic_model().is_builtin("set") { + if !checker.semantic().is_builtin("set") { return; } if let Expr::GeneratorExp(_) = argument { diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_list_call.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_list_call.rs index 597253875b..45ef0f4ae0 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_list_call.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_list_call.rs @@ -48,7 +48,7 @@ pub(crate) fn unnecessary_list_call( let Some(argument) = helpers::first_argument_with_matching_function("list", func, args) else { return; }; - if !checker.semantic_model().is_builtin("list") { + if !checker.semantic().is_builtin("list") { return; } if !argument.is_list_comp_expr() { diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_dict.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_dict.rs index e30434bbe3..d425003f04 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_dict.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_dict.rs @@ -47,10 +47,12 @@ pub(crate) fn unnecessary_list_comprehension_dict( args: &[Expr], keywords: &[Keyword], ) { - let Some(argument) = helpers::exactly_one_argument_with_matching_function("dict", func, args, keywords) else { + let Some(argument) = + helpers::exactly_one_argument_with_matching_function("dict", func, args, keywords) + else { return; }; - if !checker.semantic_model().is_builtin("dict") { + if !checker.semantic().is_builtin("dict") { return; } let Expr::ListComp(ast::ExprListComp { elt, .. }) = argument else { diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_set.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_set.rs index 8506e71249..9bf415f8f8 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_set.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_set.rs @@ -47,10 +47,12 @@ pub(crate) fn unnecessary_list_comprehension_set( args: &[Expr], keywords: &[Keyword], ) { - let Some(argument) = helpers::exactly_one_argument_with_matching_function("set", func, args, keywords) else { + let Some(argument) = + helpers::exactly_one_argument_with_matching_function("set", func, args, keywords) + else { return; }; - if !checker.semantic_model().is_builtin("set") { + if !checker.semantic().is_builtin("set") { return; } if argument.is_list_comp_expr() { diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_dict.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_dict.rs index 370bedf38f..8c981d062a 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_dict.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_dict.rs @@ -54,10 +54,12 @@ pub(crate) fn unnecessary_literal_dict( args: &[Expr], keywords: &[Keyword], ) { - let Some(argument) = helpers::exactly_one_argument_with_matching_function("dict", func, args, keywords) else { + let Some(argument) = + helpers::exactly_one_argument_with_matching_function("dict", func, args, keywords) + else { return; }; - if !checker.semantic_model().is_builtin("dict") { + if !checker.semantic().is_builtin("dict") { return; } let (kind, elts) = match argument { diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_set.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_set.rs index 9519109ac9..43f4372404 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_set.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_set.rs @@ -55,10 +55,12 @@ pub(crate) fn unnecessary_literal_set( args: &[Expr], keywords: &[Keyword], ) { - let Some(argument) = helpers::exactly_one_argument_with_matching_function("set", func, args, keywords) else { + let Some(argument) = + helpers::exactly_one_argument_with_matching_function("set", func, args, keywords) + else { return; }; - if !checker.semantic_model().is_builtin("set") { + if !checker.semantic().is_builtin("set") { return; } let kind = match argument { diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_dict_call.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_dict_call.rs index cc9ba2d5ce..119f53330b 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_dict_call.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_dict_call.rs @@ -76,7 +76,7 @@ pub(crate) fn unnecessary_literal_within_dict_call( let Some(argument) = helpers::first_argument_with_matching_function("dict", func, args) else { return; }; - if !checker.semantic_model().is_builtin("dict") { + if !checker.semantic().is_builtin("dict") { return; } let argument_kind = match argument { diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_list_call.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_list_call.rs index d1c240d55f..febd2ce931 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_list_call.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_list_call.rs @@ -79,7 +79,7 @@ pub(crate) fn unnecessary_literal_within_list_call( let Some(argument) = helpers::first_argument_with_matching_function("list", func, args) else { return; }; - if !checker.semantic_model().is_builtin("list") { + if !checker.semantic().is_builtin("list") { return; } let argument_kind = match argument { diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_tuple_call.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_tuple_call.rs index cd61c1943f..187e1ae3e8 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_tuple_call.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_within_tuple_call.rs @@ -80,7 +80,7 @@ pub(crate) fn unnecessary_literal_within_tuple_call( let Some(argument) = helpers::first_argument_with_matching_function("tuple", func, args) else { return; }; - if !checker.semantic_model().is_builtin("tuple") { + if !checker.semantic().is_builtin("tuple") { return; } let argument_kind = match argument { diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_map.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_map.rs index 086a3137a4..0505d08028 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_map.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_map.rs @@ -1,9 +1,13 @@ -use ruff_text_size::TextRange; -use rustpython_parser::ast::{self, Expr, Ranged}; +use std::fmt; + +use rustpython_parser::ast::{self, Arguments, Expr, ExprContext, Ranged, Stmt}; -use ruff_diagnostics::Diagnostic; use ruff_diagnostics::{AutofixKind, Violation}; +use ruff_diagnostics::{Diagnostic, Fix}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers::includes_arg_name; +use ruff_python_ast::visitor; +use ruff_python_ast::visitor::Visitor; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -40,7 +44,7 @@ use super::helpers; /// `{v: v ** 2 for v in values}`. #[violation] pub struct UnnecessaryMap { - obj_type: String, + object_type: ObjectType, } impl Violation for UnnecessaryMap { @@ -48,21 +52,13 @@ impl Violation for UnnecessaryMap { #[derive_message_formats] fn message(&self) -> String { - let UnnecessaryMap { obj_type } = self; - if obj_type == "generator" { - format!("Unnecessary `map` usage (rewrite using a generator expression)") - } else { - format!("Unnecessary `map` usage (rewrite using a `{obj_type}` comprehension)") - } + let UnnecessaryMap { object_type } = self; + format!("Unnecessary `map` usage (rewrite using a {object_type})") } fn autofix_title(&self) -> Option { - let UnnecessaryMap { obj_type } = self; - Some(if obj_type == "generator" { - format!("Replace `map` using a generator expression") - } else { - format!("Replace `map` using a `{obj_type}` comprehension") - }) + let UnnecessaryMap { object_type } = self; + Some(format!("Replace `map` with a {object_type}")) } } @@ -74,112 +70,198 @@ pub(crate) fn unnecessary_map( func: &Expr, args: &[Expr], ) { - fn create_diagnostic(kind: &str, location: TextRange) -> Diagnostic { - Diagnostic::new( - UnnecessaryMap { - obj_type: kind.to_string(), - }, - location, - ) - } - - let Some(id) = helpers::expr_name(func) else { + let Some(id) = helpers::expr_name(func) else { return; }; - match id { - "map" => { - if !checker.semantic_model().is_builtin(id) { - return; - } - // Exclude the parent if already matched by other arms - if let Some(Expr::Call(ast::ExprCall { func: f, .. })) = parent { - if let Some(id_parent) = helpers::expr_name(f) { - if id_parent == "dict" || id_parent == "set" || id_parent == "list" { + let object_type = match id { + "map" => ObjectType::Generator, + "list" => ObjectType::List, + "set" => ObjectType::Set, + "dict" => ObjectType::Dict, + _ => return, + }; + + if !checker.semantic().is_builtin(id) { + return; + } + + match object_type { + ObjectType::Generator => { + // Exclude the parent if already matched by other arms. + if let Some(Expr::Call(ast::ExprCall { func, .. })) = parent { + if let Some(name) = helpers::expr_name(func) { + if matches!(name, "list" | "set" | "dict") { return; } } }; - if args.len() == 2 && matches!(&args[0], Expr::Lambda(_)) { - let mut diagnostic = create_diagnostic("generator", expr.range()); - if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.try_set_fix_from_edit(|| { - fixes::fix_unnecessary_map( - checker.locator, - checker.stylist, - expr, - parent, - "generator", - ) - }); - } - checker.diagnostics.push(diagnostic); + // Only flag, e.g., `map(lambda x: x + 1, iterable)`. + let [Expr::Lambda(ast::ExprLambda { args, body, .. }), _] = args else { + return; + }; + + if late_binding(args, body) { + return; } } - "list" | "set" => { - if !checker.semantic_model().is_builtin(id) { + ObjectType::List | ObjectType::Set => { + // Only flag, e.g., `list(map(lambda x: x + 1, iterable))`. + let [Expr::Call(ast::ExprCall { func, args, .. })] = args else { + return; + }; + + if args.len() != 2 { return; } - if let Some(Expr::Call(ast::ExprCall { func, args, .. })) = args.first() { - if args.len() != 2 { - return; - } - let Some(argument) = helpers::first_argument_with_matching_function("map", func, args) else { - return; - }; - if let Expr::Lambda(_) = argument { - let mut diagnostic = create_diagnostic(id, expr.range()); - if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.try_set_fix_from_edit(|| { - fixes::fix_unnecessary_map( - checker.locator, - checker.stylist, - expr, - parent, - id, - ) - }); - } - checker.diagnostics.push(diagnostic); - } + let Some(argument) = helpers::first_argument_with_matching_function("map", func, args) + else { + return; + }; + + let Expr::Lambda(ast::ExprLambda { args, body, .. }) = argument else { + return; + }; + + if late_binding(args, body) { + return; } } - "dict" => { - if !checker.semantic_model().is_builtin(id) { + ObjectType::Dict => { + // Only flag, e.g., `dict(map(lambda v: (v, v ** 2), values))`. + let [Expr::Call(ast::ExprCall { func, args, .. })] = args else { + return; + }; + + let Some(argument) = helpers::first_argument_with_matching_function("map", func, args) + else { + return; + }; + + let Expr::Lambda(ast::ExprLambda { args, body, .. }) = argument else { + return; + }; + + let (Expr::Tuple(ast::ExprTuple { elts, .. }) | Expr::List(ast::ExprList { elts, .. })) = + body.as_ref() + else { + return; + }; + + if elts.len() != 2 { return; } - if args.len() == 1 { - if let Expr::Call(ast::ExprCall { func, args, .. }) = &args[0] { - let Some(argument) = helpers::first_argument_with_matching_function("map", func, args) else { - return; - }; - if let Expr::Lambda(ast::ExprLambda { body, .. }) = argument { - if matches!(body.as_ref(), Expr::Tuple(ast::ExprTuple { elts, .. }) | Expr::List(ast::ExprList { elts, .. } ) if elts.len() == 2) - { - let mut diagnostic = create_diagnostic(id, expr.range()); - if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.try_set_fix_from_edit(|| { - fixes::fix_unnecessary_map( - checker.locator, - checker.stylist, - expr, - parent, - id, - ) - }); - } - checker.diagnostics.push(diagnostic); + if late_binding(args, body) { + return; + } + } + } + + let mut diagnostic = Diagnostic::new(UnnecessaryMap { object_type }, expr.range()); + if checker.patch(diagnostic.kind.rule()) { + diagnostic.try_set_fix(|| { + fixes::fix_unnecessary_map(checker.locator, checker.stylist, expr, parent, object_type) + .map(Fix::suggested) + }); + } + checker.diagnostics.push(diagnostic); +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub(crate) enum ObjectType { + Generator, + List, + Set, + Dict, +} + +impl fmt::Display for ObjectType { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match self { + ObjectType::Generator => fmt.write_str("generator expression"), + ObjectType::List => fmt.write_str("`list` comprehension"), + ObjectType::Set => fmt.write_str("`set` comprehension"), + ObjectType::Dict => fmt.write_str("`dict` comprehension"), + } + } +} + +/// Returns `true` if the lambda defined by the given arguments and body contains any names that +/// are late-bound within nested lambdas. +/// +/// For example, given: +/// +/// ```python +/// map(lambda x: lambda: x, range(4)) # (0, 1, 2, 3) +/// ``` +/// +/// The `x` in the inner lambda is "late-bound". Specifically, rewriting the above as: +/// +/// ```python +/// (lambda: x for x in range(4)) # (3, 3, 3, 3) +/// ``` +/// +/// Would yield an incorrect result, as the `x` in the inner lambda would be bound to the last +/// value of `x` in the comprehension. +fn late_binding(args: &Arguments, body: &Expr) -> bool { + let mut visitor = LateBindingVisitor::new(args); + visitor.visit_expr(body); + visitor.late_bound +} + +#[derive(Debug)] +struct LateBindingVisitor<'a> { + /// The arguments to the current lambda. + args: &'a Arguments, + /// The arguments to any lambdas within the current lambda body. + lambdas: Vec<&'a Arguments>, + /// Whether any names within the current lambda body are late-bound within nested lambdas. + late_bound: bool, +} + +impl<'a> LateBindingVisitor<'a> { + fn new(args: &'a Arguments) -> Self { + Self { + args, + lambdas: Vec::new(), + late_bound: false, + } + } +} + +impl<'a> Visitor<'a> for LateBindingVisitor<'a> { + fn visit_stmt(&mut self, _stmt: &'a Stmt) {} + + fn visit_expr(&mut self, expr: &'a Expr) { + match expr { + Expr::Lambda(ast::ExprLambda { args, .. }) => { + self.lambdas.push(args); + visitor::walk_expr(self, expr); + self.lambdas.pop(); + } + Expr::Name(ast::ExprName { + id, + ctx: ExprContext::Load, + .. + }) => { + // If we're within a nested lambda... + if !self.lambdas.is_empty() { + // If the name is defined in the current lambda... + if includes_arg_name(id, self.args) { + // And isn't overridden by any nested lambdas... + if !self.lambdas.iter().any(|args| includes_arg_name(id, args)) { + // Then it's late-bound. + self.late_bound = true; } } } } + _ => visitor::walk_expr(self, expr), } - _ => (), } + + fn visit_body(&mut self, _body: &'a [Stmt]) {} } diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_subscript_reversal.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_subscript_reversal.rs index 8659025978..896b9128a5 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_subscript_reversal.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_subscript_reversal.rs @@ -1,5 +1,5 @@ use num_bigint::BigInt; -use rustpython_parser::ast::{self, Constant, Expr, Ranged, Unaryop}; +use rustpython_parser::ast::{self, Constant, Expr, Ranged, UnaryOp}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -58,15 +58,21 @@ pub(crate) fn unnecessary_subscript_reversal( if !(id == "set" || id == "sorted" || id == "reversed") { return; } - if !checker.semantic_model().is_builtin(id) { + if !checker.semantic().is_builtin(id) { return; } let Expr::Subscript(ast::ExprSubscript { slice, .. }) = first_arg else { return; }; - let Expr::Slice(ast::ExprSlice { lower, upper, step, range: _ }) = slice.as_ref() else { - return; - }; + let Expr::Slice(ast::ExprSlice { + lower, + upper, + step, + range: _, + }) = slice.as_ref() + else { + return; + }; if lower.is_some() || upper.is_some() { return; } @@ -74,16 +80,18 @@ pub(crate) fn unnecessary_subscript_reversal( return; }; let Expr::UnaryOp(ast::ExprUnaryOp { - op: Unaryop::USub, + op: UnaryOp::USub, operand, range: _, - }) = step.as_ref() else { + }) = step.as_ref() + else { return; }; let Expr::Constant(ast::ExprConstant { value: Constant::Int(val), .. - }) = operand.as_ref() else { + }) = operand.as_ref() + else { return; }; if *val != BigInt::from(1) { diff --git a/crates/ruff/src/rules/flake8_comprehensions/snapshots/ruff__rules__flake8_comprehensions__tests__C417_C417.py.snap b/crates/ruff/src/rules/flake8_comprehensions/snapshots/ruff__rules__flake8_comprehensions__tests__C417_C417.py.snap index b27c132244..acb6700d3f 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/snapshots/ruff__rules__flake8_comprehensions__tests__C417_C417.py.snap +++ b/crates/ruff/src/rules/flake8_comprehensions/snapshots/ruff__rules__flake8_comprehensions__tests__C417_C417.py.snap @@ -10,7 +10,7 @@ C417.py:3:1: C417 [*] Unnecessary `map` usage (rewrite using a generator express 4 | map(lambda x: str(x), nums) 5 | list(map(lambda x: x * 2, nums)) | - = help: Replace `map` using a generator expression + = help: Replace `map` with a generator expression ℹ Suggested fix 1 1 | # Errors. @@ -30,7 +30,7 @@ C417.py:4:1: C417 [*] Unnecessary `map` usage (rewrite using a generator express 5 | list(map(lambda x: x * 2, nums)) 6 | set(map(lambda x: x % 2 == 0, nums)) | - = help: Replace `map` using a generator expression + = help: Replace `map` with a generator expression ℹ Suggested fix 1 1 | # Errors. @@ -51,7 +51,7 @@ C417.py:5:1: C417 [*] Unnecessary `map` usage (rewrite using a `list` comprehens 6 | set(map(lambda x: x % 2 == 0, nums)) 7 | dict(map(lambda v: (v, v**2), nums)) | - = help: Replace `map` using a `list` comprehension + = help: Replace `map` with a `list` comprehension ℹ Suggested fix 2 2 | nums = [1, 2, 3] @@ -72,7 +72,7 @@ C417.py:6:1: C417 [*] Unnecessary `map` usage (rewrite using a `set` comprehensi 7 | dict(map(lambda v: (v, v**2), nums)) 8 | map(lambda: "const", nums) | - = help: Replace `map` using a `set` comprehension + = help: Replace `map` with a `set` comprehension ℹ Suggested fix 3 3 | map(lambda x: x + 1, nums) @@ -93,7 +93,7 @@ C417.py:7:1: C417 [*] Unnecessary `map` usage (rewrite using a `dict` comprehens 8 | map(lambda: "const", nums) 9 | map(lambda _: 3.0, nums) | - = help: Replace `map` using a `dict` comprehension + = help: Replace `map` with a `dict` comprehension ℹ Suggested fix 4 4 | map(lambda x: str(x), nums) @@ -114,7 +114,7 @@ C417.py:8:1: C417 [*] Unnecessary `map` usage (rewrite using a generator express 9 | map(lambda _: 3.0, nums) 10 | _ = "".join(map(lambda x: x in nums and "1" or "0", range(123))) | - = help: Replace `map` using a generator expression + = help: Replace `map` with a generator expression ℹ Suggested fix 5 5 | list(map(lambda x: x * 2, nums)) @@ -135,7 +135,7 @@ C417.py:9:1: C417 [*] Unnecessary `map` usage (rewrite using a generator express 10 | _ = "".join(map(lambda x: x in nums and "1" or "0", range(123))) 11 | all(map(lambda v: isinstance(v, dict), nums)) | - = help: Replace `map` using a generator expression + = help: Replace `map` with a generator expression ℹ Suggested fix 6 6 | set(map(lambda x: x % 2 == 0, nums)) @@ -156,7 +156,7 @@ C417.py:10:13: C417 [*] Unnecessary `map` usage (rewrite using a generator expre 11 | all(map(lambda v: isinstance(v, dict), nums)) 12 | filter(func, map(lambda v: v, nums)) | - = help: Replace `map` using a generator expression + = help: Replace `map` with a generator expression ℹ Suggested fix 7 7 | dict(map(lambda v: (v, v**2), nums)) @@ -176,7 +176,7 @@ C417.py:11:5: C417 [*] Unnecessary `map` usage (rewrite using a generator expres | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C417 12 | filter(func, map(lambda v: v, nums)) | - = help: Replace `map` using a generator expression + = help: Replace `map` with a generator expression ℹ Suggested fix 8 8 | map(lambda: "const", nums) @@ -197,7 +197,7 @@ C417.py:12:14: C417 [*] Unnecessary `map` usage (rewrite using a generator expre 13 | 14 | # When inside f-string, then the fix should be surrounded by whitespace | - = help: Replace `map` using a generator expression + = help: Replace `map` with a generator expression ℹ Suggested fix 9 9 | map(lambda _: 3.0, nums) @@ -216,7 +216,7 @@ C417.py:15:8: C417 [*] Unnecessary `map` usage (rewrite using a `set` comprehens | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C417 16 | _ = f"{dict(map(lambda v: (v, v**2), nums))}" | - = help: Replace `map` using a `set` comprehension + = help: Replace `map` with a `set` comprehension ℹ Suggested fix 12 12 | filter(func, map(lambda v: v, nums)) @@ -237,7 +237,7 @@ C417.py:16:8: C417 [*] Unnecessary `map` usage (rewrite using a `dict` comprehen 17 | 18 | # Error, but unfixable. | - = help: Replace `map` using a `dict` comprehension + = help: Replace `map` with a `dict` comprehension ℹ Suggested fix 13 13 | @@ -258,6 +258,21 @@ C417.py:21:1: C417 Unnecessary `map` usage (rewrite using a generator expression 22 | 23 | # False negatives. | - = help: Replace `map` using a generator expression + = help: Replace `map` with a generator expression + +C417.py:39:1: C417 [*] Unnecessary `map` usage (rewrite using a generator expression) + | +38 | # Error: the `x` is overridden by the inner lambda. +39 | map(lambda x: lambda x: x, range(4)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C417 + | + = help: Replace `map` with a generator expression + +ℹ Suggested fix +36 36 | map(lambda x: lambda: x, range(4)) +37 37 | +38 38 | # Error: the `x` is overridden by the inner lambda. +39 |-map(lambda x: lambda x: x, range(4)) + 39 |+(lambda x: x for x in range(4)) diff --git a/crates/ruff/src/rules/flake8_copyright/mod.rs b/crates/ruff/src/rules/flake8_copyright/mod.rs new file mode 100644 index 0000000000..6e2a39c4cb --- /dev/null +++ b/crates/ruff/src/rules/flake8_copyright/mod.rs @@ -0,0 +1,158 @@ +//! Rules from [flake8-copyright](https://pypi.org/project/flake8-copyright/). +pub(crate) mod rules; + +pub mod settings; + +#[cfg(test)] +mod tests { + use crate::registry::Rule; + use crate::test::test_snippet; + use crate::{assert_messages, settings}; + + #[test] + fn notice() { + let diagnostics = test_snippet( + r#" +# Copyright 2023 + +import os +"# + .trim(), + &settings::Settings::for_rules(vec![Rule::MissingCopyrightNotice]), + ); + assert_messages!(diagnostics); + } + + #[test] + fn notice_with_c() { + let diagnostics = test_snippet( + r#" +# Copyright (C) 2023 + +import os +"# + .trim(), + &settings::Settings::for_rules(vec![Rule::MissingCopyrightNotice]), + ); + assert_messages!(diagnostics); + } + + #[test] + fn notice_with_caps() { + let diagnostics = test_snippet( + r#" +# COPYRIGHT (C) 2023 + +import os +"# + .trim(), + &settings::Settings::for_rules(vec![Rule::MissingCopyrightNotice]), + ); + assert_messages!(diagnostics); + } + + #[test] + fn notice_with_range() { + let diagnostics = test_snippet( + r#" +# Copyright (C) 2021-2023 + +import os +"# + .trim(), + &settings::Settings::for_rules(vec![Rule::MissingCopyrightNotice]), + ); + assert_messages!(diagnostics); + } + + #[test] + fn valid_author() { + let diagnostics = test_snippet( + r#" +# Copyright (C) 2023 Ruff + +import os +"# + .trim(), + &settings::Settings { + flake8_copyright: super::settings::Settings { + author: Some("Ruff".to_string()), + ..super::settings::Settings::default() + }, + ..settings::Settings::for_rules(vec![Rule::MissingCopyrightNotice]) + }, + ); + assert_messages!(diagnostics); + } + + #[test] + fn invalid_author() { + let diagnostics = test_snippet( + r#" +# Copyright (C) 2023 Some Author + +import os +"# + .trim(), + &settings::Settings { + flake8_copyright: super::settings::Settings { + author: Some("Ruff".to_string()), + ..super::settings::Settings::default() + }, + ..settings::Settings::for_rules(vec![Rule::MissingCopyrightNotice]) + }, + ); + assert_messages!(diagnostics); + } + + #[test] + fn small_file() { + let diagnostics = test_snippet( + r#" +import os +"# + .trim(), + &settings::Settings { + flake8_copyright: super::settings::Settings { + min_file_size: 256, + ..super::settings::Settings::default() + }, + ..settings::Settings::for_rules(vec![Rule::MissingCopyrightNotice]) + }, + ); + assert_messages!(diagnostics); + } + + #[test] + fn late_notice() { + let diagnostics = test_snippet( + r#" +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content +# Content Content Content Content Content Content Content Content Content Content + +# Copyright 2023 +"# + .trim(), + &settings::Settings::for_rules(vec![Rule::MissingCopyrightNotice]), + ); + assert_messages!(diagnostics); + } +} diff --git a/crates/ruff/src/rules/flake8_copyright/rules/missing_copyright_notice.rs b/crates/ruff/src/rules/flake8_copyright/rules/missing_copyright_notice.rs new file mode 100644 index 0000000000..ce3cca98c7 --- /dev/null +++ b/crates/ruff/src/rules/flake8_copyright/rules/missing_copyright_notice.rs @@ -0,0 +1,59 @@ +use ruff_text_size::{TextRange, TextSize}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::source_code::Locator; + +use crate::settings::Settings; + +/// ## What it does +/// Checks for the absence of copyright notices within Python files. +/// +/// ## Why is this bad? +/// In some codebases, it's common to have a license header at the top of every +/// file. This rule ensures that the license header is present. +#[violation] +pub struct MissingCopyrightNotice; + +impl Violation for MissingCopyrightNotice { + #[derive_message_formats] + fn message(&self) -> String { + format!("Missing copyright notice at top of file") + } +} + +/// CPY001 +pub(crate) fn missing_copyright_notice( + locator: &Locator, + settings: &Settings, +) -> Option { + // Ignore files that are too small to contain a copyright notice. + if locator.len() < settings.flake8_copyright.min_file_size { + return None; + } + + // Only search the first 1024 bytes in the file. + let contents = if locator.len() < 1024 { + locator.contents() + } else { + locator.up_to(TextSize::from(1024)) + }; + + // Locate the copyright notice. + if let Some(match_) = settings.flake8_copyright.notice_rgx.find(contents) { + match settings.flake8_copyright.author { + Some(ref author) => { + // Ensure that it's immediately followed by the author. + if contents[match_.end()..].trim_start().starts_with(author) { + return None; + } + } + None => return None, + } + } + + Some(Diagnostic::new( + MissingCopyrightNotice, + TextRange::default(), + )) +} diff --git a/crates/ruff/src/rules/flake8_copyright/rules/mod.rs b/crates/ruff/src/rules/flake8_copyright/rules/mod.rs new file mode 100644 index 0000000000..0ad5c3a421 --- /dev/null +++ b/crates/ruff/src/rules/flake8_copyright/rules/mod.rs @@ -0,0 +1,3 @@ +pub(crate) use missing_copyright_notice::*; + +mod missing_copyright_notice; diff --git a/crates/ruff/src/rules/flake8_copyright/settings.rs b/crates/ruff/src/rules/flake8_copyright/settings.rs new file mode 100644 index 0000000000..f4f8e0c17e --- /dev/null +++ b/crates/ruff/src/rules/flake8_copyright/settings.rs @@ -0,0 +1,95 @@ +//! Settings for the `flake8-copyright` plugin. + +use once_cell::sync::Lazy; +use regex::Regex; +use serde::{Deserialize, Serialize}; + +use ruff_macros::{CacheKey, CombineOptions, ConfigurationOptions}; + +#[derive( + Debug, PartialEq, Eq, Serialize, Deserialize, Default, ConfigurationOptions, CombineOptions, +)] +#[serde( + deny_unknown_fields, + rename_all = "kebab-case", + rename = "Flake8CopyrightOptions" +)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub struct Options { + #[option( + default = r#"(?i)Copyright\s+(\(C\)\s+)?\d{4}([-,]\d{4})*"#, + value_type = "str", + example = r#"notice-rgx = "(?i)Copyright \\(C\\) \\d{4}""# + )] + /// The regular expression used to match the copyright notice, compiled + /// with the [`regex`](https://docs.rs/regex/latest/regex/) crate. + /// + /// Defaults to `(?i)Copyright\s+(\(C\)\s+)?\d{4}(-\d{4})*`, which matches + /// the following: + /// - `Copyright 2023` + /// - `Copyright (C) 2023` + /// - `Copyright 2021-2023` + /// - `Copyright (C) 2021-2023` + pub notice_rgx: Option, + #[option(default = "None", value_type = "str", example = r#"author = "Ruff""#)] + /// Author to enforce within the copyright notice. If provided, the + /// author must be present immediately following the copyright notice. + pub author: Option, + #[option( + default = r#"0"#, + value_type = "int", + example = r#" + # Avoid enforcing a header on files smaller than 1024 bytes. + min-file-size = 1024 + "# + )] + /// A minimum file size (in bytes) required for a copyright notice to + /// be enforced. By default, all files are validated. + pub min_file_size: Option, +} + +#[derive(Debug, CacheKey)] +pub struct Settings { + pub notice_rgx: Regex, + pub author: Option, + pub min_file_size: usize, +} + +static COPYRIGHT: Lazy = + Lazy::new(|| Regex::new(r"(?i)Copyright\s+(\(C\)\s+)?\d{4}(-\d{4})*").unwrap()); + +impl Default for Settings { + fn default() -> Self { + Self { + notice_rgx: COPYRIGHT.clone(), + author: None, + min_file_size: 0, + } + } +} + +impl TryFrom for Settings { + type Error = anyhow::Error; + + fn try_from(value: Options) -> Result { + Ok(Self { + notice_rgx: value + .notice_rgx + .map(|pattern| Regex::new(&pattern)) + .transpose()? + .unwrap_or_else(|| COPYRIGHT.clone()), + author: value.author, + min_file_size: value.min_file_size.unwrap_or_default(), + }) + } +} + +impl From for Options { + fn from(settings: Settings) -> Self { + Self { + notice_rgx: Some(settings.notice_rgx.to_string()), + author: settings.author, + min_file_size: Some(settings.min_file_size), + } + } +} diff --git a/crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__invalid_author.snap b/crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__invalid_author.snap new file mode 100644 index 0000000000..4393791320 --- /dev/null +++ b/crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__invalid_author.snap @@ -0,0 +1,12 @@ +--- +source: crates/ruff/src/rules/flake8_copyright/mod.rs +--- +:1:1: CPY001 Missing copyright notice at top of file + | +1 | # Copyright (C) 2023 Some Author + | CPY001 +2 | +3 | import os + | + + diff --git a/crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__late_notice.snap b/crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__late_notice.snap new file mode 100644 index 0000000000..17efa2aa8d --- /dev/null +++ b/crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__late_notice.snap @@ -0,0 +1,12 @@ +--- +source: crates/ruff/src/rules/flake8_copyright/mod.rs +--- +:1:1: CPY001 Missing copyright notice at top of file + | +1 | # Content Content Content Content Content Content Content Content Content Content + | CPY001 +2 | # Content Content Content Content Content Content Content Content Content Content +3 | # Content Content Content Content Content Content Content Content Content Content + | + + diff --git a/crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__notice.snap b/crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__notice.snap new file mode 100644 index 0000000000..383a7b0dac --- /dev/null +++ b/crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__notice.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/flake8_copyright/mod.rs +--- + diff --git a/crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__notice_with_c.snap b/crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__notice_with_c.snap new file mode 100644 index 0000000000..383a7b0dac --- /dev/null +++ b/crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__notice_with_c.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/flake8_copyright/mod.rs +--- + diff --git a/crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__notice_with_caps.snap b/crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__notice_with_caps.snap new file mode 100644 index 0000000000..383a7b0dac --- /dev/null +++ b/crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__notice_with_caps.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/flake8_copyright/mod.rs +--- + diff --git a/crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__notice_with_range.snap b/crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__notice_with_range.snap new file mode 100644 index 0000000000..383a7b0dac --- /dev/null +++ b/crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__notice_with_range.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/flake8_copyright/mod.rs +--- + diff --git a/crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__small_file.snap b/crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__small_file.snap new file mode 100644 index 0000000000..383a7b0dac --- /dev/null +++ b/crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__small_file.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/flake8_copyright/mod.rs +--- + diff --git a/crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__valid_author.snap b/crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__valid_author.snap new file mode 100644 index 0000000000..383a7b0dac --- /dev/null +++ b/crates/ruff/src/rules/flake8_copyright/snapshots/ruff__rules__flake8_copyright__tests__valid_author.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/flake8_copyright/mod.rs +--- + diff --git a/crates/ruff/src/rules/flake8_datetimez/rules/call_date_fromtimestamp.rs b/crates/ruff/src/rules/flake8_datetimez/rules/call_date_fromtimestamp.rs index 3029801216..1c41e6c702 100644 --- a/crates/ruff/src/rules/flake8_datetimez/rules/call_date_fromtimestamp.rs +++ b/crates/ruff/src/rules/flake8_datetimez/rules/call_date_fromtimestamp.rs @@ -27,10 +27,10 @@ impl Violation for CallDateFromtimestamp { /// Use `datetime.datetime.fromtimestamp(, tz=).date()` instead. pub(crate) fn call_date_fromtimestamp(checker: &mut Checker, func: &Expr, location: TextRange) { if checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["datetime", "date", "fromtimestamp"] + matches!(call_path.as_slice(), ["datetime", "date", "fromtimestamp"]) }) { checker diff --git a/crates/ruff/src/rules/flake8_datetimez/rules/call_date_today.rs b/crates/ruff/src/rules/flake8_datetimez/rules/call_date_today.rs index 461fa06761..282dff64fe 100644 --- a/crates/ruff/src/rules/flake8_datetimez/rules/call_date_today.rs +++ b/crates/ruff/src/rules/flake8_datetimez/rules/call_date_today.rs @@ -27,10 +27,10 @@ impl Violation for CallDateToday { /// Use `datetime.datetime.now(tz=).date()` instead. pub(crate) fn call_date_today(checker: &mut Checker, func: &Expr, location: TextRange) { if checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["datetime", "date", "today"] + matches!(call_path.as_slice(), ["datetime", "date", "today"]) }) { checker diff --git a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_fromtimestamp.rs b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_fromtimestamp.rs index 4ff08ba885..5ce71fa558 100644 --- a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_fromtimestamp.rs +++ b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_fromtimestamp.rs @@ -7,6 +7,8 @@ use ruff_python_ast::helpers::{has_non_none_keyword, is_const_none}; use crate::checkers::ast::Checker; +use super::helpers; + #[violation] pub struct CallDatetimeFromtimestamp; @@ -28,15 +30,22 @@ pub(crate) fn call_datetime_fromtimestamp( location: TextRange, ) { if !checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["datetime", "datetime", "fromtimestamp"] + matches!( + call_path.as_slice(), + ["datetime", "datetime", "fromtimestamp"] + ) }) { return; } + if helpers::parent_expr_is_astimezone(checker) { + return; + } + // no args / no args unqualified if args.len() < 2 && keywords.is_empty() { checker diff --git a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_now_without_tzinfo.rs b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_now_without_tzinfo.rs index 46202fb6a2..478fc06c10 100644 --- a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_now_without_tzinfo.rs +++ b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_now_without_tzinfo.rs @@ -7,6 +7,8 @@ use ruff_python_ast::helpers::{has_non_none_keyword, is_const_none}; use crate::checkers::ast::Checker; +use super::helpers; + #[violation] pub struct CallDatetimeNowWithoutTzinfo; @@ -26,15 +28,19 @@ pub(crate) fn call_datetime_now_without_tzinfo( location: TextRange, ) { if !checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["datetime", "datetime", "now"] + matches!(call_path.as_slice(), ["datetime", "datetime", "now"]) }) { return; } + if helpers::parent_expr_is_astimezone(checker) { + return; + } + // no args / no args unqualified if args.is_empty() && keywords.is_empty() { checker diff --git a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_strptime_without_zone.rs b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_strptime_without_zone.rs index 2cc4fb0b99..f690027e80 100644 --- a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_strptime_without_zone.rs +++ b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_strptime_without_zone.rs @@ -28,10 +28,10 @@ pub(crate) fn call_datetime_strptime_without_zone( location: TextRange, ) { if !checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["datetime", "datetime", "strptime"] + matches!(call_path.as_slice(), ["datetime", "datetime", "strptime"]) }) { return; @@ -49,11 +49,13 @@ pub(crate) fn call_datetime_strptime_without_zone( } }; - let (Some(grandparent), Some(parent)) = (checker.semantic_model().expr_grandparent(), checker.semantic_model().expr_parent()) else { - checker.diagnostics.push(Diagnostic::new( - CallDatetimeStrptimeWithoutZone, - location, - )); + let (Some(grandparent), Some(parent)) = ( + checker.semantic().expr_grandparent(), + checker.semantic().expr_parent(), + ) else { + checker + .diagnostics + .push(Diagnostic::new(CallDatetimeStrptimeWithoutZone, location)); return; }; diff --git a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_today.rs b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_today.rs index 5c42c7c176..0738b1ae70 100644 --- a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_today.rs +++ b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_today.rs @@ -6,6 +6,8 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; +use super::helpers; + #[violation] pub struct CallDatetimeToday; @@ -26,15 +28,21 @@ impl Violation for CallDatetimeToday { /// It uses the system local timezone. /// Use `datetime.datetime.now(tz=)` instead. pub(crate) fn call_datetime_today(checker: &mut Checker, func: &Expr, location: TextRange) { - if checker - .semantic_model() + if !checker + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["datetime", "datetime", "today"] + matches!(call_path.as_slice(), ["datetime", "datetime", "today"]) }) { - checker - .diagnostics - .push(Diagnostic::new(CallDatetimeToday, location)); + return; } + + if helpers::parent_expr_is_astimezone(checker) { + return; + } + + checker + .diagnostics + .push(Diagnostic::new(CallDatetimeToday, location)); } diff --git a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_utcfromtimestamp.rs b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_utcfromtimestamp.rs index ffa73e5416..c63c79483f 100644 --- a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_utcfromtimestamp.rs +++ b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_utcfromtimestamp.rs @@ -6,6 +6,8 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; +use super::helpers; + #[violation] pub struct CallDatetimeUtcfromtimestamp; @@ -33,15 +35,24 @@ pub(crate) fn call_datetime_utcfromtimestamp( func: &Expr, location: TextRange, ) { - if checker - .semantic_model() + if !checker + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["datetime", "datetime", "utcfromtimestamp"] + matches!( + call_path.as_slice(), + ["datetime", "datetime", "utcfromtimestamp"] + ) }) { - checker - .diagnostics - .push(Diagnostic::new(CallDatetimeUtcfromtimestamp, location)); + return; } + + if helpers::parent_expr_is_astimezone(checker) { + return; + } + + checker + .diagnostics + .push(Diagnostic::new(CallDatetimeUtcfromtimestamp, location)); } diff --git a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_utcnow.rs b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_utcnow.rs index 33d89f8673..1c92b1ea66 100644 --- a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_utcnow.rs +++ b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_utcnow.rs @@ -6,6 +6,8 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; +use super::helpers; + #[violation] pub struct CallDatetimeUtcnow; @@ -28,15 +30,21 @@ impl Violation for CallDatetimeUtcnow { /// UTC. As such, the recommended way to create an object representing the /// current time in UTC is by calling `datetime.now(timezone.utc)`. pub(crate) fn call_datetime_utcnow(checker: &mut Checker, func: &Expr, location: TextRange) { - if checker - .semantic_model() + if !checker + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["datetime", "datetime", "utcnow"] + matches!(call_path.as_slice(), ["datetime", "datetime", "utcnow"]) }) { - checker - .diagnostics - .push(Diagnostic::new(CallDatetimeUtcnow, location)); + return; } + + if helpers::parent_expr_is_astimezone(checker) { + return; + } + + checker + .diagnostics + .push(Diagnostic::new(CallDatetimeUtcnow, location)); } diff --git a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_without_tzinfo.rs b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_without_tzinfo.rs index af7eabf837..cf153a5c7d 100644 --- a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_without_tzinfo.rs +++ b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_without_tzinfo.rs @@ -7,6 +7,8 @@ use ruff_python_ast::helpers::{has_non_none_keyword, is_const_none}; use crate::checkers::ast::Checker; +use super::helpers; + #[violation] pub struct CallDatetimeWithoutTzinfo; @@ -25,15 +27,19 @@ pub(crate) fn call_datetime_without_tzinfo( location: TextRange, ) { if !checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["datetime", "datetime"] + matches!(call_path.as_slice(), ["datetime", "datetime"]) }) { return; } + if helpers::parent_expr_is_astimezone(checker) { + return; + } + // No positional arg: keyword is missing or constant None. if args.len() < 8 && !has_non_none_keyword(keywords, "tzinfo") { checker diff --git a/crates/ruff/src/rules/flake8_datetimez/rules/helpers.rs b/crates/ruff/src/rules/flake8_datetimez/rules/helpers.rs new file mode 100644 index 0000000000..bd237f9da1 --- /dev/null +++ b/crates/ruff/src/rules/flake8_datetimez/rules/helpers.rs @@ -0,0 +1,11 @@ +use rustpython_parser::ast::{Expr, ExprAttribute}; + +use crate::checkers::ast::Checker; + +/// Check if the parent expression is a call to `astimezone`. This assumes that +/// the current expression is a `datetime.datetime` object. +pub(crate) fn parent_expr_is_astimezone(checker: &Checker) -> bool { + checker.semantic().expr_parent().map_or(false, |parent| { + matches!(parent, Expr::Attribute(ExprAttribute { attr, .. }) if attr.as_str() == "astimezone") + }) +} diff --git a/crates/ruff/src/rules/flake8_datetimez/rules/mod.rs b/crates/ruff/src/rules/flake8_datetimez/rules/mod.rs index 8cfa09cdcc..87646ca775 100644 --- a/crates/ruff/src/rules/flake8_datetimez/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_datetimez/rules/mod.rs @@ -1,22 +1,12 @@ -pub(crate) use call_date_fromtimestamp::{call_date_fromtimestamp, CallDateFromtimestamp}; -pub(crate) use call_date_today::{call_date_today, CallDateToday}; -pub(crate) use call_datetime_fromtimestamp::{ - call_datetime_fromtimestamp, CallDatetimeFromtimestamp, -}; -pub(crate) use call_datetime_now_without_tzinfo::{ - call_datetime_now_without_tzinfo, CallDatetimeNowWithoutTzinfo, -}; -pub(crate) use call_datetime_strptime_without_zone::{ - call_datetime_strptime_without_zone, CallDatetimeStrptimeWithoutZone, -}; -pub(crate) use call_datetime_today::{call_datetime_today, CallDatetimeToday}; -pub(crate) use call_datetime_utcfromtimestamp::{ - call_datetime_utcfromtimestamp, CallDatetimeUtcfromtimestamp, -}; -pub(crate) use call_datetime_utcnow::{call_datetime_utcnow, CallDatetimeUtcnow}; -pub(crate) use call_datetime_without_tzinfo::{ - call_datetime_without_tzinfo, CallDatetimeWithoutTzinfo, -}; +pub(crate) use call_date_fromtimestamp::*; +pub(crate) use call_date_today::*; +pub(crate) use call_datetime_fromtimestamp::*; +pub(crate) use call_datetime_now_without_tzinfo::*; +pub(crate) use call_datetime_strptime_without_zone::*; +pub(crate) use call_datetime_today::*; +pub(crate) use call_datetime_utcfromtimestamp::*; +pub(crate) use call_datetime_utcnow::*; +pub(crate) use call_datetime_without_tzinfo::*; mod call_date_fromtimestamp; mod call_date_today; @@ -27,3 +17,4 @@ mod call_datetime_today; mod call_datetime_utcfromtimestamp; mod call_datetime_utcnow; mod call_datetime_without_tzinfo; +mod helpers; diff --git a/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ001_DTZ001.py.snap b/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ001_DTZ001.py.snap index c6b3004ade..70434e4d3e 100644 --- a/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ001_DTZ001.py.snap +++ b/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ001_DTZ001.py.snap @@ -42,6 +42,8 @@ DTZ001.py:21:1: DTZ001 The use of `datetime.datetime()` without `tzinfo` argumen 20 | # no args unqualified 21 | datetime(2000, 1, 1, 0, 0, 0) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ DTZ001 +22 | +23 | # uses `astimezone` method | diff --git a/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ002_DTZ002.py.snap b/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ002_DTZ002.py.snap index 699e908355..8ea66ef3ee 100644 --- a/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ002_DTZ002.py.snap +++ b/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ002_DTZ002.py.snap @@ -11,10 +11,12 @@ DTZ002.py:4:1: DTZ002 The use of `datetime.datetime.today()` is not allowed, use | DTZ002.py:9:1: DTZ002 The use of `datetime.datetime.today()` is not allowed, use `datetime.datetime.now(tz=)` instead - | -8 | # unqualified -9 | datetime.today() - | ^^^^^^^^^^^^^^^^ DTZ002 - | + | + 8 | # unqualified + 9 | datetime.today() + | ^^^^^^^^^^^^^^^^ DTZ002 +10 | +11 | # uses `astimezone` method + | diff --git a/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ003_DTZ003.py.snap b/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ003_DTZ003.py.snap index acf0408afd..3bf5215e39 100644 --- a/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ003_DTZ003.py.snap +++ b/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ003_DTZ003.py.snap @@ -11,10 +11,12 @@ DTZ003.py:4:1: DTZ003 The use of `datetime.datetime.utcnow()` is not allowed, us | DTZ003.py:9:1: DTZ003 The use of `datetime.datetime.utcnow()` is not allowed, use `datetime.datetime.now(tz=)` instead - | -8 | # unqualified -9 | datetime.utcnow() - | ^^^^^^^^^^^^^^^^^ DTZ003 - | + | + 8 | # unqualified + 9 | datetime.utcnow() + | ^^^^^^^^^^^^^^^^^ DTZ003 +10 | +11 | # uses `astimezone` method + | diff --git a/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ004_DTZ004.py.snap b/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ004_DTZ004.py.snap index 20cb555d19..95f2f2e68a 100644 --- a/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ004_DTZ004.py.snap +++ b/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ004_DTZ004.py.snap @@ -11,10 +11,12 @@ DTZ004.py:4:1: DTZ004 The use of `datetime.datetime.utcfromtimestamp()` is not a | DTZ004.py:9:1: DTZ004 The use of `datetime.datetime.utcfromtimestamp()` is not allowed, use `datetime.datetime.fromtimestamp(ts, tz=)` instead - | -8 | # unqualified -9 | datetime.utcfromtimestamp(1234) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ DTZ004 - | + | + 8 | # unqualified + 9 | datetime.utcfromtimestamp(1234) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ DTZ004 +10 | +11 | # uses `astimezone` method + | diff --git a/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ005_DTZ005.py.snap b/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ005_DTZ005.py.snap index b6b7d88ba3..e037abffa6 100644 --- a/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ005_DTZ005.py.snap +++ b/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ005_DTZ005.py.snap @@ -42,6 +42,8 @@ DTZ005.py:18:1: DTZ005 The use of `datetime.datetime.now()` without `tz` argumen 17 | # no args unqualified 18 | datetime.now() | ^^^^^^^^^^^^^^ DTZ005 +19 | +20 | # uses `astimezone` method | diff --git a/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ006_DTZ006.py.snap b/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ006_DTZ006.py.snap index 65b7579403..d5cba0cd7c 100644 --- a/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ006_DTZ006.py.snap +++ b/crates/ruff/src/rules/flake8_datetimez/snapshots/ruff__rules__flake8_datetimez__tests__DTZ006_DTZ006.py.snap @@ -42,6 +42,8 @@ DTZ006.py:18:1: DTZ006 The use of `datetime.datetime.fromtimestamp()` without `t 17 | # no args unqualified 18 | datetime.fromtimestamp(1234) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ DTZ006 +19 | +20 | # uses `astimezone` method | diff --git a/crates/ruff/src/rules/flake8_debugger/rules/debugger.rs b/crates/ruff/src/rules/flake8_debugger/rules/debugger.rs index 0dfdc9ce83..08a022e62a 100644 --- a/crates/ruff/src/rules/flake8_debugger/rules/debugger.rs +++ b/crates/ruff/src/rules/flake8_debugger/rules/debugger.rs @@ -7,6 +7,28 @@ use ruff_python_ast::call_path::{format_call_path, from_unqualified_name, CallPa use crate::checkers::ast::Checker; use crate::rules::flake8_debugger::types::DebuggerUsingType; +/// ## What it does +/// Checks for the presence of debugger calls and imports. +/// +/// ## Why is this bad? +/// Debugger calls and imports should be used for debugging purposes only. The +/// presence of a debugger call or import in production code is likely a +/// mistake and may cause unintended behavior, such as exposing sensitive +/// information or causing the program to hang. +/// +/// Instead, consider using a logging library to log information about the +/// program's state, and writing tests to verify that the program behaves +/// as expected. +/// +/// ## Example +/// ```python +/// def foo(): +/// breakpoint() +/// ``` +/// +/// ## References +/// - [Python documentation: `pdb` — The Python Debugger](https://docs.python.org/3/library/pdb.html) +/// - [Python documentation: `logging` — Logging facility for Python](https://docs.python.org/3/library/logging.html) #[violation] pub struct Debugger { using_type: DebuggerUsingType, @@ -23,59 +45,32 @@ impl Violation for Debugger { } } -const DEBUGGERS: &[&[&str]] = &[ - &["pdb", "set_trace"], - &["pudb", "set_trace"], - &["ipdb", "set_trace"], - &["ipdb", "sset_trace"], - &["IPython", "terminal", "embed", "InteractiveShellEmbed"], - &[ - "IPython", - "frontend", - "terminal", - "embed", - "InteractiveShellEmbed", - ], - &["celery", "contrib", "rdb", "set_trace"], - &["builtins", "breakpoint"], - &["", "breakpoint"], -]; - /// Checks for the presence of a debugger call. pub(crate) fn debugger_call(checker: &mut Checker, expr: &Expr, func: &Expr) { - if let Some(target) = checker - .semantic_model() + if let Some(using_type) = checker + .semantic() .resolve_call_path(func) .and_then(|call_path| { - DEBUGGERS - .iter() - .find(|target| call_path.as_slice() == **target) + if is_debugger_call(&call_path) { + Some(DebuggerUsingType::Call(format_call_path(&call_path))) + } else { + None + } }) { - checker.diagnostics.push(Diagnostic::new( - Debugger { - using_type: DebuggerUsingType::Call(format_call_path(target)), - }, - expr.range(), - )); + checker + .diagnostics + .push(Diagnostic::new(Debugger { using_type }, expr.range())); } } /// Checks for the presence of a debugger import. pub(crate) fn debugger_import(stmt: &Stmt, module: Option<&str>, name: &str) -> Option { - // Special-case: allow `import builtins`, which is far more general than (e.g.) - // `import celery.contrib.rdb`). - if module.is_none() && name == "builtins" { - return None; - } - if let Some(module) = module { let mut call_path: CallPath = from_unqualified_name(module); call_path.push(name); - if DEBUGGERS - .iter() - .any(|target| call_path.as_slice() == *target) - { + + if is_debugger_call(&call_path) { return Some(Diagnostic::new( Debugger { using_type: DebuggerUsingType::Import(format_call_path(&call_path)), @@ -84,11 +79,9 @@ pub(crate) fn debugger_import(stmt: &Stmt, module: Option<&str>, name: &str) -> )); } } else { - let parts: CallPath = from_unqualified_name(name); - if DEBUGGERS - .iter() - .any(|call_path| &call_path[..call_path.len() - 1] == parts.as_slice()) - { + let call_path: CallPath = from_unqualified_name(name); + + if is_debugger_import(&call_path) { return Some(Diagnostic::new( Debugger { using_type: DebuggerUsingType::Import(name.to_string()), @@ -99,3 +92,35 @@ pub(crate) fn debugger_import(stmt: &Stmt, module: Option<&str>, name: &str) -> } None } + +fn is_debugger_call(call_path: &CallPath) -> bool { + matches!( + call_path.as_slice(), + ["pdb" | "pudb" | "ipdb", "set_trace"] + | ["ipdb", "sset_trace"] + | ["IPython", "terminal", "embed", "InteractiveShellEmbed"] + | [ + "IPython", + "frontend", + "terminal", + "embed", + "InteractiveShellEmbed" + ] + | ["celery", "contrib", "rdb", "set_trace"] + | ["builtins" | "", "breakpoint"] + ) +} + +fn is_debugger_import(call_path: &CallPath) -> bool { + // Constructed by taking every pattern in `is_debugger_call`, removing the last element in + // each pattern, and de-duplicating the values. + // As a special-case, we omit `builtins` to allow `import builtins`, which is far more general + // than (e.g.) `import celery.contrib.rdb`. + matches!( + call_path.as_slice(), + ["pdb" | "pudb" | "ipdb"] + | ["IPython", "terminal", "embed"] + | ["IPython", "frontend", "terminal", "embed",] + | ["celery", "contrib", "rdb"] + ) +} diff --git a/crates/ruff/src/rules/flake8_debugger/rules/mod.rs b/crates/ruff/src/rules/flake8_debugger/rules/mod.rs index 1dda738e6a..c9e411937a 100644 --- a/crates/ruff/src/rules/flake8_debugger/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_debugger/rules/mod.rs @@ -1,3 +1,3 @@ -pub(crate) use debugger::{debugger_call, debugger_import, Debugger}; +pub(crate) use debugger::*; mod debugger; diff --git a/crates/ruff/src/rules/flake8_django/rules/all_with_model_form.rs b/crates/ruff/src/rules/flake8_django/rules/all_with_model_form.rs index da019f071a..63dc9551d6 100644 --- a/crates/ruff/src/rules/flake8_django/rules/all_with_model_form.rs +++ b/crates/ruff/src/rules/flake8_django/rules/all_with_model_form.rs @@ -54,22 +54,22 @@ pub(crate) fn all_with_model_form( ) -> Option { if !bases .iter() - .any(|base| is_model_form(checker.semantic_model(), base)) + .any(|base| is_model_form(base, checker.semantic())) { return None; } - for element in body.iter() { + for element in body { let Stmt::ClassDef(ast::StmtClassDef { name, body, .. }) = element else { continue; }; if name != "Meta" { continue; } - for element in body.iter() { + for element in body { let Stmt::Assign(ast::StmtAssign { targets, value, .. }) = element else { continue; }; - for target in targets.iter() { + for target in targets { let Expr::Name(ast::ExprName { id, .. }) = target else { continue; }; diff --git a/crates/ruff/src/rules/flake8_django/rules/exclude_with_model_form.rs b/crates/ruff/src/rules/flake8_django/rules/exclude_with_model_form.rs index af229a6a32..8079eb3099 100644 --- a/crates/ruff/src/rules/flake8_django/rules/exclude_with_model_form.rs +++ b/crates/ruff/src/rules/flake8_django/rules/exclude_with_model_form.rs @@ -52,22 +52,22 @@ pub(crate) fn exclude_with_model_form( ) -> Option { if !bases .iter() - .any(|base| is_model_form(checker.semantic_model(), base)) + .any(|base| is_model_form(base, checker.semantic())) { return None; } - for element in body.iter() { + for element in body { let Stmt::ClassDef(ast::StmtClassDef { name, body, .. }) = element else { continue; }; if name != "Meta" { continue; } - for element in body.iter() { + for element in body { let Stmt::Assign(ast::StmtAssign { targets, .. }) = element else { continue; }; - for target in targets.iter() { + for target in targets { let Expr::Name(ast::ExprName { id, .. }) = target else { continue; }; diff --git a/crates/ruff/src/rules/flake8_django/rules/helpers.rs b/crates/ruff/src/rules/flake8_django/rules/helpers.rs index 3dc1353045..5c208cc235 100644 --- a/crates/ruff/src/rules/flake8_django/rules/helpers.rs +++ b/crates/ruff/src/rules/flake8_django/rules/helpers.rs @@ -1,25 +1,27 @@ use rustpython_parser::ast::Expr; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; /// Return `true` if a Python class appears to be a Django model, based on its base classes. -pub(super) fn is_model(model: &SemanticModel, base: &Expr) -> bool { - model.resolve_call_path(base).map_or(false, |call_path| { - call_path.as_slice() == ["django", "db", "models", "Model"] +pub(super) fn is_model(base: &Expr, semantic: &SemanticModel) -> bool { + semantic.resolve_call_path(base).map_or(false, |call_path| { + matches!(call_path.as_slice(), ["django", "db", "models", "Model"]) }) } /// Return `true` if a Python class appears to be a Django model form, based on its base classes. -pub(super) fn is_model_form(model: &SemanticModel, base: &Expr) -> bool { - model.resolve_call_path(base).map_or(false, |call_path| { - call_path.as_slice() == ["django", "forms", "ModelForm"] - || call_path.as_slice() == ["django", "forms", "models", "ModelForm"] +pub(super) fn is_model_form(base: &Expr, semantic: &SemanticModel) -> bool { + semantic.resolve_call_path(base).map_or(false, |call_path| { + matches!( + call_path.as_slice(), + ["django", "forms", "ModelForm"] | ["django", "forms", "models", "ModelForm"] + ) }) } /// Return `true` if the expression is constructor for a Django model field. -pub(super) fn is_model_field(model: &SemanticModel, expr: &Expr) -> bool { - model.resolve_call_path(expr).map_or(false, |call_path| { +pub(super) fn is_model_field(expr: &Expr, semantic: &SemanticModel) -> bool { + semantic.resolve_call_path(expr).map_or(false, |call_path| { call_path .as_slice() .starts_with(&["django", "db", "models"]) @@ -28,10 +30,10 @@ pub(super) fn is_model_field(model: &SemanticModel, expr: &Expr) -> bool { /// Return the name of the field type, if the expression is constructor for a Django model field. pub(super) fn get_model_field_name<'a>( - model: &'a SemanticModel, expr: &'a Expr, + semantic: &'a SemanticModel, ) -> Option<&'a str> { - model.resolve_call_path(expr).and_then(|call_path| { + semantic.resolve_call_path(expr).and_then(|call_path| { let call_path = call_path.as_slice(); if !call_path.starts_with(&["django", "db", "models"]) { return None; diff --git a/crates/ruff/src/rules/flake8_django/rules/locals_in_render_function.rs b/crates/ruff/src/rules/flake8_django/rules/locals_in_render_function.rs index 469ff81eb6..50cd33d67d 100644 --- a/crates/ruff/src/rules/flake8_django/rules/locals_in_render_function.rs +++ b/crates/ruff/src/rules/flake8_django/rules/locals_in_render_function.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::{self, Expr, Keyword, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; use crate::checkers::ast::Checker; @@ -51,17 +51,17 @@ pub(crate) fn locals_in_render_function( keywords: &[Keyword], ) { if !checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["django", "shortcuts", "render"] + matches!(call_path.as_slice(), ["django", "shortcuts", "render"]) }) { return; } let locals = if args.len() >= 3 { - if !is_locals_call(checker.semantic_model(), &args[2]) { + if !is_locals_call(&args[2], checker.semantic()) { return; } &args[2] @@ -69,7 +69,7 @@ pub(crate) fn locals_in_render_function( .iter() .find(|keyword| keyword.arg.as_ref().map_or(false, |arg| arg == "context")) { - if !is_locals_call(checker.semantic_model(), &keyword.value) { + if !is_locals_call(&keyword.value, checker.semantic()) { return; } &keyword.value @@ -83,11 +83,11 @@ pub(crate) fn locals_in_render_function( )); } -fn is_locals_call(model: &SemanticModel, expr: &Expr) -> bool { +fn is_locals_call(expr: &Expr, semantic: &SemanticModel) -> bool { let Expr::Call(ast::ExprCall { func, .. }) = expr else { - return false + return false; }; - model - .resolve_call_path(func) - .map_or(false, |call_path| call_path.as_slice() == ["", "locals"]) + semantic.resolve_call_path(func).map_or(false, |call_path| { + matches!(call_path.as_slice(), ["", "locals"]) + }) } diff --git a/crates/ruff/src/rules/flake8_django/rules/mod.rs b/crates/ruff/src/rules/flake8_django/rules/mod.rs index 2b5188d71a..145dbe2149 100644 --- a/crates/ruff/src/rules/flake8_django/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_django/rules/mod.rs @@ -1,18 +1,10 @@ -pub(crate) use all_with_model_form::{all_with_model_form, DjangoAllWithModelForm}; -pub(crate) use exclude_with_model_form::{exclude_with_model_form, DjangoExcludeWithModelForm}; -pub(crate) use locals_in_render_function::{ - locals_in_render_function, DjangoLocalsInRenderFunction, -}; -pub(crate) use model_without_dunder_str::{model_without_dunder_str, DjangoModelWithoutDunderStr}; -pub(crate) use non_leading_receiver_decorator::{ - non_leading_receiver_decorator, DjangoNonLeadingReceiverDecorator, -}; -pub(crate) use nullable_model_string_field::{ - nullable_model_string_field, DjangoNullableModelStringField, -}; -pub(crate) use unordered_body_content_in_model::{ - unordered_body_content_in_model, DjangoUnorderedBodyContentInModel, -}; +pub(crate) use all_with_model_form::*; +pub(crate) use exclude_with_model_form::*; +pub(crate) use locals_in_render_function::*; +pub(crate) use model_without_dunder_str::*; +pub(crate) use non_leading_receiver_decorator::*; +pub(crate) use nullable_model_string_field::*; +pub(crate) use unordered_body_content_in_model::*; mod all_with_model_form; mod exclude_with_model_form; diff --git a/crates/ruff/src/rules/flake8_django/rules/model_without_dunder_str.rs b/crates/ruff/src/rules/flake8_django/rules/model_without_dunder_str.rs index 5e07cbfa53..6c0718f95a 100644 --- a/crates/ruff/src/rules/flake8_django/rules/model_without_dunder_str.rs +++ b/crates/ruff/src/rules/flake8_django/rules/model_without_dunder_str.rs @@ -1,8 +1,9 @@ -use rustpython_parser::ast::{self, Constant, Expr, Ranged, Stmt}; +use rustpython_parser::ast::{self, Expr, Ranged, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_ast::helpers::is_const_true; +use ruff_python_semantic::SemanticModel; use crate::checkers::ast::Checker; @@ -51,21 +52,20 @@ impl Violation for DjangoModelWithoutDunderStr { /// DJ008 pub(crate) fn model_without_dunder_str( - checker: &Checker, - bases: &[Expr], - body: &[Stmt], - class_location: &Stmt, -) -> Option { - if !checker_applies(checker.semantic_model(), bases, body) { - return None; + checker: &mut Checker, + ast::StmtClassDef { + name, bases, body, .. + }: &ast::StmtClassDef, +) { + if !is_non_abstract_model(bases, body, checker.semantic()) { + return; } - if !has_dunder_method(body) { - return Some(Diagnostic::new( - DjangoModelWithoutDunderStr, - class_location.range(), - )); + if has_dunder_method(body) { + return; } - None + checker + .diagnostics + .push(Diagnostic::new(DjangoModelWithoutDunderStr, name.range())); } fn has_dunder_method(body: &[Stmt]) -> bool { @@ -80,12 +80,12 @@ fn has_dunder_method(body: &[Stmt]) -> bool { }) } -fn checker_applies(model: &SemanticModel, bases: &[Expr], body: &[Stmt]) -> bool { - for base in bases.iter() { +fn is_non_abstract_model(bases: &[Expr], body: &[Stmt], semantic: &SemanticModel) -> bool { + for base in bases { if is_model_abstract(body) { continue; } - if helpers::is_model(model, base) { + if helpers::is_model(base, semantic) { return true; } } @@ -94,27 +94,27 @@ fn checker_applies(model: &SemanticModel, bases: &[Expr], body: &[Stmt]) -> bool /// Check if class is abstract, in terms of Django model inheritance. fn is_model_abstract(body: &[Stmt]) -> bool { - for element in body.iter() { - let Stmt::ClassDef(ast::StmtClassDef {name, body, ..}) = element else { - continue + for element in body { + let Stmt::ClassDef(ast::StmtClassDef { name, body, .. }) = element else { + continue; }; if name != "Meta" { continue; } - for element in body.iter() { - let Stmt::Assign(ast::StmtAssign {targets, value, ..}) = element else { + for element in body { + let Stmt::Assign(ast::StmtAssign { targets, value, .. }) = element else { continue; }; - for target in targets.iter() { - let Expr::Name(ast::ExprName {id , ..}) = target else { + for target in targets { + let Expr::Name(ast::ExprName { id, .. }) = target else { continue; }; if id != "abstract" { continue; } - let Expr::Constant(ast::ExprConstant{value: Constant::Bool(true), ..}) = value.as_ref() else { + if !is_const_true(value) { continue; - }; + } return true; } } diff --git a/crates/ruff/src/rules/flake8_django/rules/non_leading_receiver_decorator.rs b/crates/ruff/src/rules/flake8_django/rules/non_leading_receiver_decorator.rs index 7e68309510..9e823858c0 100644 --- a/crates/ruff/src/rules/flake8_django/rules/non_leading_receiver_decorator.rs +++ b/crates/ruff/src/rules/flake8_django/rules/non_leading_receiver_decorator.rs @@ -1,8 +1,9 @@ -use rustpython_parser::ast::{self, Decorator, Expr, Ranged}; +use rustpython_parser::ast::{Decorator, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::call_path::CallPath; + +use crate::checkers::ast::Checker; /// ## What it does /// Checks that Django's `@receiver` decorator is listed first, prior to @@ -48,25 +49,19 @@ impl Violation for DjangoNonLeadingReceiverDecorator { } /// DJ013 -pub(crate) fn non_leading_receiver_decorator<'a, F>( - decorator_list: &'a [Decorator], - resolve_call_path: F, -) -> Vec -where - F: Fn(&'a Expr) -> Option>, -{ - let mut diagnostics = vec![]; +pub(crate) fn non_leading_receiver_decorator(checker: &mut Checker, decorator_list: &[Decorator]) { let mut seen_receiver = false; for (i, decorator) in decorator_list.iter().enumerate() { - let is_receiver = match &decorator.expression { - Expr::Call(ast::ExprCall { func, .. }) => resolve_call_path(func) + let is_receiver = decorator.expression.as_call_expr().map_or(false, |call| { + checker + .semantic() + .resolve_call_path(&call.func) .map_or(false, |call_path| { - call_path.as_slice() == ["django", "dispatch", "receiver"] - }), - _ => false, - }; + matches!(call_path.as_slice(), ["django", "dispatch", "receiver"]) + }) + }); if i > 0 && is_receiver && !seen_receiver { - diagnostics.push(Diagnostic::new( + checker.diagnostics.push(Diagnostic::new( DjangoNonLeadingReceiverDecorator, decorator.range(), )); @@ -77,5 +72,4 @@ where seen_receiver = true; } } - diagnostics } diff --git a/crates/ruff/src/rules/flake8_django/rules/nullable_model_string_field.rs b/crates/ruff/src/rules/flake8_django/rules/nullable_model_string_field.rs index d8ccedb65b..2fc19f4f28 100644 --- a/crates/ruff/src/rules/flake8_django/rules/nullable_model_string_field.rs +++ b/crates/ruff/src/rules/flake8_django/rules/nullable_model_string_field.rs @@ -1,8 +1,8 @@ -use rustpython_parser::ast::Constant::Bool; use rustpython_parser::ast::{self, Expr, Ranged, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers::is_const_true; use crate::checkers::ast::Checker; @@ -51,24 +51,14 @@ impl Violation for DjangoNullableModelStringField { } } -const NOT_NULL_TRUE_FIELDS: [&str; 6] = [ - "CharField", - "TextField", - "SlugField", - "EmailField", - "FilePathField", - "URLField", -]; - /// DJ001 -pub(crate) fn nullable_model_string_field(checker: &Checker, body: &[Stmt]) -> Vec { - let mut errors = Vec::new(); - for statement in body.iter() { - let Stmt::Assign(ast::StmtAssign {value, ..}) = statement else { - continue +pub(crate) fn nullable_model_string_field(checker: &mut Checker, body: &[Stmt]) { + for statement in body { + let Stmt::Assign(ast::StmtAssign { value, .. }) = statement else { + continue; }; if let Some(field_name) = is_nullable_field(checker, value) { - errors.push(Diagnostic::new( + checker.diagnostics.push(Diagnostic::new( DjangoNullableModelStringField { field_name: field_name.to_string(), }, @@ -76,32 +66,34 @@ pub(crate) fn nullable_model_string_field(checker: &Checker, body: &[Stmt]) -> V )); } } - errors } fn is_nullable_field<'a>(checker: &'a Checker, value: &'a Expr) -> Option<&'a str> { - let Expr::Call(ast::ExprCall {func, keywords, ..}) = value else { + let Expr::Call(ast::ExprCall { func, keywords, .. }) = value else { return None; }; - let Some(valid_field_name) = helpers::get_model_field_name(checker.semantic_model(), func) else { + let Some(valid_field_name) = helpers::get_model_field_name(func, checker.semantic()) else { return None; }; - if !NOT_NULL_TRUE_FIELDS.contains(&valid_field_name) { + if !matches!( + valid_field_name, + "CharField" | "TextField" | "SlugField" | "EmailField" | "FilePathField" | "URLField" + ) { return None; } let mut null_key = false; let mut blank_key = false; let mut unique_key = false; - for keyword in keywords.iter() { - let Expr::Constant(ast::ExprConstant {value: Bool(true), ..}) = &keyword.value else { - continue - }; + for keyword in keywords { let Some(argument) = &keyword.arg else { - continue + continue; }; + if !is_const_true(&keyword.value) { + continue; + } match argument.as_str() { "blank" => blank_key = true, "null" => null_key = true, diff --git a/crates/ruff/src/rules/flake8_django/rules/unordered_body_content_in_model.rs b/crates/ruff/src/rules/flake8_django/rules/unordered_body_content_in_model.rs index 857ebc96ee..b46d825ca2 100644 --- a/crates/ruff/src/rules/flake8_django/rules/unordered_body_content_in_model.rs +++ b/crates/ruff/src/rules/flake8_django/rules/unordered_body_content_in_model.rs @@ -4,7 +4,7 @@ use rustpython_parser::ast::{self, Expr, Ranged, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; use crate::checkers::ast::Checker; @@ -63,20 +63,23 @@ use super::helpers; /// [Django Style Guide]: https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/coding-style/#model-style #[violation] pub struct DjangoUnorderedBodyContentInModel { - elem_type: ContentType, - before: ContentType, + element_type: ContentType, + prev_element_type: ContentType, } impl Violation for DjangoUnorderedBodyContentInModel { #[derive_message_formats] fn message(&self) -> String { - let DjangoUnorderedBodyContentInModel { elem_type, before } = self; - format!("Order of model's inner classes, methods, and fields does not follow the Django Style Guide: {elem_type} should come before {before}") + let DjangoUnorderedBodyContentInModel { + element_type, + prev_element_type, + } = self; + format!("Order of model's inner classes, methods, and fields does not follow the Django Style Guide: {element_type} should come before {prev_element_type}") } } #[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq)] -pub(crate) enum ContentType { +enum ContentType { FieldDeclaration, ManagerDeclaration, MetaClass, @@ -100,11 +103,11 @@ impl fmt::Display for ContentType { } } -fn get_element_type(model: &SemanticModel, element: &Stmt) -> Option { +fn get_element_type(element: &Stmt, semantic: &SemanticModel) -> Option { match element { Stmt::Assign(ast::StmtAssign { targets, value, .. }) => { if let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() { - if helpers::is_model_field(model, func) { + if helpers::is_model_field(func, semantic) { return Some(ContentType::FieldDeclaration); } } @@ -145,28 +148,42 @@ pub(crate) fn unordered_body_content_in_model( ) { if !bases .iter() - .any(|base| helpers::is_model(checker.semantic_model(), base)) + .any(|base| helpers::is_model(base, checker.semantic())) { return; } - let mut elements_type_found = Vec::new(); - for element in body.iter() { - let Some(current_element_type) = get_element_type(checker.semantic_model(), element) else { + + // Track all the element types we've seen so far. + let mut element_types = Vec::new(); + let mut prev_element_type = None; + for element in body { + let Some(element_type) = get_element_type(element, checker.semantic()) else { continue; }; - let Some(&element_type) = elements_type_found + + // Skip consecutive elements of the same type. It's less noisy to only report + // violations at type boundaries (e.g., avoid raising a violation for _every_ + // field declaration that's out of order). + if prev_element_type == Some(element_type) { + continue; + } + + prev_element_type = Some(element_type); + + if let Some(&prev_element_type) = element_types .iter() - .find(|&&element_type| element_type > current_element_type) else { - elements_type_found.push(current_element_type); - continue; - }; - let diagnostic = Diagnostic::new( - DjangoUnorderedBodyContentInModel { - elem_type: current_element_type, - before: element_type, - }, - element.range(), - ); - checker.diagnostics.push(diagnostic); + .find(|&&prev_element_type| prev_element_type > element_type) + { + let diagnostic = Diagnostic::new( + DjangoUnorderedBodyContentInModel { + element_type, + prev_element_type, + }, + element.range(), + ); + checker.diagnostics.push(diagnostic); + } else { + element_types.push(element_type); + } } } diff --git a/crates/ruff/src/rules/flake8_django/snapshots/ruff__rules__flake8_django__tests__DJ008_DJ008.py.snap b/crates/ruff/src/rules/flake8_django/snapshots/ruff__rules__flake8_django__tests__DJ008_DJ008.py.snap index 1af01e2856..2aae3d948b 100644 --- a/crates/ruff/src/rules/flake8_django/snapshots/ruff__rules__flake8_django__tests__DJ008_DJ008.py.snap +++ b/crates/ruff/src/rules/flake8_django/snapshots/ruff__rules__flake8_django__tests__DJ008_DJ008.py.snap @@ -1,58 +1,26 @@ --- source: crates/ruff/src/rules/flake8_django/mod.rs --- -DJ008.py:6:1: DJ008 Model does not define `__str__` method +DJ008.py:6:7: DJ008 Model does not define `__str__` method + | +5 | # Models without __str__ +6 | class TestModel1(models.Model): + | ^^^^^^^^^^ DJ008 +7 | new_field = models.CharField(max_length=10) + | + +DJ008.py:21:7: DJ008 Model does not define `__str__` method | - 5 | # Models without __str__ - 6 | / class TestModel1(models.Model): - 7 | | new_field = models.CharField(max_length=10) - 8 | | - 9 | | class Meta: -10 | | verbose_name = "test model" -11 | | verbose_name_plural = "test models" -12 | | -13 | | @property -14 | | def my_brand_new_property(self): -15 | | return 1 -16 | | -17 | | def my_beautiful_method(self): -18 | | return 2 - | |________________^ DJ008 +21 | class TestModel2(Model): + | ^^^^^^^^^^ DJ008 +22 | new_field = models.CharField(max_length=10) | -DJ008.py:21:1: DJ008 Model does not define `__str__` method +DJ008.py:36:7: DJ008 Model does not define `__str__` method | -21 | / class TestModel2(Model): -22 | | new_field = models.CharField(max_length=10) -23 | | -24 | | class Meta: -25 | | verbose_name = "test model" -26 | | verbose_name_plural = "test models" -27 | | -28 | | @property -29 | | def my_brand_new_property(self): -30 | | return 1 -31 | | -32 | | def my_beautiful_method(self): -33 | | return 2 - | |________________^ DJ008 - | - -DJ008.py:36:1: DJ008 Model does not define `__str__` method - | -36 | / class TestModel3(Model): -37 | | new_field = models.CharField(max_length=10) -38 | | -39 | | class Meta: -40 | | abstract = False -41 | | -42 | | @property -43 | | def my_brand_new_property(self): -44 | | return 1 -45 | | -46 | | def my_beautiful_method(self): -47 | | return 2 - | |________________^ DJ008 +36 | class TestModel3(Model): + | ^^^^^^^^^^ DJ008 +37 | new_field = models.CharField(max_length=10) | diff --git a/crates/ruff/src/rules/flake8_django/snapshots/ruff__rules__flake8_django__tests__DJ012_DJ012.py.snap b/crates/ruff/src/rules/flake8_django/snapshots/ruff__rules__flake8_django__tests__DJ012_DJ012.py.snap index 835df4524c..d187a99a41 100644 --- a/crates/ruff/src/rules/flake8_django/snapshots/ruff__rules__flake8_django__tests__DJ012_DJ012.py.snap +++ b/crates/ruff/src/rules/flake8_django/snapshots/ruff__rules__flake8_django__tests__DJ012_DJ012.py.snap @@ -37,4 +37,21 @@ DJ012.py:69:5: DJ012 Order of model's inner classes, methods, and fields does no | |____________^ DJ012 | +DJ012.py:123:5: DJ012 Order of model's inner classes, methods, and fields does not follow the Django Style Guide: field declaration should come before `Meta` class + | +121 | verbose_name = "test" +122 | +123 | first_name = models.CharField(max_length=32) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ DJ012 +124 | last_name = models.CharField(max_length=32) + | + +DJ012.py:129:5: DJ012 Order of model's inner classes, methods, and fields does not follow the Django Style Guide: field declaration should come before `Meta` class + | +127 | pass +128 | +129 | middle_name = models.CharField(max_length=32) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ DJ012 + | + diff --git a/crates/ruff/src/rules/flake8_errmsg/rules/mod.rs b/crates/ruff/src/rules/flake8_errmsg/rules/mod.rs index 8740e30113..9fa6564a86 100644 --- a/crates/ruff/src/rules/flake8_errmsg/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_errmsg/rules/mod.rs @@ -1,5 +1,3 @@ -pub(crate) use string_in_exception::{ - string_in_exception, DotFormatInException, FStringInException, RawStringInException, -}; +pub(crate) use string_in_exception::*; mod string_in_exception; diff --git a/crates/ruff/src/rules/flake8_errmsg/rules/string_in_exception.rs b/crates/ruff/src/rules/flake8_errmsg/rules/string_in_exception.rs index 9078a9dab7..7cf51caf11 100644 --- a/crates/ruff/src/rules/flake8_errmsg/rules/string_in_exception.rs +++ b/crates/ruff/src/rules/flake8_errmsg/rules/string_in_exception.rs @@ -172,6 +172,96 @@ impl Violation for DotFormatInException { } } +/// EM101, EM102, EM103 +pub(crate) fn string_in_exception(checker: &mut Checker, stmt: &Stmt, exc: &Expr) { + if let Expr::Call(ast::ExprCall { args, .. }) = exc { + if let Some(first) = args.first() { + match first { + // Check for string literals. + Expr::Constant(ast::ExprConstant { + value: Constant::Str(string), + .. + }) => { + if checker.enabled(Rule::RawStringInException) { + if string.len() >= checker.settings.flake8_errmsg.max_string_length { + let mut diagnostic = + Diagnostic::new(RawStringInException, first.range()); + if checker.patch(diagnostic.kind.rule()) { + if let Some(indentation) = + whitespace::indentation(checker.locator, stmt) + { + if checker.semantic().is_available("msg") { + diagnostic.set_fix(generate_fix( + stmt, + first, + indentation, + checker.stylist, + checker.generator(), + )); + } + } + } + checker.diagnostics.push(diagnostic); + } + } + } + // Check for f-strings. + Expr::JoinedStr(_) => { + if checker.enabled(Rule::FStringInException) { + let mut diagnostic = Diagnostic::new(FStringInException, first.range()); + if checker.patch(diagnostic.kind.rule()) { + if let Some(indentation) = + whitespace::indentation(checker.locator, stmt) + { + if checker.semantic().is_available("msg") { + diagnostic.set_fix(generate_fix( + stmt, + first, + indentation, + checker.stylist, + checker.generator(), + )); + } + } + } + checker.diagnostics.push(diagnostic); + } + } + // Check for .format() calls. + Expr::Call(ast::ExprCall { func, .. }) => { + if checker.enabled(Rule::DotFormatInException) { + if let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = + func.as_ref() + { + if attr == "format" && value.is_constant_expr() { + let mut diagnostic = + Diagnostic::new(DotFormatInException, first.range()); + if checker.patch(diagnostic.kind.rule()) { + if let Some(indentation) = + whitespace::indentation(checker.locator, stmt) + { + if checker.semantic().is_available("msg") { + diagnostic.set_fix(generate_fix( + stmt, + first, + indentation, + checker.stylist, + checker.generator(), + )); + } + } + } + checker.diagnostics.push(diagnostic); + } + } + } + } + _ => {} + } + } + } +} + /// Generate the [`Fix`] for EM001, EM002, and EM003 violations. /// /// This assumes that the violation is fixable and that the patch should @@ -189,24 +279,22 @@ fn generate_fix( stylist: &Stylist, generator: Generator, ) -> Fix { - let node = Expr::Name(ast::ExprName { - id: "msg".into(), - ctx: ExprContext::Store, - range: TextRange::default(), - }); - let node1 = Stmt::Assign(ast::StmtAssign { - targets: vec![node], + let assignment = Stmt::Assign(ast::StmtAssign { + targets: vec![Expr::Name(ast::ExprName { + id: "msg".into(), + ctx: ExprContext::Store, + range: TextRange::default(), + })], value: Box::new(exc_arg.clone()), type_comment: None, range: TextRange::default(), }); - let assignment = generator.stmt(&node1); - #[allow(deprecated)] - Fix::unspecified_edits( + + Fix::suggested_edits( Edit::insertion( format!( "{}{}{}", - assignment, + generator.stmt(&assignment), stylist.line_ending().as_str(), indentation, ), @@ -218,106 +306,3 @@ fn generate_fix( )], ) } - -/// EM101, EM102, EM103 -pub(crate) fn string_in_exception(checker: &mut Checker, stmt: &Stmt, exc: &Expr) { - if let Expr::Call(ast::ExprCall { args, .. }) = exc { - if let Some(first) = args.first() { - match first { - // Check for string literals. - Expr::Constant(ast::ExprConstant { - value: Constant::Str(string), - .. - }) => { - if checker.enabled(Rule::RawStringInException) { - if string.len() >= checker.settings.flake8_errmsg.max_string_length { - let indentation = whitespace::indentation(checker.locator, stmt) - .and_then(|indentation| { - if checker.semantic_model().find_binding("msg").is_none() { - Some(indentation) - } else { - None - } - }); - let mut diagnostic = - Diagnostic::new(RawStringInException, first.range()); - if let Some(indentation) = indentation { - if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(generate_fix( - stmt, - first, - indentation, - checker.stylist, - checker.generator(), - )); - } - } - checker.diagnostics.push(diagnostic); - } - } - } - // Check for f-strings. - Expr::JoinedStr(_) => { - if checker.enabled(Rule::FStringInException) { - let indentation = whitespace::indentation(checker.locator, stmt).and_then( - |indentation| { - if checker.semantic_model().find_binding("msg").is_none() { - Some(indentation) - } else { - None - } - }, - ); - let mut diagnostic = Diagnostic::new(FStringInException, first.range()); - if let Some(indentation) = indentation { - if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(generate_fix( - stmt, - first, - indentation, - checker.stylist, - checker.generator(), - )); - } - } - checker.diagnostics.push(diagnostic); - } - } - // Check for .format() calls. - Expr::Call(ast::ExprCall { func, .. }) => { - if checker.enabled(Rule::DotFormatInException) { - if let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = - func.as_ref() - { - if attr == "format" && value.is_constant_expr() { - let indentation = whitespace::indentation(checker.locator, stmt) - .and_then(|indentation| { - if checker.semantic_model().find_binding("msg").is_none() { - Some(indentation) - } else { - None - } - }); - let mut diagnostic = - Diagnostic::new(DotFormatInException, first.range()); - if let Some(indentation) = indentation { - if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(generate_fix( - stmt, - first, - indentation, - checker.stylist, - checker.generator(), - )); - } - } - checker.diagnostics.push(diagnostic); - } - } - } - } - _ => {} - } - } - } -} diff --git a/crates/ruff/src/rules/flake8_executable/helpers.rs b/crates/ruff/src/rules/flake8_executable/helpers.rs index f52e746bd0..be200dfb2c 100644 --- a/crates/ruff/src/rules/flake8_executable/helpers.rs +++ b/crates/ruff/src/rules/flake8_executable/helpers.rs @@ -1,88 +1,12 @@ -#[cfg(target_family = "unix")] +#![cfg(target_family = "unix")] + use std::os::unix::fs::PermissionsExt; -#[cfg(target_family = "unix")] use std::path::Path; -#[cfg(target_family = "unix")] use anyhow::Result; -use once_cell::sync::Lazy; -use regex::Regex; -use ruff_text_size::{TextLen, TextSize}; -static SHEBANG_REGEX: Lazy = - Lazy::new(|| Regex::new(r"^(?P\s*)#!(?P.*)").unwrap()); - -#[derive(Debug, PartialEq, Eq)] -pub(crate) enum ShebangDirective<'a> { - None, - // whitespace length, start of the shebang, contents - Match(TextSize, TextSize, &'a str), -} - -pub(crate) fn extract_shebang(line: &str) -> ShebangDirective { - // Minor optimization to avoid matches in the common case. - if !line.contains('!') { - return ShebangDirective::None; - } - match SHEBANG_REGEX.captures(line) { - Some(caps) => match caps.name("spaces") { - Some(spaces) => match caps.name("directive") { - Some(matches) => ShebangDirective::Match( - spaces.as_str().text_len(), - TextSize::try_from(matches.start()).unwrap(), - matches.as_str(), - ), - None => ShebangDirective::None, - }, - None => ShebangDirective::None, - }, - None => ShebangDirective::None, - } -} - -#[cfg(target_family = "unix")] -pub(crate) fn is_executable(filepath: &Path) -> Result { - { - let metadata = filepath.metadata()?; - let permissions = metadata.permissions(); - Ok(permissions.mode() & 0o111 != 0) - } -} - -#[cfg(test)] -mod tests { - use ruff_text_size::TextSize; - - use crate::rules::flake8_executable::helpers::{ - extract_shebang, ShebangDirective, SHEBANG_REGEX, - }; - - #[test] - fn shebang_regex() { - // Positive cases - assert!(SHEBANG_REGEX.is_match("#!/usr/bin/python")); - assert!(SHEBANG_REGEX.is_match("#!/usr/bin/env python")); - assert!(SHEBANG_REGEX.is_match(" #!/usr/bin/env python")); - assert!(SHEBANG_REGEX.is_match(" #!/usr/bin/env python")); - - // Negative cases - assert!(!SHEBANG_REGEX.is_match("hello world")); - } - - #[test] - fn shebang_extract_match() { - assert_eq!(extract_shebang("not a match"), ShebangDirective::None); - assert_eq!( - extract_shebang("#!/usr/bin/env python"), - ShebangDirective::Match(TextSize::from(0), TextSize::from(2), "/usr/bin/env python") - ); - assert_eq!( - extract_shebang(" #!/usr/bin/env python"), - ShebangDirective::Match(TextSize::from(2), TextSize::from(4), "/usr/bin/env python") - ); - assert_eq!( - extract_shebang("print('test') #!/usr/bin/python"), - ShebangDirective::None - ); - } +pub(super) fn is_executable(filepath: &Path) -> Result { + let metadata = filepath.metadata()?; + let permissions = metadata.permissions(); + Ok(permissions.mode() & 0o111 != 0) } diff --git a/crates/ruff/src/rules/flake8_executable/rules/mod.rs b/crates/ruff/src/rules/flake8_executable/rules/mod.rs index 4e300fae44..35b1aa269e 100644 --- a/crates/ruff/src/rules/flake8_executable/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_executable/rules/mod.rs @@ -1,8 +1,8 @@ -pub(crate) use shebang_missing::{shebang_missing, ShebangMissingExecutableFile}; -pub(crate) use shebang_newline::{shebang_newline, ShebangNotFirstLine}; -pub(crate) use shebang_not_executable::{shebang_not_executable, ShebangNotExecutable}; -pub(crate) use shebang_python::{shebang_python, ShebangMissingPython}; -pub(crate) use shebang_whitespace::{shebang_whitespace, ShebangLeadingWhitespace}; +pub(crate) use shebang_missing::*; +pub(crate) use shebang_newline::*; +pub(crate) use shebang_not_executable::*; +pub(crate) use shebang_python::*; +pub(crate) use shebang_whitespace::*; mod shebang_missing; mod shebang_newline; diff --git a/crates/ruff/src/rules/flake8_executable/rules/shebang_missing.rs b/crates/ruff/src/rules/flake8_executable/rules/shebang_missing.rs index ea33e57d0c..2f1e47f015 100644 --- a/crates/ruff/src/rules/flake8_executable/rules/shebang_missing.rs +++ b/crates/ruff/src/rules/flake8_executable/rules/shebang_missing.rs @@ -2,6 +2,8 @@ use std::path::Path; +use wsl; + use ruff_text_size::TextRange; use ruff_diagnostics::{Diagnostic, Violation}; @@ -11,6 +13,24 @@ use crate::registry::AsRule; #[cfg(target_family = "unix")] use crate::rules::flake8_executable::helpers::is_executable; +/// ## What it does +/// Checks for executable `.py` files that do not have a shebang. +/// +/// ## Why is this bad? +/// In Python, a shebang (also known as a hashbang) is the first line of a +/// script, which specifies the interpreter that should be used to run the +/// script. +/// +/// If a `.py` file is executable, but does not have a shebang, it may be run +/// with the wrong interpreter, or fail to run at all. +/// +/// If the file is meant to be executable, add a shebang; otherwise, remove the +/// executable bit from the file. +/// +/// _This rule is only available on Unix-like systems._ +/// +/// ## References +/// - [Python documentation: Executable Python Scripts](https://docs.python.org/3/tutorial/appendix.html#executable-python-scripts) #[violation] pub struct ShebangMissingExecutableFile; @@ -24,6 +44,11 @@ impl Violation for ShebangMissingExecutableFile { /// EXE002 #[cfg(target_family = "unix")] pub(crate) fn shebang_missing(filepath: &Path) -> Option { + // WSL supports Windows file systems, which do not have executable bits. + // Instead, everything is executable. Therefore, we skip this rule on WSL. + if wsl::is_wsl() { + return None; + } if let Ok(true) = is_executable(filepath) { let diagnostic = Diagnostic::new(ShebangMissingExecutableFile, TextRange::default()); return Some(diagnostic); diff --git a/crates/ruff/src/rules/flake8_executable/rules/shebang_newline.rs b/crates/ruff/src/rules/flake8_executable/rules/shebang_newline.rs index 9c605ba940..626e29020a 100644 --- a/crates/ruff/src/rules/flake8_executable/rules/shebang_newline.rs +++ b/crates/ruff/src/rules/flake8_executable/rules/shebang_newline.rs @@ -3,8 +3,34 @@ use ruff_text_size::{TextLen, TextRange}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use crate::rules::flake8_executable::helpers::ShebangDirective; +use crate::comments::shebang::ShebangDirective; +/// ## What it does +/// Checks for a shebang directive that is not at the beginning of the file. +/// +/// ## Why is this bad? +/// In Python, a shebang (also known as a hashbang) is the first line of a +/// script, which specifies the interpreter that should be used to run the +/// script. +/// +/// The shebang's `#!` prefix must be the first two characters of a file. If +/// the shebang is not at the beginning of the file, it will be ignored, which +/// is likely a mistake. +/// +/// ## Example +/// ```python +/// foo = 1 +/// #!/usr/bin/env python3 +/// ``` +/// +/// Use instead: +/// ```python +/// #!/usr/bin/env python3 +/// foo = 1 +/// ``` +/// +/// ## References +/// - [Python documentation: Executable Python Scripts](https://docs.python.org/3/tutorial/appendix.html#executable-python-scripts) #[violation] pub struct ShebangNotFirstLine; @@ -21,17 +47,14 @@ pub(crate) fn shebang_newline( shebang: &ShebangDirective, first_line: bool, ) -> Option { - if let ShebangDirective::Match(_, start, content) = shebang { - if first_line { - None - } else { - let diagnostic = Diagnostic::new( - ShebangNotFirstLine, - TextRange::at(range.start() + start, content.text_len()), - ); - Some(diagnostic) - } - } else { + let ShebangDirective { offset, contents } = shebang; + + if first_line { None + } else { + Some(Diagnostic::new( + ShebangNotFirstLine, + TextRange::at(range.start() + offset, contents.text_len()), + )) } } diff --git a/crates/ruff/src/rules/flake8_executable/rules/shebang_not_executable.rs b/crates/ruff/src/rules/flake8_executable/rules/shebang_not_executable.rs index 6cfcc13887..a1400f0b67 100644 --- a/crates/ruff/src/rules/flake8_executable/rules/shebang_not_executable.rs +++ b/crates/ruff/src/rules/flake8_executable/rules/shebang_not_executable.rs @@ -2,16 +2,37 @@ use std::path::Path; +use wsl; + use ruff_text_size::{TextLen, TextRange, TextSize}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +use crate::comments::shebang::ShebangDirective; use crate::registry::AsRule; #[cfg(target_family = "unix")] use crate::rules::flake8_executable::helpers::is_executable; -use crate::rules::flake8_executable::helpers::ShebangDirective; +/// ## What it does +/// Checks for a shebang directive in a file that is not executable. +/// +/// ## Why is this bad? +/// In Python, a shebang (also known as a hashbang) is the first line of a +/// script, which specifies the interpreter that should be used to run the +/// script. +/// +/// The presence of a shebang suggests that a file is intended to be +/// executable. If a file contains a shebang but is not executable, then the +/// shebang is misleading, or the file is missing the executable bit. +/// +/// If the file is meant to be executable, add a shebang; otherwise, remove the +/// executable bit from the file. +/// +/// _This rule is only available on Unix-like systems._ +/// +/// ## References +/// - [Python documentation: Executable Python Scripts](https://docs.python.org/3/tutorial/appendix.html#executable-python-scripts) #[violation] pub struct ShebangNotExecutable; @@ -29,15 +50,21 @@ pub(crate) fn shebang_not_executable( range: TextRange, shebang: &ShebangDirective, ) -> Option { - if let ShebangDirective::Match(_, start, content) = shebang { - if let Ok(false) = is_executable(filepath) { - let diagnostic = Diagnostic::new( - ShebangNotExecutable, - TextRange::at(range.start() + start, content.text_len()), - ); - return Some(diagnostic); - } + // WSL supports Windows file systems, which do not have executable bits. + // Instead, everything is executable. Therefore, we skip this rule on WSL. + if wsl::is_wsl() { + return None; } + let ShebangDirective { offset, contents } = shebang; + + if let Ok(false) = is_executable(filepath) { + let diagnostic = Diagnostic::new( + ShebangNotExecutable, + TextRange::at(range.start() + offset, contents.text_len()), + ); + return Some(diagnostic); + } + None } diff --git a/crates/ruff/src/rules/flake8_executable/rules/shebang_python.rs b/crates/ruff/src/rules/flake8_executable/rules/shebang_python.rs index 73f3d1f333..832533d8ca 100644 --- a/crates/ruff/src/rules/flake8_executable/rules/shebang_python.rs +++ b/crates/ruff/src/rules/flake8_executable/rules/shebang_python.rs @@ -3,8 +3,33 @@ use ruff_text_size::{TextLen, TextRange, TextSize}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use crate::rules::flake8_executable::helpers::ShebangDirective; +use crate::comments::shebang::ShebangDirective; +/// ## What it does +/// Checks for a shebang directive in `.py` files that does not contain `python`. +/// +/// ## Why is this bad? +/// In Python, a shebang (also known as a hashbang) is the first line of a +/// script, which specifies the interpreter that should be used to run the +/// script. +/// +/// For Python scripts, the shebang must contain `python` to indicate that the +/// script should be executed as a Python script. If the shebang does not +/// contain `python`, then the file will be executed with the default +/// interpreter, which is likely a mistake. +/// +/// ## Example +/// ```python +/// #!/usr/bin/env bash +/// ``` +/// +/// Use instead: +/// ```python +/// #!/usr/bin/env python3 +/// ``` +/// +/// ## References +/// - [Python documentation: Executable Python Scripts](https://docs.python.org/3/tutorial/appendix.html#executable-python-scripts) #[violation] pub struct ShebangMissingPython; @@ -17,19 +42,14 @@ impl Violation for ShebangMissingPython { /// EXE003 pub(crate) fn shebang_python(range: TextRange, shebang: &ShebangDirective) -> Option { - if let ShebangDirective::Match(_, start, content) = shebang { - if content.contains("python") || content.contains("pytest") { - None - } else { - let diagnostic = Diagnostic::new( - ShebangMissingPython, - TextRange::at(range.start() + start, content.text_len()) - .sub_start(TextSize::from(2)), - ); + let ShebangDirective { offset, contents } = shebang; - Some(diagnostic) - } - } else { + if contents.contains("python") || contents.contains("pytest") { None + } else { + Some(Diagnostic::new( + ShebangMissingPython, + TextRange::at(range.start() + offset, contents.text_len()).sub_start(TextSize::from(2)), + )) } } diff --git a/crates/ruff/src/rules/flake8_executable/rules/shebang_whitespace.rs b/crates/ruff/src/rules/flake8_executable/rules/shebang_whitespace.rs index 0458d61432..3b8c990897 100644 --- a/crates/ruff/src/rules/flake8_executable/rules/shebang_whitespace.rs +++ b/crates/ruff/src/rules/flake8_executable/rules/shebang_whitespace.rs @@ -1,10 +1,36 @@ +use std::ops::Sub; + use ruff_text_size::{TextRange, TextSize}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; -use crate::rules::flake8_executable::helpers::ShebangDirective; +use crate::comments::shebang::ShebangDirective; +/// ## What it does +/// Checks for whitespace before a shebang directive. +/// +/// ## Why is this bad? +/// In Python, a shebang (also known as a hashbang) is the first line of a +/// script, which specifies the interpreter that should be used to run the +/// script. +/// +/// The shebang's `#!` prefix must be the first two characters of a file. The +/// presence of whitespace before the shebang will cause the shebang to be +/// ignored, which is likely a mistake. +/// +/// ## Example +/// ```python +/// #!/usr/bin/env python3 +/// ``` +/// +/// Use instead: +/// ```python +/// #!/usr/bin/env python3 +/// ``` +/// +/// ## References +/// - [Python documentation: Executable Python Scripts](https://docs.python.org/3/tutorial/appendix.html#executable-python-scripts) #[violation] pub struct ShebangLeadingWhitespace; @@ -25,23 +51,25 @@ pub(crate) fn shebang_whitespace( shebang: &ShebangDirective, autofix: bool, ) -> Option { - if let ShebangDirective::Match(n_spaces, start, ..) = shebang { - if *n_spaces > TextSize::from(0) && *start == n_spaces + TextSize::from(2) { - let mut diagnostic = Diagnostic::new( - ShebangLeadingWhitespace, - TextRange::at(range.start(), *n_spaces), - ); - if autofix { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_deletion(TextRange::at( - range.start(), - *n_spaces, - )))); - } - Some(diagnostic) - } else { - None + let ShebangDirective { + offset, + contents: _, + } = shebang; + + if *offset > TextSize::from(2) { + let leading_space_start = range.start(); + let leading_space_len = offset.sub(TextSize::new(2)); + let mut diagnostic = Diagnostic::new( + ShebangLeadingWhitespace, + TextRange::at(leading_space_start, leading_space_len), + ); + if autofix { + diagnostic.set_fix(Fix::automatic(Edit::range_deletion(TextRange::at( + leading_space_start, + leading_space_len, + )))); } + Some(diagnostic) } else { None } diff --git a/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE004_1.py.snap b/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE004_1.py.snap index 3a7d2f6f30..1cda8e5af0 100644 --- a/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE004_1.py.snap +++ b/crates/ruff/src/rules/flake8_executable/snapshots/ruff__rules__flake8_executable__tests__EXE004_1.py.snap @@ -8,7 +8,7 @@ EXE004_1.py:1:1: EXE004 [*] Avoid whitespace before shebang | = help: Remove whitespace before shebang -ℹ Suggested fix +ℹ Fix 1 |- #!/usr/bin/python 1 |+#!/usr/bin/python diff --git a/crates/ruff/src/rules/flake8_fixme/rules/mod.rs b/crates/ruff/src/rules/flake8_fixme/rules/mod.rs index 92e8eae87d..9e5980aeb9 100644 --- a/crates/ruff/src/rules/flake8_fixme/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_fixme/rules/mod.rs @@ -1,5 +1,3 @@ -pub(crate) use todos::{ - todos, LineContainsFixme, LineContainsHack, LineContainsTodo, LineContainsXxx, -}; +pub(crate) use todos::*; mod todos; diff --git a/crates/ruff/src/rules/flake8_fixme/rules/todos.rs b/crates/ruff/src/rules/flake8_fixme/rules/todos.rs index 495fccc030..d43d20b623 100644 --- a/crates/ruff/src/rules/flake8_fixme/rules/todos.rs +++ b/crates/ruff/src/rules/flake8_fixme/rules/todos.rs @@ -39,18 +39,19 @@ impl Violation for LineContainsHack { } } -pub(crate) fn todos(directive_ranges: &[TodoComment]) -> Vec { - directive_ranges - .iter() - .map(|TodoComment { directive, .. }| match directive.kind { - // FIX001 - TodoDirectiveKind::Fixme => Diagnostic::new(LineContainsFixme, directive.range), - // FIX002 - TodoDirectiveKind::Hack => Diagnostic::new(LineContainsHack, directive.range), - // FIX003 - TodoDirectiveKind::Todo => Diagnostic::new(LineContainsTodo, directive.range), - // FIX004 - TodoDirectiveKind::Xxx => Diagnostic::new(LineContainsXxx, directive.range), - }) - .collect::>() +pub(crate) fn todos(diagnostics: &mut Vec, directive_ranges: &[TodoComment]) { + diagnostics.extend( + directive_ranges + .iter() + .map(|TodoComment { directive, .. }| match directive.kind { + // FIX001 + TodoDirectiveKind::Fixme => Diagnostic::new(LineContainsFixme, directive.range), + // FIX002 + TodoDirectiveKind::Hack => Diagnostic::new(LineContainsHack, directive.range), + // FIX003 + TodoDirectiveKind::Todo => Diagnostic::new(LineContainsTodo, directive.range), + // FIX004 + TodoDirectiveKind::Xxx => Diagnostic::new(LineContainsXxx, directive.range), + }), + ); } diff --git a/crates/ruff/src/rules/flake8_future_annotations/rules/future_rewritable_type_annotation.rs b/crates/ruff/src/rules/flake8_future_annotations/rules/future_rewritable_type_annotation.rs index 1594938fec..33e604c821 100644 --- a/crates/ruff/src/rules/flake8_future_annotations/rules/future_rewritable_type_annotation.rs +++ b/crates/ruff/src/rules/flake8_future_annotations/rules/future_rewritable_type_annotation.rs @@ -49,6 +49,9 @@ use crate::checkers::ast::Checker; /// def func(obj: dict[str, int | None]) -> None: /// ... /// ``` +/// +/// ## Options +/// - `target-version` #[violation] pub struct FutureRewritableTypeAnnotation { name: String, @@ -65,7 +68,7 @@ impl Violation for FutureRewritableTypeAnnotation { /// FA100 pub(crate) fn future_rewritable_type_annotation(checker: &mut Checker, expr: &Expr) { let name = checker - .semantic_model() + .semantic() .resolve_call_path(expr) .map(|binding| format_call_path(&binding)); diff --git a/crates/ruff/src/rules/flake8_future_annotations/rules/mod.rs b/crates/ruff/src/rules/flake8_future_annotations/rules/mod.rs index e5e674a5c8..3af2f506fd 100644 --- a/crates/ruff/src/rules/flake8_future_annotations/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_future_annotations/rules/mod.rs @@ -1,9 +1,5 @@ -pub(crate) use future_required_type_annotation::{ - future_required_type_annotation, FutureRequiredTypeAnnotation, Reason, -}; -pub(crate) use future_rewritable_type_annotation::{ - future_rewritable_type_annotation, FutureRewritableTypeAnnotation, -}; +pub(crate) use future_required_type_annotation::*; +pub(crate) use future_rewritable_type_annotation::*; mod future_required_type_annotation; mod future_rewritable_type_annotation; diff --git a/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__edge_case.py.snap b/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__edge_case.py.snap index fc0cdbf8ad..ed1dc0f3df 100644 --- a/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__edge_case.py.snap +++ b/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__edge_case.py.snap @@ -1,6 +1,14 @@ --- source: crates/ruff/src/rules/flake8_future_annotations/mod.rs --- +edge_case.py:5:13: FA100 Missing `from __future__ import annotations`, but uses `typing.List` + | +5 | def main(_: List[int]) -> None: + | ^^^^ FA100 +6 | a_list: t.List[str] = [] +7 | a_list.append("hello") + | + edge_case.py:6:13: FA100 Missing `from __future__ import annotations`, but uses `typing.List` | 5 | def main(_: List[int]) -> None: diff --git a/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__fa102_no_future_import_uses_lowercase.py.snap b/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__fa102_no_future_import_uses_lowercase.py.snap index 5fb0fe7444..14acba04f7 100644 --- a/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__fa102_no_future_import_uses_lowercase.py.snap +++ b/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__fa102_no_future_import_uses_lowercase.py.snap @@ -9,4 +9,11 @@ no_future_import_uses_lowercase.py:2:13: FA102 Missing `from __future__ import a 3 | a_list.append("hello") | +no_future_import_uses_lowercase.py:6:14: FA102 Missing `from __future__ import annotations`, but uses PEP 585 collection + | +6 | def hello(y: dict[str, int]) -> None: + | ^^^^^^^^^^^^^^ FA102 +7 | del y + | + diff --git a/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__fa102_no_future_import_uses_union.py.snap b/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__fa102_no_future_import_uses_union.py.snap index cee83994c0..6bf73cdcf1 100644 --- a/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__fa102_no_future_import_uses_union.py.snap +++ b/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__fa102_no_future_import_uses_union.py.snap @@ -17,4 +17,18 @@ no_future_import_uses_union.py:2:13: FA102 Missing `from __future__ import annot 3 | a_list.append("hello") | +no_future_import_uses_union.py:6:14: FA102 Missing `from __future__ import annotations`, but uses PEP 604 union + | +6 | def hello(y: dict[str, int] | None) -> None: + | ^^^^^^^^^^^^^^^^^^^^^ FA102 +7 | del y + | + +no_future_import_uses_union.py:6:14: FA102 Missing `from __future__ import annotations`, but uses PEP 585 collection + | +6 | def hello(y: dict[str, int] | None) -> None: + | ^^^^^^^^^^^^^^ FA102 +7 | del y + | + diff --git a/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__fa102_no_future_import_uses_union_inner.py.snap b/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__fa102_no_future_import_uses_union_inner.py.snap index d153861855..7f5156463a 100644 --- a/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__fa102_no_future_import_uses_union_inner.py.snap +++ b/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__fa102_no_future_import_uses_union_inner.py.snap @@ -17,6 +17,22 @@ no_future_import_uses_union_inner.py:2:18: FA102 Missing `from __future__ import 3 | a_list.append("hello") | +no_future_import_uses_union_inner.py:6:14: FA102 Missing `from __future__ import annotations`, but uses PEP 585 collection + | +6 | def hello(y: dict[str | None, int]) -> None: + | ^^^^^^^^^^^^^^^^^^^^^ FA102 +7 | z: tuple[str, str | None, str] = tuple(y) +8 | del z + | + +no_future_import_uses_union_inner.py:6:19: FA102 Missing `from __future__ import annotations`, but uses PEP 604 union + | +6 | def hello(y: dict[str | None, int]) -> None: + | ^^^^^^^^^^ FA102 +7 | z: tuple[str, str | None, str] = tuple(y) +8 | del z + | + no_future_import_uses_union_inner.py:7:8: FA102 Missing `from __future__ import annotations`, but uses PEP 585 collection | 6 | def hello(y: dict[str | None, int]) -> None: diff --git a/crates/ruff/src/rules/flake8_gettext/rules/f_string_in_gettext_func_call.rs b/crates/ruff/src/rules/flake8_gettext/rules/f_string_in_gettext_func_call.rs index 0f6b8c6a60..89957a02ac 100644 --- a/crates/ruff/src/rules/flake8_gettext/rules/f_string_in_gettext_func_call.rs +++ b/crates/ruff/src/rules/flake8_gettext/rules/f_string_in_gettext_func_call.rs @@ -3,6 +3,8 @@ use rustpython_parser::ast::{Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +use crate::checkers::ast::Checker; + #[violation] pub struct FStringInGetTextFuncCall; @@ -14,11 +16,12 @@ impl Violation for FStringInGetTextFuncCall { } /// INT001 -pub(crate) fn f_string_in_gettext_func_call(args: &[Expr]) -> Option { +pub(crate) fn f_string_in_gettext_func_call(checker: &mut Checker, args: &[Expr]) { if let Some(first) = args.first() { if first.is_joined_str_expr() { - return Some(Diagnostic::new(FStringInGetTextFuncCall {}, first.range())); + checker + .diagnostics + .push(Diagnostic::new(FStringInGetTextFuncCall {}, first.range())); } } - None } diff --git a/crates/ruff/src/rules/flake8_gettext/rules/format_in_gettext_func_call.rs b/crates/ruff/src/rules/flake8_gettext/rules/format_in_gettext_func_call.rs index ec159d0337..2f99369f25 100644 --- a/crates/ruff/src/rules/flake8_gettext/rules/format_in_gettext_func_call.rs +++ b/crates/ruff/src/rules/flake8_gettext/rules/format_in_gettext_func_call.rs @@ -3,6 +3,8 @@ use rustpython_parser::ast::{self, Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +use crate::checkers::ast::Checker; + #[violation] pub struct FormatInGetTextFuncCall; @@ -14,15 +16,16 @@ impl Violation for FormatInGetTextFuncCall { } /// INT002 -pub(crate) fn format_in_gettext_func_call(args: &[Expr]) -> Option { +pub(crate) fn format_in_gettext_func_call(checker: &mut Checker, args: &[Expr]) { if let Some(first) = args.first() { if let Expr::Call(ast::ExprCall { func, .. }) = &first { if let Expr::Attribute(ast::ExprAttribute { attr, .. }) = func.as_ref() { if attr == "format" { - return Some(Diagnostic::new(FormatInGetTextFuncCall {}, first.range())); + checker + .diagnostics + .push(Diagnostic::new(FormatInGetTextFuncCall {}, first.range())); } } } } - None } diff --git a/crates/ruff/src/rules/flake8_gettext/rules/is_gettext_func_call.rs b/crates/ruff/src/rules/flake8_gettext/rules/is_gettext_func_call.rs index 0e539ead9a..e2f78c4476 100644 --- a/crates/ruff/src/rules/flake8_gettext/rules/is_gettext_func_call.rs +++ b/crates/ruff/src/rules/flake8_gettext/rules/is_gettext_func_call.rs @@ -3,7 +3,7 @@ use rustpython_parser::ast::{self, Expr}; /// Returns true if the [`Expr`] is an internationalization function call. pub(crate) fn is_gettext_func_call(func: &Expr, functions_names: &[String]) -> bool { if let Expr::Name(ast::ExprName { id, .. }) = func { - functions_names.contains(id.as_ref()) + functions_names.contains(id) } else { false } diff --git a/crates/ruff/src/rules/flake8_gettext/rules/mod.rs b/crates/ruff/src/rules/flake8_gettext/rules/mod.rs index 1a80938fd1..490011e5fb 100644 --- a/crates/ruff/src/rules/flake8_gettext/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_gettext/rules/mod.rs @@ -1,13 +1,7 @@ -pub(crate) use f_string_in_gettext_func_call::{ - f_string_in_gettext_func_call, FStringInGetTextFuncCall, -}; -pub(crate) use format_in_gettext_func_call::{ - format_in_gettext_func_call, FormatInGetTextFuncCall, -}; -pub(crate) use is_gettext_func_call::is_gettext_func_call; -pub(crate) use printf_in_gettext_func_call::{ - printf_in_gettext_func_call, PrintfInGetTextFuncCall, -}; +pub(crate) use f_string_in_gettext_func_call::*; +pub(crate) use format_in_gettext_func_call::*; +pub(crate) use is_gettext_func_call::*; +pub(crate) use printf_in_gettext_func_call::*; mod f_string_in_gettext_func_call; mod format_in_gettext_func_call; diff --git a/crates/ruff/src/rules/flake8_gettext/rules/printf_in_gettext_func_call.rs b/crates/ruff/src/rules/flake8_gettext/rules/printf_in_gettext_func_call.rs index 088eaa60f8..eab5d74c93 100644 --- a/crates/ruff/src/rules/flake8_gettext/rules/printf_in_gettext_func_call.rs +++ b/crates/ruff/src/rules/flake8_gettext/rules/printf_in_gettext_func_call.rs @@ -1,5 +1,6 @@ use rustpython_parser::ast::{self, Constant, Expr, Operator, Ranged}; +use crate::checkers::ast::Checker; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -14,7 +15,7 @@ impl Violation for PrintfInGetTextFuncCall { } /// INT003 -pub(crate) fn printf_in_gettext_func_call(args: &[Expr]) -> Option { +pub(crate) fn printf_in_gettext_func_call(checker: &mut Checker, args: &[Expr]) { if let Some(first) = args.first() { if let Expr::BinOp(ast::ExprBinOp { op: Operator::Mod { .. }, @@ -27,9 +28,10 @@ pub(crate) fn printf_in_gettext_func_call(args: &[Expr]) -> Option { .. }) = left.as_ref() { - return Some(Diagnostic::new(PrintfInGetTextFuncCall {}, first.range())); + checker + .diagnostics + .push(Diagnostic::new(PrintfInGetTextFuncCall {}, first.range())); } } } - None } diff --git a/crates/ruff/src/rules/flake8_gettext/settings.rs b/crates/ruff/src/rules/flake8_gettext/settings.rs index a16d61e15c..a845688dde 100644 --- a/crates/ruff/src/rules/flake8_gettext/settings.rs +++ b/crates/ruff/src/rules/flake8_gettext/settings.rs @@ -57,12 +57,7 @@ impl From for Settings { .function_names .unwrap_or_else(default_func_names) .into_iter() - .chain( - options - .extend_function_names - .unwrap_or_default() - .into_iter(), - ) + .chain(options.extend_function_names.unwrap_or_default()) .collect(), } } diff --git a/crates/ruff/src/rules/flake8_implicit_str_concat/rules/implicit.rs b/crates/ruff/src/rules/flake8_implicit_str_concat/rules/implicit.rs index 43dac7b3e5..227d889d02 100644 --- a/crates/ruff/src/rules/flake8_implicit_str_concat/rules/implicit.rs +++ b/crates/ruff/src/rules/flake8_implicit_str_concat/rules/implicit.rs @@ -1,11 +1,11 @@ use itertools::Itertools; use ruff_text_size::TextRange; use rustpython_parser::lexer::LexResult; -use rustpython_parser::Tok; -use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::source_code::Locator; +use ruff_python_ast::str::{leading_quote, trailing_quote}; use crate::rules::flake8_implicit_str_concat::settings::Settings; @@ -34,17 +34,23 @@ use crate::rules::flake8_implicit_str_concat::settings::Settings; pub struct SingleLineImplicitStringConcatenation; impl Violation for SingleLineImplicitStringConcatenation { + const AUTOFIX: AutofixKind = AutofixKind::Sometimes; + #[derive_message_formats] fn message(&self) -> String { format!("Implicitly concatenated string literals on one line") } + + fn autofix_title(&self) -> Option { + Some("Combine string literals".to_string()) + } } /// ## What it does /// Checks for implicitly concatenated strings that span multiple lines. /// /// ## Why is this bad? -/// For string literals that wrap across multiple lines, PEP 8 recommends +/// For string literals that wrap across multiple lines, [PEP 8] recommends /// the use of implicit string concatenation within parentheses instead of /// using a backslash for line continuation, as the former is more readable /// than the latter. @@ -54,9 +60,6 @@ impl Violation for SingleLineImplicitStringConcatenation { /// altogether, set the `flake8-implicit-str-concat.allow-multiline` option /// to `false`. /// -/// ## Options -/// - `flake8-implicit-str-concat.allow-multiline` -/// /// ## Example /// ```python /// z = "The quick brown fox jumps over the lazy "\ @@ -71,8 +74,10 @@ impl Violation for SingleLineImplicitStringConcatenation { /// ) /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#maximum-line-length) +/// ## Options +/// - `flake8-implicit-str-concat.allow-multiline` +/// +/// [PEP 8]: https://peps.python.org/pep-0008/#maximum-line-length #[violation] pub struct MultiLineImplicitStringConcatenation; @@ -85,33 +90,69 @@ impl Violation for MultiLineImplicitStringConcatenation { /// ISC001, ISC002 pub(crate) fn implicit( + diagnostics: &mut Vec, tokens: &[LexResult], settings: &Settings, locator: &Locator, -) -> Vec { - let mut diagnostics = vec![]; +) { for ((a_tok, a_range), (b_tok, b_range)) in tokens .iter() .flatten() .filter(|(tok, _)| { - !matches!(tok, Tok::Comment(..)) - && (settings.allow_multiline || !matches!(tok, Tok::NonLogicalNewline)) + !tok.is_comment() && (settings.allow_multiline || !tok.is_non_logical_newline()) }) .tuple_windows() { - if matches!(a_tok, Tok::String { .. }) && matches!(b_tok, Tok::String { .. }) { + if a_tok.is_string() && b_tok.is_string() { if locator.contains_line_break(TextRange::new(a_range.end(), b_range.start())) { diagnostics.push(Diagnostic::new( MultiLineImplicitStringConcatenation, TextRange::new(a_range.start(), b_range.end()), )); } else { - diagnostics.push(Diagnostic::new( + let mut diagnostic = Diagnostic::new( SingleLineImplicitStringConcatenation, TextRange::new(a_range.start(), b_range.end()), - )); - } - } + ); + + if let Some(fix) = concatenate_strings(*a_range, *b_range, locator) { + diagnostic.set_fix(fix); + } + + diagnostics.push(diagnostic); + }; + }; } - diagnostics +} + +fn concatenate_strings(a_range: TextRange, b_range: TextRange, locator: &Locator) -> Option { + let a_text = &locator.contents()[a_range]; + let b_text = &locator.contents()[b_range]; + + let a_leading_quote = leading_quote(a_text)?; + let b_leading_quote = leading_quote(b_text)?; + + // Require, for now, that the leading quotes are the same. + if a_leading_quote != b_leading_quote { + return None; + } + + let a_trailing_quote = trailing_quote(a_text)?; + let b_trailing_quote = trailing_quote(b_text)?; + + // Require, for now, that the trailing quotes are the same. + if a_trailing_quote != b_trailing_quote { + return None; + } + + let a_body = &a_text[a_leading_quote.len()..a_text.len() - a_trailing_quote.len()]; + let b_body = &b_text[b_leading_quote.len()..b_text.len() - b_trailing_quote.len()]; + + let concatenation = format!("{a_leading_quote}{a_body}{b_body}{a_trailing_quote}"); + let range = TextRange::new(a_range.start(), b_range.end()); + + Some(Fix::automatic(Edit::range_replacement( + concatenation, + range, + ))) } diff --git a/crates/ruff/src/rules/flake8_implicit_str_concat/rules/mod.rs b/crates/ruff/src/rules/flake8_implicit_str_concat/rules/mod.rs index 605341dc27..8ec813567d 100644 --- a/crates/ruff/src/rules/flake8_implicit_str_concat/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_implicit_str_concat/rules/mod.rs @@ -1,7 +1,5 @@ -pub(crate) use explicit::{explicit, ExplicitStringConcatenation}; -pub(crate) use implicit::{ - implicit, MultiLineImplicitStringConcatenation, SingleLineImplicitStringConcatenation, -}; +pub(crate) use explicit::*; +pub(crate) use implicit::*; mod explicit; mod implicit; diff --git a/crates/ruff/src/rules/flake8_implicit_str_concat/snapshots/ruff__rules__flake8_implicit_str_concat__tests__ISC001_ISC.py.snap b/crates/ruff/src/rules/flake8_implicit_str_concat/snapshots/ruff__rules__flake8_implicit_str_concat__tests__ISC001_ISC.py.snap index 6770789051..afeafc7660 100644 --- a/crates/ruff/src/rules/flake8_implicit_str_concat/snapshots/ruff__rules__flake8_implicit_str_concat__tests__ISC001_ISC.py.snap +++ b/crates/ruff/src/rules/flake8_implicit_str_concat/snapshots/ruff__rules__flake8_implicit_str_concat__tests__ISC001_ISC.py.snap @@ -1,20 +1,151 @@ --- source: crates/ruff/src/rules/flake8_implicit_str_concat/mod.rs --- -ISC.py:1:5: ISC001 Implicitly concatenated string literals on one line +ISC.py:1:5: ISC001 [*] Implicitly concatenated string literals on one line | 1 | _ = "a" "b" "c" | ^^^^^^^ ISC001 2 | 3 | _ = "abc" + "def" | + = help: Combine string literals -ISC.py:1:9: ISC001 Implicitly concatenated string literals on one line +ℹ Fix +1 |-_ = "a" "b" "c" + 1 |+_ = "ab" "c" +2 2 | +3 3 | _ = "abc" + "def" +4 4 | + +ISC.py:1:9: ISC001 [*] Implicitly concatenated string literals on one line | 1 | _ = "a" "b" "c" | ^^^^^^^ ISC001 2 | 3 | _ = "abc" + "def" | + = help: Combine string literals + +ℹ Fix +1 |-_ = "a" "b" "c" + 1 |+_ = "a" "bc" +2 2 | +3 3 | _ = "abc" + "def" +4 4 | + +ISC.py:38:5: ISC001 [*] Implicitly concatenated string literals on one line + | +36 | ) +37 | +38 | _ = """a""" """b""" + | ^^^^^^^^^^^^^^^ ISC001 +39 | +40 | _ = """a + | + = help: Combine string literals + +ℹ Fix +35 35 | b"def" +36 36 | ) +37 37 | +38 |-_ = """a""" """b""" + 38 |+_ = """ab""" +39 39 | +40 40 | _ = """a +41 41 | b""" """c + +ISC.py:40:5: ISC001 [*] Implicitly concatenated string literals on one line + | +38 | _ = """a""" """b""" +39 | +40 | _ = """a + | _____^ +41 | | b""" """c +42 | | d""" + | |____^ ISC001 +43 | +44 | _ = f"""a""" f"""b""" + | + = help: Combine string literals + +ℹ Fix +38 38 | _ = """a""" """b""" +39 39 | +40 40 | _ = """a +41 |-b""" """c + 41 |+bc +42 42 | d""" +43 43 | +44 44 | _ = f"""a""" f"""b""" + +ISC.py:44:5: ISC001 [*] Implicitly concatenated string literals on one line + | +42 | d""" +43 | +44 | _ = f"""a""" f"""b""" + | ^^^^^^^^^^^^^^^^^ ISC001 +45 | +46 | _ = f"a" "b" + | + = help: Combine string literals + +ℹ Fix +41 41 | b""" """c +42 42 | d""" +43 43 | +44 |-_ = f"""a""" f"""b""" + 44 |+_ = f"""ab""" +45 45 | +46 46 | _ = f"a" "b" +47 47 | + +ISC.py:46:5: ISC001 Implicitly concatenated string literals on one line + | +44 | _ = f"""a""" f"""b""" +45 | +46 | _ = f"a" "b" + | ^^^^^^^^ ISC001 +47 | +48 | _ = """a""" "b" + | + = help: Combine string literals + +ISC.py:48:5: ISC001 Implicitly concatenated string literals on one line + | +46 | _ = f"a" "b" +47 | +48 | _ = """a""" "b" + | ^^^^^^^^^^^ ISC001 +49 | +50 | _ = 'a' "b" + | + = help: Combine string literals + +ISC.py:50:5: ISC001 Implicitly concatenated string literals on one line + | +48 | _ = """a""" "b" +49 | +50 | _ = 'a' "b" + | ^^^^^^^ ISC001 +51 | +52 | _ = rf"a" rf"b" + | + = help: Combine string literals + +ISC.py:52:5: ISC001 [*] Implicitly concatenated string literals on one line + | +50 | _ = 'a' "b" +51 | +52 | _ = rf"a" rf"b" + | ^^^^^^^^^^^ ISC001 + | + = help: Combine string literals + +ℹ Fix +49 49 | +50 50 | _ = 'a' "b" +51 51 | +52 |-_ = rf"a" rf"b" + 52 |+_ = rf"ab" diff --git a/crates/ruff/src/rules/flake8_implicit_str_concat/snapshots/ruff__rules__flake8_implicit_str_concat__tests__multiline_ISC001_ISC.py.snap b/crates/ruff/src/rules/flake8_implicit_str_concat/snapshots/ruff__rules__flake8_implicit_str_concat__tests__multiline_ISC001_ISC.py.snap index 6770789051..afeafc7660 100644 --- a/crates/ruff/src/rules/flake8_implicit_str_concat/snapshots/ruff__rules__flake8_implicit_str_concat__tests__multiline_ISC001_ISC.py.snap +++ b/crates/ruff/src/rules/flake8_implicit_str_concat/snapshots/ruff__rules__flake8_implicit_str_concat__tests__multiline_ISC001_ISC.py.snap @@ -1,20 +1,151 @@ --- source: crates/ruff/src/rules/flake8_implicit_str_concat/mod.rs --- -ISC.py:1:5: ISC001 Implicitly concatenated string literals on one line +ISC.py:1:5: ISC001 [*] Implicitly concatenated string literals on one line | 1 | _ = "a" "b" "c" | ^^^^^^^ ISC001 2 | 3 | _ = "abc" + "def" | + = help: Combine string literals -ISC.py:1:9: ISC001 Implicitly concatenated string literals on one line +ℹ Fix +1 |-_ = "a" "b" "c" + 1 |+_ = "ab" "c" +2 2 | +3 3 | _ = "abc" + "def" +4 4 | + +ISC.py:1:9: ISC001 [*] Implicitly concatenated string literals on one line | 1 | _ = "a" "b" "c" | ^^^^^^^ ISC001 2 | 3 | _ = "abc" + "def" | + = help: Combine string literals + +ℹ Fix +1 |-_ = "a" "b" "c" + 1 |+_ = "a" "bc" +2 2 | +3 3 | _ = "abc" + "def" +4 4 | + +ISC.py:38:5: ISC001 [*] Implicitly concatenated string literals on one line + | +36 | ) +37 | +38 | _ = """a""" """b""" + | ^^^^^^^^^^^^^^^ ISC001 +39 | +40 | _ = """a + | + = help: Combine string literals + +ℹ Fix +35 35 | b"def" +36 36 | ) +37 37 | +38 |-_ = """a""" """b""" + 38 |+_ = """ab""" +39 39 | +40 40 | _ = """a +41 41 | b""" """c + +ISC.py:40:5: ISC001 [*] Implicitly concatenated string literals on one line + | +38 | _ = """a""" """b""" +39 | +40 | _ = """a + | _____^ +41 | | b""" """c +42 | | d""" + | |____^ ISC001 +43 | +44 | _ = f"""a""" f"""b""" + | + = help: Combine string literals + +ℹ Fix +38 38 | _ = """a""" """b""" +39 39 | +40 40 | _ = """a +41 |-b""" """c + 41 |+bc +42 42 | d""" +43 43 | +44 44 | _ = f"""a""" f"""b""" + +ISC.py:44:5: ISC001 [*] Implicitly concatenated string literals on one line + | +42 | d""" +43 | +44 | _ = f"""a""" f"""b""" + | ^^^^^^^^^^^^^^^^^ ISC001 +45 | +46 | _ = f"a" "b" + | + = help: Combine string literals + +ℹ Fix +41 41 | b""" """c +42 42 | d""" +43 43 | +44 |-_ = f"""a""" f"""b""" + 44 |+_ = f"""ab""" +45 45 | +46 46 | _ = f"a" "b" +47 47 | + +ISC.py:46:5: ISC001 Implicitly concatenated string literals on one line + | +44 | _ = f"""a""" f"""b""" +45 | +46 | _ = f"a" "b" + | ^^^^^^^^ ISC001 +47 | +48 | _ = """a""" "b" + | + = help: Combine string literals + +ISC.py:48:5: ISC001 Implicitly concatenated string literals on one line + | +46 | _ = f"a" "b" +47 | +48 | _ = """a""" "b" + | ^^^^^^^^^^^ ISC001 +49 | +50 | _ = 'a' "b" + | + = help: Combine string literals + +ISC.py:50:5: ISC001 Implicitly concatenated string literals on one line + | +48 | _ = """a""" "b" +49 | +50 | _ = 'a' "b" + | ^^^^^^^ ISC001 +51 | +52 | _ = rf"a" rf"b" + | + = help: Combine string literals + +ISC.py:52:5: ISC001 [*] Implicitly concatenated string literals on one line + | +50 | _ = 'a' "b" +51 | +52 | _ = rf"a" rf"b" + | ^^^^^^^^^^^ ISC001 + | + = help: Combine string literals + +ℹ Fix +49 49 | +50 50 | _ = 'a' "b" +51 51 | +52 |-_ = rf"a" rf"b" + 52 |+_ = rf"ab" diff --git a/crates/ruff/src/rules/flake8_import_conventions/rules/conventional_import_alias.rs b/crates/ruff/src/rules/flake8_import_conventions/rules/conventional_import_alias.rs deleted file mode 100644 index 58ee94eca0..0000000000 --- a/crates/ruff/src/rules/flake8_import_conventions/rules/conventional_import_alias.rs +++ /dev/null @@ -1,61 +0,0 @@ -use rustc_hash::FxHashMap; -use rustpython_parser::ast::{Ranged, Stmt}; - -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, violation}; - -/// ## What it does -/// Checks for imports that are typically imported using a common convention, -/// like `import pandas as pd`, and enforces that convention. -/// -/// ## Why is this bad? -/// Consistency is good. Use a common convention for imports to make your code -/// more readable and idiomatic. -/// -/// For example, `import pandas as pd` is a common -/// convention for importing the `pandas` library, and users typically expect -/// Pandas to be aliased as `pd`. -/// -/// ## Example -/// ```python -/// import pandas -/// ``` -/// -/// Use instead: -/// ```python -/// import pandas as pd -/// ``` -#[violation] -pub struct UnconventionalImportAlias { - name: String, - asname: String, -} - -impl Violation for UnconventionalImportAlias { - #[derive_message_formats] - fn message(&self) -> String { - let UnconventionalImportAlias { name, asname } = self; - format!("`{name}` should be imported as `{asname}`") - } -} - -/// ICN001 -pub(crate) fn conventional_import_alias( - stmt: &Stmt, - name: &str, - asname: Option<&str>, - conventions: &FxHashMap, -) -> Option { - if let Some(expected_alias) = conventions.get(name) { - if asname != Some(expected_alias) { - return Some(Diagnostic::new( - UnconventionalImportAlias { - name: name.to_string(), - asname: expected_alias.to_string(), - }, - stmt.range(), - )); - } - } - None -} diff --git a/crates/ruff/src/rules/flake8_import_conventions/rules/mod.rs b/crates/ruff/src/rules/flake8_import_conventions/rules/mod.rs index 63f88d0f9a..96b426deb6 100644 --- a/crates/ruff/src/rules/flake8_import_conventions/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_import_conventions/rules/mod.rs @@ -1,7 +1,7 @@ -pub(crate) use banned_import_alias::{banned_import_alias, BannedImportAlias}; -pub(crate) use banned_import_from::{banned_import_from, BannedImportFrom}; -pub(crate) use conventional_import_alias::{conventional_import_alias, UnconventionalImportAlias}; +pub(crate) use banned_import_alias::*; +pub(crate) use banned_import_from::*; +pub(crate) use unconventional_import_alias::*; mod banned_import_alias; mod banned_import_from; -mod conventional_import_alias; +mod unconventional_import_alias; diff --git a/crates/ruff/src/rules/flake8_import_conventions/rules/unconventional_import_alias.rs b/crates/ruff/src/rules/flake8_import_conventions/rules/unconventional_import_alias.rs new file mode 100644 index 0000000000..dc5876dac3 --- /dev/null +++ b/crates/ruff/src/rules/flake8_import_conventions/rules/unconventional_import_alias.rs @@ -0,0 +1,94 @@ +use rustc_hash::FxHashMap; + +use ruff_diagnostics::{AutofixKind, Diagnostic, Fix, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_semantic::Scope; + +use crate::checkers::ast::Checker; +use crate::registry::AsRule; +use crate::renamer::Renamer; + +/// ## What it does +/// Checks for imports that are typically imported using a common convention, +/// like `import pandas as pd`, and enforces that convention. +/// +/// ## Why is this bad? +/// Consistency is good. Use a common convention for imports to make your code +/// more readable and idiomatic. +/// +/// For example, `import pandas as pd` is a common +/// convention for importing the `pandas` library, and users typically expect +/// Pandas to be aliased as `pd`. +/// +/// ## Example +/// ```python +/// import pandas +/// ``` +/// +/// Use instead: +/// ```python +/// import pandas as pd +/// ``` +#[violation] +pub struct UnconventionalImportAlias { + name: String, + asname: String, +} + +impl Violation for UnconventionalImportAlias { + const AUTOFIX: AutofixKind = AutofixKind::Sometimes; + + #[derive_message_formats] + fn message(&self) -> String { + let UnconventionalImportAlias { name, asname } = self; + format!("`{name}` should be imported as `{asname}`") + } + + fn autofix_title(&self) -> Option { + let UnconventionalImportAlias { name, asname } = self; + Some(format!("Alias `{name}` to `{asname}`")) + } +} + +/// ICN001 +pub(crate) fn unconventional_import_alias( + checker: &Checker, + scope: &Scope, + diagnostics: &mut Vec, + conventions: &FxHashMap, +) -> Option { + for (name, binding_id) in scope.all_bindings() { + let binding = checker.semantic().binding(binding_id); + + let Some(qualified_name) = binding.qualified_name() else { + continue; + }; + + let Some(expected_alias) = conventions.get(qualified_name) else { + continue; + }; + + if binding.is_alias() && name == expected_alias { + continue; + } + + let mut diagnostic = Diagnostic::new( + UnconventionalImportAlias { + name: qualified_name.to_string(), + asname: expected_alias.to_string(), + }, + binding.range, + ); + if checker.patch(diagnostic.kind.rule()) { + if checker.semantic().is_available(expected_alias) { + diagnostic.try_set_fix(|| { + let (edit, rest) = + Renamer::rename(name, expected_alias, scope, checker.semantic())?; + Ok(Fix::suggested_edits(edit, rest)) + }); + } + } + diagnostics.push(diagnostic); + } + None +} diff --git a/crates/ruff/src/rules/flake8_import_conventions/settings.rs b/crates/ruff/src/rules/flake8_import_conventions/settings.rs index 95a0fb7e2e..d5a038b54c 100644 --- a/crates/ruff/src/rules/flake8_import_conventions/settings.rs +++ b/crates/ruff/src/rules/flake8_import_conventions/settings.rs @@ -13,6 +13,7 @@ const CONVENTIONAL_ALIASES: &[(&str, &str)] = &[ ("pandas", "pd"), ("seaborn", "sns"), ("tensorflow", "tf"), + ("tkinter", "tk"), ("holoviews", "hv"), ("panel", "pn"), ("plotly.express", "px"), @@ -31,7 +32,7 @@ const CONVENTIONAL_ALIASES: &[(&str, &str)] = &[ #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct Options { #[option( - default = r#"{"altair": "alt", "matplotlib": "mpl", "matplotlib.pyplot": "plt", "numpy": "np", "pandas": "pd", "seaborn": "sns", "tensorflow": "tf", "holoviews": "hv", "panel": "pn", "plotly.express": "px", "polars": "pl", "pyarrow": "pa"}"#, + default = r#"{"altair": "alt", "matplotlib": "mpl", "matplotlib.pyplot": "plt", "numpy": "np", "pandas": "pd", "seaborn": "sns", "tensorflow": "tf", "tkinter": "tk", "holoviews": "hv", "panel": "pn", "plotly.express": "px", "polars": "pl", "pyarrow": "pa"}"#, value_type = "dict[str, str]", example = r#" [tool.ruff.flake8-import-conventions.aliases] diff --git a/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__custom.snap b/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__custom.snap index c201a802f9..1b7779d90d 100644 --- a/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__custom.snap +++ b/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__custom.snap @@ -1,280 +1,308 @@ --- source: crates/ruff/src/rules/flake8_import_conventions/mod.rs --- -custom.py:3:1: ICN001 `altair` should be imported as `alt` +custom.py:3:8: ICN001 `altair` should be imported as `alt` | 1 | import math # not checked 2 | 3 | import altair # unconventional - | ^^^^^^^^^^^^^ ICN001 + | ^^^^^^ ICN001 4 | import dask.array # unconventional 5 | import dask.dataframe # unconventional | + = help: Alias `altair` to `alt` -custom.py:4:1: ICN001 `dask.array` should be imported as `da` +custom.py:4:8: ICN001 `dask.array` should be imported as `da` | 3 | import altair # unconventional 4 | import dask.array # unconventional - | ^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^^^^^^^ ICN001 5 | import dask.dataframe # unconventional 6 | import matplotlib.pyplot # unconventional | + = help: Alias `dask.array` to `da` -custom.py:5:1: ICN001 `dask.dataframe` should be imported as `dd` +custom.py:5:8: ICN001 `dask.dataframe` should be imported as `dd` | 3 | import altair # unconventional 4 | import dask.array # unconventional 5 | import dask.dataframe # unconventional - | ^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^^^^^^^^^^^ ICN001 6 | import matplotlib.pyplot # unconventional 7 | import numpy # unconventional | + = help: Alias `dask.dataframe` to `dd` -custom.py:6:1: ICN001 `matplotlib.pyplot` should be imported as `plt` +custom.py:6:8: ICN001 `matplotlib.pyplot` should be imported as `plt` | 4 | import dask.array # unconventional 5 | import dask.dataframe # unconventional 6 | import matplotlib.pyplot # unconventional - | ^^^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^^^^^^^^^^^^^^ ICN001 7 | import numpy # unconventional 8 | import pandas # unconventional | + = help: Alias `matplotlib.pyplot` to `plt` -custom.py:7:1: ICN001 `numpy` should be imported as `np` +custom.py:7:8: ICN001 `numpy` should be imported as `np` | 5 | import dask.dataframe # unconventional 6 | import matplotlib.pyplot # unconventional 7 | import numpy # unconventional - | ^^^^^^^^^^^^ ICN001 + | ^^^^^ ICN001 8 | import pandas # unconventional 9 | import seaborn # unconventional | + = help: Alias `numpy` to `np` -custom.py:8:1: ICN001 `pandas` should be imported as `pd` +custom.py:8:8: ICN001 `pandas` should be imported as `pd` | 6 | import matplotlib.pyplot # unconventional 7 | import numpy # unconventional 8 | import pandas # unconventional - | ^^^^^^^^^^^^^ ICN001 + | ^^^^^^ ICN001 9 | import seaborn # unconventional 10 | import tensorflow # unconventional | + = help: Alias `pandas` to `pd` -custom.py:9:1: ICN001 `seaborn` should be imported as `sns` +custom.py:9:8: ICN001 `seaborn` should be imported as `sns` | 7 | import numpy # unconventional 8 | import pandas # unconventional 9 | import seaborn # unconventional - | ^^^^^^^^^^^^^^ ICN001 + | ^^^^^^^ ICN001 10 | import tensorflow # unconventional 11 | import holoviews # unconventional | + = help: Alias `seaborn` to `sns` -custom.py:10:1: ICN001 `tensorflow` should be imported as `tf` +custom.py:10:8: ICN001 `tensorflow` should be imported as `tf` | 8 | import pandas # unconventional 9 | import seaborn # unconventional 10 | import tensorflow # unconventional - | ^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^^^^^^^ ICN001 11 | import holoviews # unconventional 12 | import panel # unconventional | + = help: Alias `tensorflow` to `tf` -custom.py:11:1: ICN001 `holoviews` should be imported as `hv` +custom.py:11:8: ICN001 `holoviews` should be imported as `hv` | 9 | import seaborn # unconventional 10 | import tensorflow # unconventional 11 | import holoviews # unconventional - | ^^^^^^^^^^^^^^^^ ICN001 + | ^^^^^^^^^ ICN001 12 | import panel # unconventional 13 | import plotly.express # unconventional | + = help: Alias `holoviews` to `hv` -custom.py:12:1: ICN001 `panel` should be imported as `pn` +custom.py:12:8: ICN001 `panel` should be imported as `pn` | 10 | import tensorflow # unconventional 11 | import holoviews # unconventional 12 | import panel # unconventional - | ^^^^^^^^^^^^ ICN001 + | ^^^^^ ICN001 13 | import plotly.express # unconventional 14 | import matplotlib # unconventional | + = help: Alias `panel` to `pn` -custom.py:13:1: ICN001 `plotly.express` should be imported as `px` +custom.py:13:8: ICN001 `plotly.express` should be imported as `px` | 11 | import holoviews # unconventional 12 | import panel # unconventional 13 | import plotly.express # unconventional - | ^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^^^^^^^^^^^ ICN001 14 | import matplotlib # unconventional 15 | import polars # unconventional | + = help: Alias `plotly.express` to `px` -custom.py:14:1: ICN001 `matplotlib` should be imported as `mpl` +custom.py:14:8: ICN001 `matplotlib` should be imported as `mpl` | 12 | import panel # unconventional 13 | import plotly.express # unconventional 14 | import matplotlib # unconventional - | ^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^^^^^^^ ICN001 15 | import polars # unconventional 16 | import pyarrow # unconventional | + = help: Alias `matplotlib` to `mpl` -custom.py:15:1: ICN001 `polars` should be imported as `pl` +custom.py:15:8: ICN001 `polars` should be imported as `pl` | 13 | import plotly.express # unconventional 14 | import matplotlib # unconventional 15 | import polars # unconventional - | ^^^^^^^^^^^^^ ICN001 + | ^^^^^^ ICN001 16 | import pyarrow # unconventional | + = help: Alias `polars` to `pl` -custom.py:16:1: ICN001 `pyarrow` should be imported as `pa` +custom.py:16:8: ICN001 `pyarrow` should be imported as `pa` | 14 | import matplotlib # unconventional 15 | import polars # unconventional 16 | import pyarrow # unconventional - | ^^^^^^^^^^^^^^ ICN001 + | ^^^^^^^ ICN001 17 | 18 | import altair as altr # unconventional | + = help: Alias `pyarrow` to `pa` -custom.py:18:1: ICN001 `altair` should be imported as `alt` +custom.py:18:18: ICN001 `altair` should be imported as `alt` | 16 | import pyarrow # unconventional 17 | 18 | import altair as altr # unconventional - | ^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^ ICN001 19 | import matplotlib.pyplot as plot # unconventional 20 | import dask.array as darray # unconventional | + = help: Alias `altair` to `alt` -custom.py:19:1: ICN001 `matplotlib.pyplot` should be imported as `plt` +custom.py:19:29: ICN001 `matplotlib.pyplot` should be imported as `plt` | 18 | import altair as altr # unconventional 19 | import matplotlib.pyplot as plot # unconventional - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^ ICN001 20 | import dask.array as darray # unconventional 21 | import dask.dataframe as ddf # unconventional | + = help: Alias `matplotlib.pyplot` to `plt` -custom.py:20:1: ICN001 `dask.array` should be imported as `da` +custom.py:20:22: ICN001 `dask.array` should be imported as `da` | 18 | import altair as altr # unconventional 19 | import matplotlib.pyplot as plot # unconventional 20 | import dask.array as darray # unconventional - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^^^ ICN001 21 | import dask.dataframe as ddf # unconventional 22 | import numpy as nmp # unconventional | + = help: Alias `dask.array` to `da` -custom.py:21:1: ICN001 `dask.dataframe` should be imported as `dd` +custom.py:21:26: ICN001 `dask.dataframe` should be imported as `dd` | 19 | import matplotlib.pyplot as plot # unconventional 20 | import dask.array as darray # unconventional 21 | import dask.dataframe as ddf # unconventional - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^ ICN001 22 | import numpy as nmp # unconventional 23 | import pandas as pdas # unconventional | + = help: Alias `dask.dataframe` to `dd` -custom.py:22:1: ICN001 `numpy` should be imported as `np` +custom.py:22:17: ICN001 `numpy` should be imported as `np` | 20 | import dask.array as darray # unconventional 21 | import dask.dataframe as ddf # unconventional 22 | import numpy as nmp # unconventional - | ^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^ ICN001 23 | import pandas as pdas # unconventional 24 | import seaborn as sbrn # unconventional | + = help: Alias `numpy` to `np` -custom.py:23:1: ICN001 `pandas` should be imported as `pd` +custom.py:23:18: ICN001 `pandas` should be imported as `pd` | 21 | import dask.dataframe as ddf # unconventional 22 | import numpy as nmp # unconventional 23 | import pandas as pdas # unconventional - | ^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^ ICN001 24 | import seaborn as sbrn # unconventional 25 | import tensorflow as tfz # unconventional | + = help: Alias `pandas` to `pd` -custom.py:24:1: ICN001 `seaborn` should be imported as `sns` +custom.py:24:19: ICN001 `seaborn` should be imported as `sns` | 22 | import numpy as nmp # unconventional 23 | import pandas as pdas # unconventional 24 | import seaborn as sbrn # unconventional - | ^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^ ICN001 25 | import tensorflow as tfz # unconventional 26 | import holoviews as hsv # unconventional | + = help: Alias `seaborn` to `sns` -custom.py:25:1: ICN001 `tensorflow` should be imported as `tf` +custom.py:25:22: ICN001 `tensorflow` should be imported as `tf` | 23 | import pandas as pdas # unconventional 24 | import seaborn as sbrn # unconventional 25 | import tensorflow as tfz # unconventional - | ^^^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^ ICN001 26 | import holoviews as hsv # unconventional 27 | import panel as pns # unconventional | + = help: Alias `tensorflow` to `tf` -custom.py:26:1: ICN001 `holoviews` should be imported as `hv` +custom.py:26:21: ICN001 `holoviews` should be imported as `hv` | 24 | import seaborn as sbrn # unconventional 25 | import tensorflow as tfz # unconventional 26 | import holoviews as hsv # unconventional - | ^^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^ ICN001 27 | import panel as pns # unconventional 28 | import plotly.express as pltx # unconventional | + = help: Alias `holoviews` to `hv` -custom.py:27:1: ICN001 `panel` should be imported as `pn` +custom.py:27:17: ICN001 `panel` should be imported as `pn` | 25 | import tensorflow as tfz # unconventional 26 | import holoviews as hsv # unconventional 27 | import panel as pns # unconventional - | ^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^ ICN001 28 | import plotly.express as pltx # unconventional 29 | import matplotlib as ml # unconventional | + = help: Alias `panel` to `pn` -custom.py:28:1: ICN001 `plotly.express` should be imported as `px` +custom.py:28:26: ICN001 `plotly.express` should be imported as `px` | 26 | import holoviews as hsv # unconventional 27 | import panel as pns # unconventional 28 | import plotly.express as pltx # unconventional - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^ ICN001 29 | import matplotlib as ml # unconventional 30 | import polars as ps # unconventional | + = help: Alias `plotly.express` to `px` -custom.py:29:1: ICN001 `matplotlib` should be imported as `mpl` +custom.py:29:22: ICN001 `matplotlib` should be imported as `mpl` | 27 | import panel as pns # unconventional 28 | import plotly.express as pltx # unconventional 29 | import matplotlib as ml # unconventional - | ^^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^ ICN001 30 | import polars as ps # unconventional 31 | import pyarrow as arr # unconventional | + = help: Alias `matplotlib` to `mpl` -custom.py:30:1: ICN001 `polars` should be imported as `pl` +custom.py:30:18: ICN001 `polars` should be imported as `pl` | 28 | import plotly.express as pltx # unconventional 29 | import matplotlib as ml # unconventional 30 | import polars as ps # unconventional - | ^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^ ICN001 31 | import pyarrow as arr # unconventional | + = help: Alias `polars` to `pl` -custom.py:31:1: ICN001 `pyarrow` should be imported as `pa` +custom.py:31:19: ICN001 `pyarrow` should be imported as `pa` | 29 | import matplotlib as ml # unconventional 30 | import polars as ps # unconventional 31 | import pyarrow as arr # unconventional - | ^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^ ICN001 32 | 33 | import altair as alt # conventional | + = help: Alias `pyarrow` to `pa` diff --git a/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__defaults.snap b/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__defaults.snap index 16167a773c..9197e9486a 100644 --- a/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__defaults.snap +++ b/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__defaults.snap @@ -1,100 +1,132 @@ --- source: crates/ruff/src/rules/flake8_import_conventions/mod.rs --- -defaults.py:3:1: ICN001 `altair` should be imported as `alt` +defaults.py:3:8: ICN001 `altair` should be imported as `alt` | 1 | import math # not checked 2 | 3 | import altair # unconventional - | ^^^^^^^^^^^^^ ICN001 + | ^^^^^^ ICN001 4 | import matplotlib.pyplot # unconventional 5 | import numpy # unconventional | + = help: Alias `altair` to `alt` -defaults.py:4:1: ICN001 `matplotlib.pyplot` should be imported as `plt` +defaults.py:4:8: ICN001 `matplotlib.pyplot` should be imported as `plt` | 3 | import altair # unconventional 4 | import matplotlib.pyplot # unconventional - | ^^^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^^^^^^^^^^^^^^ ICN001 5 | import numpy # unconventional 6 | import pandas # unconventional | + = help: Alias `matplotlib.pyplot` to `plt` -defaults.py:5:1: ICN001 `numpy` should be imported as `np` +defaults.py:5:8: ICN001 `numpy` should be imported as `np` | 3 | import altair # unconventional 4 | import matplotlib.pyplot # unconventional 5 | import numpy # unconventional - | ^^^^^^^^^^^^ ICN001 + | ^^^^^ ICN001 6 | import pandas # unconventional 7 | import seaborn # unconventional | + = help: Alias `numpy` to `np` -defaults.py:6:1: ICN001 `pandas` should be imported as `pd` +defaults.py:6:8: ICN001 `pandas` should be imported as `pd` | 4 | import matplotlib.pyplot # unconventional 5 | import numpy # unconventional 6 | import pandas # unconventional - | ^^^^^^^^^^^^^ ICN001 + | ^^^^^^ ICN001 7 | import seaborn # unconventional +8 | import tkinter # unconventional | + = help: Alias `pandas` to `pd` -defaults.py:7:1: ICN001 `seaborn` should be imported as `sns` +defaults.py:7:8: ICN001 `seaborn` should be imported as `sns` | 5 | import numpy # unconventional 6 | import pandas # unconventional 7 | import seaborn # unconventional - | ^^^^^^^^^^^^^^ ICN001 -8 | -9 | import altair as altr # unconventional + | ^^^^^^^ ICN001 +8 | import tkinter # unconventional | + = help: Alias `seaborn` to `sns` -defaults.py:9:1: ICN001 `altair` should be imported as `alt` +defaults.py:8:8: ICN001 `tkinter` should be imported as `tk` | + 6 | import pandas # unconventional 7 | import seaborn # unconventional - 8 | - 9 | import altair as altr # unconventional - | ^^^^^^^^^^^^^^^^^^^^^ ICN001 -10 | import matplotlib.pyplot as plot # unconventional -11 | import numpy as nmp # unconventional + 8 | import tkinter # unconventional + | ^^^^^^^ ICN001 + 9 | +10 | import altair as altr # unconventional | + = help: Alias `tkinter` to `tk` -defaults.py:10:1: ICN001 `matplotlib.pyplot` should be imported as `plt` +defaults.py:10:18: ICN001 `altair` should be imported as `alt` | - 9 | import altair as altr # unconventional -10 | import matplotlib.pyplot as plot # unconventional - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ICN001 -11 | import numpy as nmp # unconventional -12 | import pandas as pdas # unconventional + 8 | import tkinter # unconventional + 9 | +10 | import altair as altr # unconventional + | ^^^^ ICN001 +11 | import matplotlib.pyplot as plot # unconventional +12 | import numpy as nmp # unconventional | + = help: Alias `altair` to `alt` -defaults.py:11:1: ICN001 `numpy` should be imported as `np` +defaults.py:11:29: ICN001 `matplotlib.pyplot` should be imported as `plt` | - 9 | import altair as altr # unconventional -10 | import matplotlib.pyplot as plot # unconventional -11 | import numpy as nmp # unconventional - | ^^^^^^^^^^^^^^^^^^^ ICN001 -12 | import pandas as pdas # unconventional -13 | import seaborn as sbrn # unconventional +10 | import altair as altr # unconventional +11 | import matplotlib.pyplot as plot # unconventional + | ^^^^ ICN001 +12 | import numpy as nmp # unconventional +13 | import pandas as pdas # unconventional | + = help: Alias `matplotlib.pyplot` to `plt` -defaults.py:12:1: ICN001 `pandas` should be imported as `pd` +defaults.py:12:17: ICN001 `numpy` should be imported as `np` | -10 | import matplotlib.pyplot as plot # unconventional -11 | import numpy as nmp # unconventional -12 | import pandas as pdas # unconventional - | ^^^^^^^^^^^^^^^^^^^^^ ICN001 -13 | import seaborn as sbrn # unconventional +10 | import altair as altr # unconventional +11 | import matplotlib.pyplot as plot # unconventional +12 | import numpy as nmp # unconventional + | ^^^ ICN001 +13 | import pandas as pdas # unconventional +14 | import seaborn as sbrn # unconventional | + = help: Alias `numpy` to `np` -defaults.py:13:1: ICN001 `seaborn` should be imported as `sns` +defaults.py:13:18: ICN001 `pandas` should be imported as `pd` | -11 | import numpy as nmp # unconventional -12 | import pandas as pdas # unconventional -13 | import seaborn as sbrn # unconventional - | ^^^^^^^^^^^^^^^^^^^^^^ ICN001 -14 | -15 | import altair as alt # conventional +11 | import matplotlib.pyplot as plot # unconventional +12 | import numpy as nmp # unconventional +13 | import pandas as pdas # unconventional + | ^^^^ ICN001 +14 | import seaborn as sbrn # unconventional +15 | import tkinter as tkr # unconventional | + = help: Alias `pandas` to `pd` + +defaults.py:14:19: ICN001 `seaborn` should be imported as `sns` + | +12 | import numpy as nmp # unconventional +13 | import pandas as pdas # unconventional +14 | import seaborn as sbrn # unconventional + | ^^^^ ICN001 +15 | import tkinter as tkr # unconventional + | + = help: Alias `seaborn` to `sns` + +defaults.py:15:19: ICN001 `tkinter` should be imported as `tk` + | +13 | import pandas as pdas # unconventional +14 | import seaborn as sbrn # unconventional +15 | import tkinter as tkr # unconventional + | ^^^ ICN001 +16 | +17 | import altair as alt # conventional + | + = help: Alias `tkinter` to `tk` diff --git a/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__from_imports.snap b/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__from_imports.snap index 31d49549f1..bda73c8ac3 100644 --- a/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__from_imports.snap +++ b/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__from_imports.snap @@ -1,83 +1,91 @@ --- source: crates/ruff/src/rules/flake8_import_conventions/mod.rs --- -from_imports.py:3:1: ICN001 `xml.dom.minidom` should be imported as `md` +from_imports.py:3:8: ICN001 `xml.dom.minidom` should be imported as `md` | 1 | # Test absolute imports 2 | # Violation cases 3 | import xml.dom.minidom - | ^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^^^^^^^^^^^^ ICN001 4 | import xml.dom.minidom as wrong 5 | from xml.dom import minidom as wrong | + = help: Alias `xml.dom.minidom` to `md` -from_imports.py:4:1: ICN001 `xml.dom.minidom` should be imported as `md` +from_imports.py:4:27: ICN001 `xml.dom.minidom` should be imported as `md` | 2 | # Violation cases 3 | import xml.dom.minidom 4 | import xml.dom.minidom as wrong - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^^ ICN001 5 | from xml.dom import minidom as wrong 6 | from xml.dom import minidom | + = help: Alias `xml.dom.minidom` to `md` -from_imports.py:5:1: ICN001 `xml.dom.minidom` should be imported as `md` +from_imports.py:5:32: ICN001 `xml.dom.minidom` should be imported as `md` | 3 | import xml.dom.minidom 4 | import xml.dom.minidom as wrong 5 | from xml.dom import minidom as wrong - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^^ ICN001 6 | from xml.dom import minidom 7 | from xml.dom.minidom import parseString as wrong # Ensure ICN001 throws on function import. | + = help: Alias `xml.dom.minidom` to `md` -from_imports.py:6:1: ICN001 `xml.dom.minidom` should be imported as `md` +from_imports.py:6:21: ICN001 `xml.dom.minidom` should be imported as `md` | 4 | import xml.dom.minidom as wrong 5 | from xml.dom import minidom as wrong 6 | from xml.dom import minidom - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^^^^ ICN001 7 | from xml.dom.minidom import parseString as wrong # Ensure ICN001 throws on function import. 8 | from xml.dom.minidom import parseString | + = help: Alias `xml.dom.minidom` to `md` -from_imports.py:7:1: ICN001 `xml.dom.minidom.parseString` should be imported as `pstr` +from_imports.py:7:44: ICN001 `xml.dom.minidom.parseString` should be imported as `pstr` | 5 | from xml.dom import minidom as wrong 6 | from xml.dom import minidom 7 | from xml.dom.minidom import parseString as wrong # Ensure ICN001 throws on function import. - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^^ ICN001 8 | from xml.dom.minidom import parseString 9 | from xml.dom.minidom import parse, parseString | + = help: Alias `xml.dom.minidom.parseString` to `pstr` -from_imports.py:8:1: ICN001 `xml.dom.minidom.parseString` should be imported as `pstr` +from_imports.py:8:29: ICN001 `xml.dom.minidom.parseString` should be imported as `pstr` | 6 | from xml.dom import minidom 7 | from xml.dom.minidom import parseString as wrong # Ensure ICN001 throws on function import. 8 | from xml.dom.minidom import parseString - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^^^^^^^^ ICN001 9 | from xml.dom.minidom import parse, parseString 10 | from xml.dom.minidom import parse as ps, parseString as wrong | + = help: Alias `xml.dom.minidom.parseString` to `pstr` -from_imports.py:9:1: ICN001 `xml.dom.minidom.parseString` should be imported as `pstr` +from_imports.py:9:36: ICN001 `xml.dom.minidom.parseString` should be imported as `pstr` | 7 | from xml.dom.minidom import parseString as wrong # Ensure ICN001 throws on function import. 8 | from xml.dom.minidom import parseString 9 | from xml.dom.minidom import parse, parseString - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^^^^^^^^ ICN001 10 | from xml.dom.minidom import parse as ps, parseString as wrong | + = help: Alias `xml.dom.minidom.parseString` to `pstr` -from_imports.py:10:1: ICN001 `xml.dom.minidom.parseString` should be imported as `pstr` +from_imports.py:10:57: ICN001 `xml.dom.minidom.parseString` should be imported as `pstr` | 8 | from xml.dom.minidom import parseString 9 | from xml.dom.minidom import parse, parseString 10 | from xml.dom.minidom import parse as ps, parseString as wrong - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^^ ICN001 11 | 12 | # No ICN001 violations | + = help: Alias `xml.dom.minidom.parseString` to `pstr` diff --git a/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__override_default.snap b/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__override_default.snap index 905d5e6c24..a0e6bef88c 100644 --- a/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__override_default.snap +++ b/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__override_default.snap @@ -1,100 +1,110 @@ --- source: crates/ruff/src/rules/flake8_import_conventions/mod.rs --- -override_default.py:3:1: ICN001 `altair` should be imported as `alt` +override_default.py:3:8: ICN001 `altair` should be imported as `alt` | 1 | import math # not checked 2 | 3 | import altair # unconventional - | ^^^^^^^^^^^^^ ICN001 + | ^^^^^^ ICN001 4 | import matplotlib.pyplot # unconventional 5 | import numpy # unconventional | + = help: Alias `altair` to `alt` -override_default.py:4:1: ICN001 `matplotlib.pyplot` should be imported as `plt` +override_default.py:4:8: ICN001 `matplotlib.pyplot` should be imported as `plt` | 3 | import altair # unconventional 4 | import matplotlib.pyplot # unconventional - | ^^^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^^^^^^^^^^^^^^ ICN001 5 | import numpy # unconventional 6 | import pandas # unconventional | + = help: Alias `matplotlib.pyplot` to `plt` -override_default.py:5:1: ICN001 `numpy` should be imported as `nmp` +override_default.py:5:8: ICN001 `numpy` should be imported as `nmp` | 3 | import altair # unconventional 4 | import matplotlib.pyplot # unconventional 5 | import numpy # unconventional - | ^^^^^^^^^^^^ ICN001 + | ^^^^^ ICN001 6 | import pandas # unconventional 7 | import seaborn # unconventional | + = help: Alias `numpy` to `nmp` -override_default.py:6:1: ICN001 `pandas` should be imported as `pd` +override_default.py:6:8: ICN001 `pandas` should be imported as `pd` | 4 | import matplotlib.pyplot # unconventional 5 | import numpy # unconventional 6 | import pandas # unconventional - | ^^^^^^^^^^^^^ ICN001 + | ^^^^^^ ICN001 7 | import seaborn # unconventional | + = help: Alias `pandas` to `pd` -override_default.py:7:1: ICN001 `seaborn` should be imported as `sns` +override_default.py:7:8: ICN001 `seaborn` should be imported as `sns` | 5 | import numpy # unconventional 6 | import pandas # unconventional 7 | import seaborn # unconventional - | ^^^^^^^^^^^^^^ ICN001 + | ^^^^^^^ ICN001 8 | 9 | import altair as altr # unconventional | + = help: Alias `seaborn` to `sns` -override_default.py:9:1: ICN001 `altair` should be imported as `alt` +override_default.py:9:18: ICN001 `altair` should be imported as `alt` | 7 | import seaborn # unconventional 8 | 9 | import altair as altr # unconventional - | ^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^ ICN001 10 | import matplotlib.pyplot as plot # unconventional 11 | import numpy as np # unconventional | + = help: Alias `altair` to `alt` -override_default.py:10:1: ICN001 `matplotlib.pyplot` should be imported as `plt` +override_default.py:10:29: ICN001 `matplotlib.pyplot` should be imported as `plt` | 9 | import altair as altr # unconventional 10 | import matplotlib.pyplot as plot # unconventional - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^ ICN001 11 | import numpy as np # unconventional 12 | import pandas as pdas # unconventional | + = help: Alias `matplotlib.pyplot` to `plt` -override_default.py:11:1: ICN001 `numpy` should be imported as `nmp` +override_default.py:11:17: ICN001 `numpy` should be imported as `nmp` | 9 | import altair as altr # unconventional 10 | import matplotlib.pyplot as plot # unconventional 11 | import numpy as np # unconventional - | ^^^^^^^^^^^^^^^^^^ ICN001 + | ^^ ICN001 12 | import pandas as pdas # unconventional 13 | import seaborn as sbrn # unconventional | + = help: Alias `numpy` to `nmp` -override_default.py:12:1: ICN001 `pandas` should be imported as `pd` +override_default.py:12:18: ICN001 `pandas` should be imported as `pd` | 10 | import matplotlib.pyplot as plot # unconventional 11 | import numpy as np # unconventional 12 | import pandas as pdas # unconventional - | ^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^ ICN001 13 | import seaborn as sbrn # unconventional | + = help: Alias `pandas` to `pd` -override_default.py:13:1: ICN001 `seaborn` should be imported as `sns` +override_default.py:13:19: ICN001 `seaborn` should be imported as `sns` | 11 | import numpy as np # unconventional 12 | import pandas as pdas # unconventional 13 | import seaborn as sbrn # unconventional - | ^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^ ICN001 14 | 15 | import altair as alt # conventional | + = help: Alias `seaborn` to `sns` diff --git a/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__remove_default.snap b/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__remove_default.snap index 55b3c4fda9..b02135a6cb 100644 --- a/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__remove_default.snap +++ b/crates/ruff/src/rules/flake8_import_conventions/snapshots/ruff__rules__flake8_import_conventions__tests__remove_default.snap @@ -1,80 +1,88 @@ --- source: crates/ruff/src/rules/flake8_import_conventions/mod.rs --- -remove_default.py:3:1: ICN001 `altair` should be imported as `alt` +remove_default.py:3:8: ICN001 `altair` should be imported as `alt` | 1 | import math # not checked 2 | 3 | import altair # unconventional - | ^^^^^^^^^^^^^ ICN001 + | ^^^^^^ ICN001 4 | import matplotlib.pyplot # unconventional 5 | import numpy # not checked | + = help: Alias `altair` to `alt` -remove_default.py:4:1: ICN001 `matplotlib.pyplot` should be imported as `plt` +remove_default.py:4:8: ICN001 `matplotlib.pyplot` should be imported as `plt` | 3 | import altair # unconventional 4 | import matplotlib.pyplot # unconventional - | ^^^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^^^^^^^^^^^^^^ ICN001 5 | import numpy # not checked 6 | import pandas # unconventional | + = help: Alias `matplotlib.pyplot` to `plt` -remove_default.py:6:1: ICN001 `pandas` should be imported as `pd` +remove_default.py:6:8: ICN001 `pandas` should be imported as `pd` | 4 | import matplotlib.pyplot # unconventional 5 | import numpy # not checked 6 | import pandas # unconventional - | ^^^^^^^^^^^^^ ICN001 + | ^^^^^^ ICN001 7 | import seaborn # unconventional | + = help: Alias `pandas` to `pd` -remove_default.py:7:1: ICN001 `seaborn` should be imported as `sns` +remove_default.py:7:8: ICN001 `seaborn` should be imported as `sns` | 5 | import numpy # not checked 6 | import pandas # unconventional 7 | import seaborn # unconventional - | ^^^^^^^^^^^^^^ ICN001 + | ^^^^^^^ ICN001 8 | 9 | import altair as altr # unconventional | + = help: Alias `seaborn` to `sns` -remove_default.py:9:1: ICN001 `altair` should be imported as `alt` +remove_default.py:9:18: ICN001 `altair` should be imported as `alt` | 7 | import seaborn # unconventional 8 | 9 | import altair as altr # unconventional - | ^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^ ICN001 10 | import matplotlib.pyplot as plot # unconventional 11 | import numpy as nmp # not checked | + = help: Alias `altair` to `alt` -remove_default.py:10:1: ICN001 `matplotlib.pyplot` should be imported as `plt` +remove_default.py:10:29: ICN001 `matplotlib.pyplot` should be imported as `plt` | 9 | import altair as altr # unconventional 10 | import matplotlib.pyplot as plot # unconventional - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^ ICN001 11 | import numpy as nmp # not checked 12 | import pandas as pdas # unconventional | + = help: Alias `matplotlib.pyplot` to `plt` -remove_default.py:12:1: ICN001 `pandas` should be imported as `pd` +remove_default.py:12:18: ICN001 `pandas` should be imported as `pd` | 10 | import matplotlib.pyplot as plot # unconventional 11 | import numpy as nmp # not checked 12 | import pandas as pdas # unconventional - | ^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^ ICN001 13 | import seaborn as sbrn # unconventional | + = help: Alias `pandas` to `pd` -remove_default.py:13:1: ICN001 `seaborn` should be imported as `sns` +remove_default.py:13:19: ICN001 `seaborn` should be imported as `sns` | 11 | import numpy as nmp # not checked 12 | import pandas as pdas # unconventional 13 | import seaborn as sbrn # unconventional - | ^^^^^^^^^^^^^^^^^^^^^^ ICN001 + | ^^^^ ICN001 14 | 15 | import altair as alt # conventional | + = help: Alias `seaborn` to `sns` diff --git a/crates/ruff/src/rules/flake8_logging_format/mod.rs b/crates/ruff/src/rules/flake8_logging_format/mod.rs index c76235febe..c334117482 100644 --- a/crates/ruff/src/rules/flake8_logging_format/mod.rs +++ b/crates/ruff/src/rules/flake8_logging_format/mod.rs @@ -1,4 +1,4 @@ -//! Rules from [flake8-logging-format](https://pypi.org/project/flake8-logging-format/0.9.0/). +//! Rules from [flake8-logging-format](https://pypi.org/project/flake8-logging-format/). pub(crate) mod rules; pub(crate) mod violations; diff --git a/crates/ruff/src/rules/flake8_logging_format/rules/logging_call.rs b/crates/ruff/src/rules/flake8_logging_format/rules/logging_call.rs index dceee291b9..cbfebaf121 100644 --- a/crates/ruff/src/rules/flake8_logging_format/rules/logging_call.rs +++ b/crates/ruff/src/rules/flake8_logging_format/rules/logging_call.rs @@ -4,7 +4,6 @@ use rustpython_parser::ast::{self, Constant, Expr, Keyword, Operator, Ranged}; use ruff_diagnostics::{Diagnostic, Edit, Fix}; use ruff_python_ast::helpers::{find_keyword, SimpleCallArgs}; use ruff_python_semantic::analyze::logging; -use ruff_python_semantic::analyze::logging::exc_info; use ruff_python_stdlib::logging::LoggingLevel; use crate::checkers::ast::Checker; @@ -14,30 +13,35 @@ use crate::rules::flake8_logging_format::violations::{ LoggingRedundantExcInfo, LoggingStringConcat, LoggingStringFormat, LoggingWarn, }; -const RESERVED_ATTRS: &[&str; 22] = &[ - "args", - "asctime", - "created", - "exc_info", - "exc_text", - "filename", - "funcName", - "levelname", - "levelno", - "lineno", - "module", - "msecs", - "message", - "msg", - "name", - "pathname", - "process", - "processName", - "relativeCreated", - "stack_info", - "thread", - "threadName", -]; +/// Returns `true` if the attribute is a reserved attribute on the `logging` module's `LogRecord` +/// class. +fn is_reserved_attr(attr: &str) -> bool { + matches!( + attr, + "args" + | "asctime" + | "created" + | "exc_info" + | "exc_text" + | "filename" + | "funcName" + | "levelname" + | "levelno" + | "lineno" + | "module" + | "msecs" + | "message" + | "msg" + | "name" + | "pathname" + | "process" + | "processName" + | "relativeCreated" + | "stack_info" + | "thread" + | "threadName" + ) +} /// Check logging messages for violations. fn check_msg(checker: &mut Checker, msg: &Expr) { @@ -91,13 +95,13 @@ fn check_log_record_attr_clash(checker: &mut Checker, extra: &Keyword) { for key in keys { if let Some(key) = &key { if let Expr::Constant(ast::ExprConstant { - value: Constant::Str(string), + value: Constant::Str(attr), .. }) = key { - if RESERVED_ATTRS.contains(&string.as_str()) { + if is_reserved_attr(attr) { checker.diagnostics.push(Diagnostic::new( - LoggingExtraAttrClash(string.to_string()), + LoggingExtraAttrClash(attr.to_string()), key.range(), )); } @@ -107,15 +111,17 @@ fn check_log_record_attr_clash(checker: &mut Checker, extra: &Keyword) { } Expr::Call(ast::ExprCall { func, keywords, .. }) => { if checker - .semantic_model() + .semantic() .resolve_call_path(func) - .map_or(false, |call_path| call_path.as_slice() == ["", "dict"]) + .map_or(false, |call_path| { + matches!(call_path.as_slice(), ["", "dict"]) + }) { for keyword in keywords { - if let Some(key) = &keyword.arg { - if RESERVED_ATTRS.contains(&key.as_str()) { + if let Some(attr) = &keyword.arg { + if is_reserved_attr(attr) { checker.diagnostics.push(Diagnostic::new( - LoggingExtraAttrClash(key.to_string()), + LoggingExtraAttrClash(attr.to_string()), keyword.range(), )); } @@ -152,7 +158,7 @@ pub(crate) fn logging_call( args: &[Expr], keywords: &[Keyword], ) { - if !logging::is_logger_candidate(func, checker.semantic_model()) { + if !logging::is_logger_candidate(func, checker.semantic()) { return; } @@ -176,8 +182,7 @@ pub(crate) fn logging_call( { let mut diagnostic = Diagnostic::new(LoggingWarn, level_call_range); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( "warning".to_string(), level_call_range, ))); @@ -194,10 +199,10 @@ pub(crate) fn logging_call( // G201, G202 if checker.any_enabled(&[Rule::LoggingExcInfo, Rule::LoggingRedundantExcInfo]) { - if !checker.semantic_model().in_exception_handler() { + if !checker.semantic().in_exception_handler() { return; } - let Some(exc_info) = exc_info(keywords, checker.semantic_model()) else { + let Some(exc_info) = logging::exc_info(keywords, checker.semantic()) else { return; }; if let LoggingCallType::LevelCall(logging_level) = logging_call_type { diff --git a/crates/ruff/src/rules/flake8_logging_format/rules/mod.rs b/crates/ruff/src/rules/flake8_logging_format/rules/mod.rs index c0cce2dd57..94db04d8df 100644 --- a/crates/ruff/src/rules/flake8_logging_format/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_logging_format/rules/mod.rs @@ -1,3 +1,3 @@ -pub(crate) use logging_call::logging_call; +pub(crate) use logging_call::*; mod logging_call; diff --git a/crates/ruff/src/rules/flake8_logging_format/snapshots/ruff__rules__flake8_logging_format__tests__G001.py.snap b/crates/ruff/src/rules/flake8_logging_format/snapshots/ruff__rules__flake8_logging_format__tests__G001.py.snap index ce85d609df..e1ed539d35 100644 --- a/crates/ruff/src/rules/flake8_logging_format/snapshots/ruff__rules__flake8_logging_format__tests__G001.py.snap +++ b/crates/ruff/src/rules/flake8_logging_format/snapshots/ruff__rules__flake8_logging_format__tests__G001.py.snap @@ -1,7 +1,7 @@ --- source: crates/ruff/src/rules/flake8_logging_format/mod.rs --- -G001.py:4:14: G001 Logging statement uses `string.format()` +G001.py:4:14: G001 Logging statement uses `str.format` | 2 | import logging as foo 3 | @@ -11,7 +11,7 @@ G001.py:4:14: G001 Logging statement uses `string.format()` 6 | foo.info("Hello {}".format("World!")) | -G001.py:5:27: G001 Logging statement uses `string.format()` +G001.py:5:27: G001 Logging statement uses `str.format` | 4 | logging.info("Hello {}".format("World!")) 5 | logging.log(logging.INFO, "Hello {}".format("World!")) @@ -20,7 +20,7 @@ G001.py:5:27: G001 Logging statement uses `string.format()` 7 | logging.log(logging.INFO, msg="Hello {}".format("World!")) | -G001.py:6:10: G001 Logging statement uses `string.format()` +G001.py:6:10: G001 Logging statement uses `str.format` | 4 | logging.info("Hello {}".format("World!")) 5 | logging.log(logging.INFO, "Hello {}".format("World!")) @@ -30,7 +30,7 @@ G001.py:6:10: G001 Logging statement uses `string.format()` 8 | logging.log(level=logging.INFO, msg="Hello {}".format("World!")) | -G001.py:7:31: G001 Logging statement uses `string.format()` +G001.py:7:31: G001 Logging statement uses `str.format` | 5 | logging.log(logging.INFO, "Hello {}".format("World!")) 6 | foo.info("Hello {}".format("World!")) @@ -40,7 +40,7 @@ G001.py:7:31: G001 Logging statement uses `string.format()` 9 | logging.log(msg="Hello {}".format("World!"), level=logging.INFO) | -G001.py:8:37: G001 Logging statement uses `string.format()` +G001.py:8:37: G001 Logging statement uses `str.format` | 6 | foo.info("Hello {}".format("World!")) 7 | logging.log(logging.INFO, msg="Hello {}".format("World!")) @@ -49,7 +49,7 @@ G001.py:8:37: G001 Logging statement uses `string.format()` 9 | logging.log(msg="Hello {}".format("World!"), level=logging.INFO) | -G001.py:9:17: G001 Logging statement uses `string.format()` +G001.py:9:17: G001 Logging statement uses `str.format` | 7 | logging.log(logging.INFO, msg="Hello {}".format("World!")) 8 | logging.log(level=logging.INFO, msg="Hello {}".format("World!")) @@ -59,7 +59,7 @@ G001.py:9:17: G001 Logging statement uses `string.format()` 11 | # Flask support | -G001.py:16:31: G001 Logging statement uses `string.format()` +G001.py:16:31: G001 Logging statement uses `str.format` | 14 | from flask import current_app as app 15 | @@ -69,7 +69,7 @@ G001.py:16:31: G001 Logging statement uses `string.format()` 18 | app.logger.log(logging.INFO, "Hello {}".format("World!")) | -G001.py:17:25: G001 Logging statement uses `string.format()` +G001.py:17:25: G001 Logging statement uses `str.format` | 16 | flask.current_app.logger.info("Hello {}".format("World!")) 17 | current_app.logger.info("Hello {}".format("World!")) @@ -77,7 +77,7 @@ G001.py:17:25: G001 Logging statement uses `string.format()` 18 | app.logger.log(logging.INFO, "Hello {}".format("World!")) | -G001.py:18:30: G001 Logging statement uses `string.format()` +G001.py:18:30: G001 Logging statement uses `str.format` | 16 | flask.current_app.logger.info("Hello {}".format("World!")) 17 | current_app.logger.info("Hello {}".format("World!")) diff --git a/crates/ruff/src/rules/flake8_logging_format/snapshots/ruff__rules__flake8_logging_format__tests__G010.py.snap b/crates/ruff/src/rules/flake8_logging_format/snapshots/ruff__rules__flake8_logging_format__tests__G010.py.snap index 66a96150f2..b0a57cc6d1 100644 --- a/crates/ruff/src/rules/flake8_logging_format/snapshots/ruff__rules__flake8_logging_format__tests__G010.py.snap +++ b/crates/ruff/src/rules/flake8_logging_format/snapshots/ruff__rules__flake8_logging_format__tests__G010.py.snap @@ -11,7 +11,7 @@ G010.py:4:9: G010 [*] Logging statement uses `warn` instead of `warning` | = help: Convert to `warn` -ℹ Suggested fix +ℹ Fix 1 1 | import logging 2 2 | from distutils import log 3 3 | diff --git a/crates/ruff/src/rules/flake8_logging_format/snapshots/ruff__rules__flake8_logging_format__tests__G101_1.py.snap b/crates/ruff/src/rules/flake8_logging_format/snapshots/ruff__rules__flake8_logging_format__tests__G101_1.py.snap index 4bdc1ccbd7..ba7d9daa0d 100644 --- a/crates/ruff/src/rules/flake8_logging_format/snapshots/ruff__rules__flake8_logging_format__tests__G101_1.py.snap +++ b/crates/ruff/src/rules/flake8_logging_format/snapshots/ruff__rules__flake8_logging_format__tests__G101_1.py.snap @@ -1,7 +1,7 @@ --- source: crates/ruff/src/rules/flake8_logging_format/mod.rs --- -G101_1.py:6:9: G101 Logging statement uses an extra field that clashes with a LogRecord field: `name` +G101_1.py:6:9: G101 Logging statement uses an `extra` field that clashes with a `LogRecord` field: `name` | 4 | "Hello world!", 5 | extra={ diff --git a/crates/ruff/src/rules/flake8_logging_format/snapshots/ruff__rules__flake8_logging_format__tests__G101_2.py.snap b/crates/ruff/src/rules/flake8_logging_format/snapshots/ruff__rules__flake8_logging_format__tests__G101_2.py.snap index b1084f13a9..0db9761fbc 100644 --- a/crates/ruff/src/rules/flake8_logging_format/snapshots/ruff__rules__flake8_logging_format__tests__G101_2.py.snap +++ b/crates/ruff/src/rules/flake8_logging_format/snapshots/ruff__rules__flake8_logging_format__tests__G101_2.py.snap @@ -1,7 +1,7 @@ --- source: crates/ruff/src/rules/flake8_logging_format/mod.rs --- -G101_2.py:6:9: G101 Logging statement uses an extra field that clashes with a LogRecord field: `name` +G101_2.py:6:9: G101 Logging statement uses an `extra` field that clashes with a `LogRecord` field: `name` | 4 | "Hello world!", 5 | extra=dict( diff --git a/crates/ruff/src/rules/flake8_logging_format/violations.rs b/crates/ruff/src/rules/flake8_logging_format/violations.rs index 622c9b83ec..3bada9ec59 100644 --- a/crates/ruff/src/rules/flake8_logging_format/violations.rs +++ b/crates/ruff/src/rules/flake8_logging_format/violations.rs @@ -1,16 +1,130 @@ use ruff_diagnostics::{AlwaysAutofixableViolation, Violation}; use ruff_macros::{derive_message_formats, violation}; +/// ## What it does +/// Checks for uses of `str.format` to format logging messages. +/// +/// ## Why is this bad? +/// The `logging` module provides a mechanism for passing additional values to +/// be logged using the `extra` keyword argument. This is more consistent, more +/// efficient, and less error-prone than formatting the string directly. +/// +/// Using `str.format` to format a logging message requires that Python eagerly +/// format the string, even if the logging statement is never executed (e.g., +/// if the log level is above the level of the logging statement), whereas +/// using the `extra` keyword argument defers formatting until required. +/// +/// Additionally, the use of `extra` will ensure that the values are made +/// available to all handlers, which can then be configured to log the values +/// in a consistent manner. +/// +/// As an alternative to `extra`, passing values as arguments to the logging +/// method can also be used to defer string formatting until required. +/// +/// ## Example +/// ```python +/// import logging +/// +/// logging.basicConfig(format="%(message)s", level=logging.INFO) +/// +/// user = "Maria" +/// +/// logging.info("{} - Something happened".format(user)) +/// ``` +/// +/// Use instead: +/// ```python +/// import logging +/// +/// logging.basicConfig(format="%(user_id)s - %(message)s", level=logging.INFO) +/// +/// user = "Maria" +/// +/// logging.info("Something happened", extra={"user_id": user}) +/// ``` +/// +/// Or: +/// ```python +/// import logging +/// +/// logging.basicConfig(format="%(message)s", level=logging.INFO) +/// +/// user = "Maria" +/// +/// logging.info("%s - Something happened", user) +/// ``` +/// +/// ## References +/// - [Python documentation: `logging`](https://docs.python.org/3/library/logging.html) +/// - [Python documentation: Optimization](https://docs.python.org/3/howto/logging.html#optimization) #[violation] pub struct LoggingStringFormat; impl Violation for LoggingStringFormat { #[derive_message_formats] fn message(&self) -> String { - format!("Logging statement uses `string.format()`") + format!("Logging statement uses `str.format`") } } +/// ## What it does +/// Checks for uses of `printf`-style format strings to format logging +/// messages. +/// +/// ## Why is this bad? +/// The `logging` module provides a mechanism for passing additional values to +/// be logged using the `extra` keyword argument. This is more consistent, more +/// efficient, and less error-prone than formatting the string directly. +/// +/// Using `printf`-style format strings to format a logging message requires +/// that Python eagerly format the string, even if the logging statement is +/// never executed (e.g., if the log level is above the level of the logging +/// statement), whereas using the `extra` keyword argument defers formatting +/// until required. +/// +/// Additionally, the use of `extra` will ensure that the values are made +/// available to all handlers, which can then be configured to log the values +/// in a consistent manner. +/// +/// As an alternative to `extra`, passing values as arguments to the logging +/// method can also be used to defer string formatting until required. +/// +/// ## Example +/// ```python +/// import logging +/// +/// logging.basicConfig(format="%(message)s", level=logging.INFO) +/// +/// user = "Maria" +/// +/// logging.info("%s - Something happened" % user) +/// ``` +/// +/// Use instead: +/// ```python +/// import logging +/// +/// logging.basicConfig(format="%(user_id)s - %(message)s", level=logging.INFO) +/// +/// user = "Maria" +/// +/// logging.info("Something happened", extra=dict(user_id=user)) +/// ``` +/// +/// Or: +/// ```python +/// import logging +/// +/// logging.basicConfig(format="%(message)s", level=logging.INFO) +/// +/// user = "Maria" +/// +/// logging.info("%s - Something happened", user) +/// ``` +/// +/// ## References +/// - [Python documentation: `logging`](https://docs.python.org/3/library/logging.html) +/// - [Python documentation: Optimization](https://docs.python.org/3/howto/logging.html#optimization) #[violation] pub struct LoggingPercentFormat; @@ -21,6 +135,63 @@ impl Violation for LoggingPercentFormat { } } +/// ## What it does +/// Checks for uses string concatenation via the `+` operator to format logging +/// messages. +/// +/// ## Why is this bad? +/// The `logging` module provides a mechanism for passing additional values to +/// be logged using the `extra` keyword argument. This is more consistent, more +/// efficient, and less error-prone than formatting the string directly. +/// +/// Using concatenation to format a logging message requires that Python +/// eagerly format the string, even if the logging statement is never executed +/// (e.g., if the log level is above the level of the logging statement), +/// whereas using the `extra` keyword argument defers formatting until required. +/// +/// Additionally, the use of `extra` will ensure that the values are made +/// available to all handlers, which can then be configured to log the values +/// in a consistent manner. +/// +/// As an alternative to `extra`, passing values as arguments to the logging +/// method can also be used to defer string formatting until required. +/// +/// ## Example +/// ```python +/// import logging +/// +/// logging.basicConfig(format="%(message)s", level=logging.INFO) +/// +/// user = "Maria" +/// +/// logging.info(user + " - Something happened") +/// ``` +/// +/// Use instead: +/// ```python +/// import logging +/// +/// logging.basicConfig(format="%(user_id)s - %(message)s", level=logging.INFO) +/// +/// user = "Maria" +/// +/// logging.info("Something happened", extra=dict(user_id=user)) +/// ``` +/// +/// Or: +/// ```python +/// import logging +/// +/// logging.basicConfig(format="%(message)s", level=logging.INFO) +/// +/// user = "Maria" +/// +/// logging.info("%s - Something happened", user) +/// ``` +/// +/// ## References +/// - [Python documentation: `logging`](https://docs.python.org/3/library/logging.html) +/// - [Python documentation: Optimization](https://docs.python.org/3/howto/logging.html#optimization) #[violation] pub struct LoggingStringConcat; @@ -31,6 +202,62 @@ impl Violation for LoggingStringConcat { } } +/// ## What it does +/// Checks for uses of f-strings to format logging messages. +/// +/// ## Why is this bad? +/// The `logging` module provides a mechanism for passing additional values to +/// be logged using the `extra` keyword argument. This is more consistent, more +/// efficient, and less error-prone than formatting the string directly. +/// +/// Using f-strings to format a logging message requires that Python eagerly +/// format the string, even if the logging statement is never executed (e.g., +/// if the log level is above the level of the logging statement), whereas +/// using the `extra` keyword argument defers formatting until required. +/// +/// Additionally, the use of `extra` will ensure that the values are made +/// available to all handlers, which can then be configured to log the values +/// in a consistent manner. +/// +/// As an alternative to `extra`, passing values as arguments to the logging +/// method can also be used to defer string formatting until required. +/// +/// ## Example +/// ```python +/// import logging +/// +/// logging.basicConfig(format="%(message)s", level=logging.INFO) +/// +/// user = "Maria" +/// +/// logging.info(f"{user} - Something happened") +/// ``` +/// +/// Use instead: +/// ```python +/// import logging +/// +/// logging.basicConfig(format="%(user_id)s - %(message)s", level=logging.INFO) +/// +/// user = "Maria" +/// +/// logging.info("Something happened", extra=dict(user_id=user)) +/// ``` +/// +/// Or: +/// ```python +/// import logging +/// +/// logging.basicConfig(format="%(message)s", level=logging.INFO) +/// +/// user = "Maria" +/// +/// logging.info("%s - Something happened", user) +/// ``` +/// +/// ## References +/// - [Python documentation: `logging`](https://docs.python.org/3/library/logging.html) +/// - [Python documentation: Optimization](https://docs.python.org/3/howto/logging.html#optimization) #[violation] pub struct LoggingFString; @@ -41,6 +268,31 @@ impl Violation for LoggingFString { } } +/// ## What it does +/// Checks for uses of `logging.warn` and `logging.Logger.warn`. +/// +/// ## Why is this bad? +/// `logging.warn` and `logging.Logger.warn` are deprecated in favor of +/// `logging.warning` and `logging.Logger.warning`, which are functionally +/// equivalent. +/// +/// ## Example +/// ```python +/// import logging +/// +/// logging.warn("Something happened") +/// ``` +/// +/// Use instead: +/// ```python +/// import logging +/// +/// logging.warning("Something happened") +/// ``` +/// +/// ## References +/// - [Python documentation: `logging.warning`](https://docs.python.org/3/library/logging.html#logging.warning) +/// - [Python documentation: `logging.Logger.warning`](https://docs.python.org/3/library/logging.html#logging.Logger.warning) #[violation] pub struct LoggingWarn; @@ -55,6 +307,43 @@ impl AlwaysAutofixableViolation for LoggingWarn { } } +/// ## What it does +/// Checks for `extra` keywords in logging statements that clash with +/// `LogRecord` attributes. +/// +/// ## Why is this bad? +/// The `logging` module provides a mechanism for passing additional values to +/// be logged using the `extra` keyword argument. These values are then passed +/// to the `LogRecord` constructor. +/// +/// Providing a value via `extra` that clashes with one of the attributes of +/// the `LogRecord` constructor will raise a `KeyError` when the `LogRecord` is +/// constructed. +/// +/// ## Example +/// ```python +/// import logging +/// +/// logging.basicConfig(format="%(name) - %(message)s", level=logging.INFO) +/// +/// username = "Maria" +/// +/// logging.info("Something happened", extra=dict(name=username)) +/// ``` +/// +/// Use instead: +/// ```python +/// import logging +/// +/// logging.basicConfig(format="%(user_id)s - %(message)s", level=logging.INFO) +/// +/// username = "Maria" +/// +/// logging.info("Something happened", extra=dict(user=username)) +/// ``` +/// +/// ## References +/// - [Python documentation: LogRecord attributes](https://docs.python.org/3/library/logging.html#logrecord-attributes) #[violation] pub struct LoggingExtraAttrClash(pub String); @@ -63,11 +352,44 @@ impl Violation for LoggingExtraAttrClash { fn message(&self) -> String { let LoggingExtraAttrClash(key) = self; format!( - "Logging statement uses an extra field that clashes with a LogRecord field: `{key}`" + "Logging statement uses an `extra` field that clashes with a `LogRecord` field: `{key}`" ) } } +/// ## What it does +/// Checks for uses of `logging.error` that pass `exc_info=True`. +/// +/// ## Why is this bad? +/// Calling `logging.error` with `exc_info=True` is equivalent to calling +/// `logging.exception`. Using `logging.exception` is more concise, more +/// readable, and conveys the intent of the logging statement more clearly. +/// +/// ## Example +/// ```python +/// import logging +/// +/// try: +/// ... +/// except ValueError: +/// logging.error("Exception occurred", exc_info=True) +/// ``` +/// +/// Use instead: +/// ```python +/// import logging +/// +/// try: +/// ... +/// except ValueError: +/// logging.exception("Exception occurred") +/// ``` +/// +/// ## References +/// - [Python documentation: `logging.exception`](https://docs.python.org/3/library/logging.html#logging.exception) +/// - [Python documentation: `exception`](https://docs.python.org/3/library/logging.html#logging.Logger.exception) +/// - [Python documentation: `logging.error`](https://docs.python.org/3/library/logging.html#logging.error) +/// - [Python documentation: `error`](https://docs.python.org/3/library/logging.html#logging.Logger.error) #[violation] pub struct LoggingExcInfo; @@ -78,6 +400,41 @@ impl Violation for LoggingExcInfo { } } +/// ## What it does +/// Checks for redundant `exc_info` keyword arguments in logging statements. +/// +/// ## Why is this bad? +/// `exc_info` is `True` by default for `logging.exception`, and `False` by +/// default for `logging.error`. +/// +/// Passing `exc_info=True` to `logging.exception` calls is redundant, as is +/// passing `exc_info=False` to `logging.error` calls. +/// +/// ## Example +/// ```python +/// import logging +/// +/// try: +/// ... +/// except ValueError: +/// logging.exception("Exception occurred", exc_info=True) +/// ``` +/// +/// Use instead: +/// ```python +/// import logging +/// +/// try: +/// ... +/// except ValueError: +/// logging.exception("Exception occurred") +/// ``` +/// +/// ## References +/// - [Python documentation: `logging.exception`](https://docs.python.org/3/library/logging.html#logging.exception) +/// - [Python documentation: `exception`](https://docs.python.org/3/library/logging.html#logging.Logger.exception) +/// - [Python documentation: `logging.error`](https://docs.python.org/3/library/logging.html#logging.error) +/// - [Python documentation: `error`](https://docs.python.org/3/library/logging.html#logging.Logger.error) #[violation] pub struct LoggingRedundantExcInfo; diff --git a/crates/ruff/src/rules/flake8_no_pep420/rules/mod.rs b/crates/ruff/src/rules/flake8_no_pep420/rules/mod.rs index 6b3762cae3..060a6879aa 100644 --- a/crates/ruff/src/rules/flake8_no_pep420/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_no_pep420/rules/mod.rs @@ -1,3 +1,3 @@ -pub(crate) use implicit_namespace_package::{implicit_namespace_package, ImplicitNamespacePackage}; +pub(crate) use implicit_namespace_package::*; mod implicit_namespace_package; diff --git a/crates/ruff/src/rules/flake8_pie/rules/duplicate_class_field_definition.rs b/crates/ruff/src/rules/flake8_pie/rules/duplicate_class_field_definition.rs index ce0991ac98..6351778925 100644 --- a/crates/ruff/src/rules/flake8_pie/rules/duplicate_class_field_definition.rs +++ b/crates/ruff/src/rules/flake8_pie/rules/duplicate_class_field_definition.rs @@ -93,7 +93,6 @@ pub(crate) fn duplicate_class_field_definition<'a, 'b>( Some(parent), checker.locator, checker.indexer, - checker.stylist, ); diagnostic.set_fix(Fix::suggested(edit).isolate(checker.isolation(Some(parent)))); } diff --git a/crates/ruff/src/rules/flake8_pie/rules/mod.rs b/crates/ruff/src/rules/flake8_pie/rules/mod.rs index 75a94605ee..e86cd7a19d 100644 --- a/crates/ruff/src/rules/flake8_pie/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_pie/rules/mod.rs @@ -1,12 +1,10 @@ -pub(crate) use duplicate_class_field_definition::{ - duplicate_class_field_definition, DuplicateClassFieldDefinition, -}; -pub(crate) use multiple_starts_ends_with::{multiple_starts_ends_with, MultipleStartsEndsWith}; -pub(crate) use no_unnecessary_pass::{no_unnecessary_pass, UnnecessaryPass}; -pub(crate) use non_unique_enums::{non_unique_enums, NonUniqueEnums}; -pub(crate) use reimplemented_list_builtin::{reimplemented_list_builtin, ReimplementedListBuiltin}; -pub(crate) use unnecessary_dict_kwargs::{unnecessary_dict_kwargs, UnnecessaryDictKwargs}; -pub(crate) use unnecessary_spread::{unnecessary_spread, UnnecessarySpread}; +pub(crate) use duplicate_class_field_definition::*; +pub(crate) use multiple_starts_ends_with::*; +pub(crate) use no_unnecessary_pass::*; +pub(crate) use non_unique_enums::*; +pub(crate) use reimplemented_list_builtin::*; +pub(crate) use unnecessary_dict_kwargs::*; +pub(crate) use unnecessary_spread::*; mod duplicate_class_field_definition; mod multiple_starts_ends_with; diff --git a/crates/ruff/src/rules/flake8_pie/rules/multiple_starts_ends_with.rs b/crates/ruff/src/rules/flake8_pie/rules/multiple_starts_ends_with.rs index f00986155f..05893da8e5 100644 --- a/crates/ruff/src/rules/flake8_pie/rules/multiple_starts_ends_with.rs +++ b/crates/ruff/src/rules/flake8_pie/rules/multiple_starts_ends_with.rs @@ -5,7 +5,7 @@ use itertools::Either::{Left, Right}; use ruff_text_size::TextRange; -use rustpython_parser::ast::{self, Boolop, Expr, ExprContext, Ranged}; +use rustpython_parser::ast::{self, BoolOp, Expr, ExprContext, Identifier, Ranged}; use ruff_diagnostics::AlwaysAutofixableViolation; use ruff_diagnostics::{Diagnostic, Edit, Fix}; @@ -38,8 +38,8 @@ use crate::registry::AsRule; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/stdtypes.html#str.startswith) -/// - [Python documentation](https://docs.python.org/3/library/stdtypes.html#str.endswith) +/// - [Python documentation: `str.startswith`](https://docs.python.org/3/library/stdtypes.html#str.startswith) +/// - [Python documentation: `str.endswith`](https://docs.python.org/3/library/stdtypes.html#str.endswith) #[violation] pub struct MultipleStartsEndsWith { attr: String, @@ -60,7 +60,12 @@ impl AlwaysAutofixableViolation for MultipleStartsEndsWith { /// PIE810 pub(crate) fn multiple_starts_ends_with(checker: &mut Checker, expr: &Expr) { - let Expr::BoolOp(ast::ExprBoolOp { op: Boolop::Or, values, range: _ }) = expr else { + let Expr::BoolOp(ast::ExprBoolOp { + op: BoolOp::Or, + values, + range: _, + }) = expr + else { return; }; @@ -70,24 +75,25 @@ pub(crate) fn multiple_starts_ends_with(checker: &mut Checker, expr: &Expr) { func, args, keywords, - range: _ - }) = &call else { - continue + range: _, + }) = &call + else { + continue; }; if !(args.len() == 1 && keywords.is_empty()) { continue; } - let Expr::Attribute(ast::ExprAttribute { value, attr, .. } )= func.as_ref() else { - continue + let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = func.as_ref() else { + continue; }; if attr != "startswith" && attr != "endswith" { continue; } - let Expr::Name(ast::ExprName { id: arg_name, .. } )= value.as_ref() else { - continue + let Expr::Name(ast::ExprName { id: arg_name, .. }) = value.as_ref() else { + continue; }; duplicates @@ -110,8 +116,17 @@ pub(crate) fn multiple_starts_ends_with(checker: &mut Checker, expr: &Expr) { .iter() .map(|index| &values[*index]) .map(|expr| { - let Expr::Call(ast::ExprCall { func: _, args, keywords: _, range: _}) = expr else { - unreachable!("{}", format!("Indices should only contain `{attr_name}` calls")) + let Expr::Call(ast::ExprCall { + func: _, + args, + keywords: _, + range: _, + }) = expr + else { + unreachable!( + "{}", + format!("Indices should only contain `{attr_name}` calls") + ) }; args.get(0) .unwrap_or_else(|| panic!("`{attr_name}` should have one argument")) @@ -140,7 +155,7 @@ pub(crate) fn multiple_starts_ends_with(checker: &mut Checker, expr: &Expr) { }); let node2 = Expr::Attribute(ast::ExprAttribute { value: Box::new(node1), - attr: attr_name.into(), + attr: Identifier::new(attr_name.to_string(), TextRange::default()), ctx: ExprContext::Load, range: TextRange::default(), }); @@ -155,7 +170,7 @@ pub(crate) fn multiple_starts_ends_with(checker: &mut Checker, expr: &Expr) { // Generate the combined `BoolOp`. let mut call = Some(call); let node = Expr::BoolOp(ast::ExprBoolOp { - op: Boolop::Or, + op: BoolOp::Or, values: values .iter() .enumerate() diff --git a/crates/ruff/src/rules/flake8_pie/rules/no_unnecessary_pass.rs b/crates/ruff/src/rules/flake8_pie/rules/no_unnecessary_pass.rs index c688376d7f..1cc40d052e 100644 --- a/crates/ruff/src/rules/flake8_pie/rules/no_unnecessary_pass.rs +++ b/crates/ruff/src/rules/flake8_pie/rules/no_unnecessary_pass.rs @@ -32,7 +32,7 @@ use crate::registry::AsRule; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/reference/simple_stmts.html#the-pass-statement) +/// - [Python documentation: The `pass` statement](https://docs.python.org/3/reference/simple_stmts.html#the-pass-statement) #[violation] pub struct UnnecessaryPass; @@ -67,13 +67,7 @@ pub(crate) fn no_unnecessary_pass(checker: &mut Checker, body: &[Stmt]) { let edit = if let Some(index) = trailing_comment_start_offset(stmt, checker.locator) { Edit::range_deletion(stmt.range().add_end(index)) } else { - autofix::edits::delete_stmt( - stmt, - None, - checker.locator, - checker.indexer, - checker.stylist, - ) + autofix::edits::delete_stmt(stmt, None, checker.locator, checker.indexer) }; diagnostic.set_fix(Fix::automatic(edit)); } diff --git a/crates/ruff/src/rules/flake8_pie/rules/non_unique_enums.rs b/crates/ruff/src/rules/flake8_pie/rules/non_unique_enums.rs index 47e1a427a5..63a5664bad 100644 --- a/crates/ruff/src/rules/flake8_pie/rules/non_unique_enums.rs +++ b/crates/ruff/src/rules/flake8_pie/rules/non_unique_enums.rs @@ -38,7 +38,7 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/enum.html#enum.Enum) +/// - [Python documentation: `enum.Enum`](https://docs.python.org/3/library/enum.html#enum.Enum) #[violation] pub struct NonUniqueEnums { value: String, @@ -66,9 +66,11 @@ pub(crate) fn non_unique_enums<'a, 'b>( if !bases.iter().any(|expr| { checker - .semantic_model() + .semantic() .resolve_call_path(expr) - .map_or(false, |call_path| call_path.as_slice() == ["enum", "Enum"]) + .map_or(false, |call_path| { + matches!(call_path.as_slice(), ["enum", "Enum"]) + }) }) { return; } @@ -81,9 +83,11 @@ pub(crate) fn non_unique_enums<'a, 'b>( if let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() { if checker - .semantic_model() + .semantic() .resolve_call_path(func) - .map_or(false, |call_path| call_path.as_slice() == ["enum", "auto"]) + .map_or(false, |call_path| { + matches!(call_path.as_slice(), ["enum", "auto"]) + }) { continue; } diff --git a/crates/ruff/src/rules/flake8_pie/rules/reimplemented_list_builtin.rs b/crates/ruff/src/rules/flake8_pie/rules/reimplemented_list_builtin.rs index c7e562a36a..e2b68792eb 100644 --- a/crates/ruff/src/rules/flake8_pie/rules/reimplemented_list_builtin.rs +++ b/crates/ruff/src/rules/flake8_pie/rules/reimplemented_list_builtin.rs @@ -34,7 +34,7 @@ use crate::registry::AsRule; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/functions.html#func-list) +/// - [Python documentation: `list`](https://docs.python.org/3/library/functions.html#func-list) #[violation] pub struct ReimplementedListBuiltin; @@ -69,7 +69,7 @@ pub(crate) fn reimplemented_list_builtin(checker: &mut Checker, expr: &ExprLambd if elts.is_empty() { let mut diagnostic = Diagnostic::new(ReimplementedListBuiltin, expr.range()); if checker.patch(diagnostic.kind.rule()) { - if checker.semantic_model().is_builtin("list") { + if checker.semantic().is_builtin("list") { diagnostic.set_fix(Fix::automatic(Edit::range_replacement( "list".to_string(), expr.range(), diff --git a/crates/ruff/src/rules/flake8_pie/rules/unnecessary_dict_kwargs.rs b/crates/ruff/src/rules/flake8_pie/rules/unnecessary_dict_kwargs.rs index b621d1ff5f..5fedf58108 100644 --- a/crates/ruff/src/rules/flake8_pie/rules/unnecessary_dict_kwargs.rs +++ b/crates/ruff/src/rules/flake8_pie/rules/unnecessary_dict_kwargs.rs @@ -34,8 +34,8 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/reference/expressions.html#dictionary-displays) -/// - [Python documentation](https://docs.python.org/3/reference/expressions.html#calls) +/// - [Python documentation: Dictionary displays](https://docs.python.org/3/reference/expressions.html#dictionary-displays) +/// - [Python documentation: Calls](https://docs.python.org/3/reference/expressions.html#calls) #[violation] pub struct UnnecessaryDictKwargs; diff --git a/crates/ruff/src/rules/flake8_pie/rules/unnecessary_spread.rs b/crates/ruff/src/rules/flake8_pie/rules/unnecessary_spread.rs index 855d4ea26d..278d23d07c 100644 --- a/crates/ruff/src/rules/flake8_pie/rules/unnecessary_spread.rs +++ b/crates/ruff/src/rules/flake8_pie/rules/unnecessary_spread.rs @@ -26,7 +26,7 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/reference/expressions.html#dictionary-displays) +/// - [Python documentation: Dictionary displays](https://docs.python.org/3/reference/expressions.html#dictionary-displays) #[violation] pub struct UnnecessarySpread; diff --git a/crates/ruff/src/rules/flake8_print/rules/mod.rs b/crates/ruff/src/rules/flake8_print/rules/mod.rs index e044fe39e9..954fd75b9e 100644 --- a/crates/ruff/src/rules/flake8_print/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_print/rules/mod.rs @@ -1,3 +1,3 @@ -pub(crate) use print_call::{print_call, PPrint, Print}; +pub(crate) use print_call::*; mod print_call; diff --git a/crates/ruff/src/rules/flake8_print/rules/print_call.rs b/crates/ruff/src/rules/flake8_print/rules/print_call.rs index 13993a202c..a3c9bba035 100644 --- a/crates/ruff/src/rules/flake8_print/rules/print_call.rs +++ b/crates/ruff/src/rules/flake8_print/rules/print_call.rs @@ -78,11 +78,10 @@ impl Violation for PPrint { /// T201, T203 pub(crate) fn print_call(checker: &mut Checker, func: &Expr, keywords: &[Keyword]) { let diagnostic = { - let call_path = checker.semantic_model().resolve_call_path(func); - if call_path - .as_ref() - .map_or(false, |call_path| *call_path.as_slice() == ["", "print"]) - { + let call_path = checker.semantic().resolve_call_path(func); + if call_path.as_ref().map_or(false, |call_path| { + matches!(call_path.as_slice(), ["", "print"]) + }) { // If the print call has a `file=` argument (that isn't `None`, `"sys.stdout"`, // or `"sys.stderr"`), don't trigger T201. if let Some(keyword) = keywords @@ -90,21 +89,20 @@ pub(crate) fn print_call(checker: &mut Checker, func: &Expr, keywords: &[Keyword .find(|keyword| keyword.arg.as_ref().map_or(false, |arg| arg == "file")) { if !is_const_none(&keyword.value) { - if checker - .semantic_model() - .resolve_call_path(&keyword.value) - .map_or(true, |call_path| { + if checker.semantic().resolve_call_path(&keyword.value).map_or( + true, + |call_path| { call_path.as_slice() != ["sys", "stdout"] && call_path.as_slice() != ["sys", "stderr"] - }) - { + }, + ) { return; } } } Diagnostic::new(Print, func.range()) } else if call_path.as_ref().map_or(false, |call_path| { - *call_path.as_slice() == ["pprint", "pprint"] + matches!(call_path.as_slice(), ["pprint", "pprint"]) }) { Diagnostic::new(PPrint, func.range()) } else { diff --git a/crates/ruff/src/rules/flake8_pyi/helpers.rs b/crates/ruff/src/rules/flake8_pyi/helpers.rs new file mode 100644 index 0000000000..0f37e94470 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/helpers.rs @@ -0,0 +1,54 @@ +use ruff_python_semantic::SemanticModel; +use rustpython_parser::ast::{self, Expr, Operator}; + +/// Traverse a "union" type annotation, applying `func` to each union member. +/// Supports traversal of `Union` and `|` union expressions. +/// The function is called with each expression in the union (excluding declarations of nested unions) +/// and the parent expression (if any). +pub(super) fn traverse_union<'a, F>( + func: &mut F, + semantic: &SemanticModel, + expr: &'a Expr, + parent: Option<&'a Expr>, +) where + F: FnMut(&'a Expr, Option<&'a Expr>), +{ + // Ex) x | y + if let Expr::BinOp(ast::ExprBinOp { + op: Operator::BitOr, + left, + right, + range: _, + }) = expr + { + // The union data structure usually looks like this: + // a | b | c -> (a | b) | c + // + // However, parenthesized expressions can coerce it into any structure: + // a | (b | c) + // + // So we have to traverse both branches in order (left, then right), to report members + // in the order they appear in the source code. + + // Traverse the left then right arms + traverse_union(func, semantic, left, Some(expr)); + traverse_union(func, semantic, right, Some(expr)); + return; + } + + // Ex) Union[x, y] + if let Expr::Subscript(ast::ExprSubscript { value, slice, .. }) = expr { + if semantic.match_typing_expr(value, "Union") { + if let Expr::Tuple(ast::ExprTuple { elts, .. }) = slice.as_ref() { + // Traverse each element of the tuple within the union recursively to handle cases + // such as `Union[..., Union[...]] + elts.iter() + .for_each(|elt| traverse_union(func, semantic, elt, Some(expr))); + return; + } + } + } + + // Otherwise, call the function on expression + func(expr, parent); +} diff --git a/crates/ruff/src/rules/flake8_pyi/mod.rs b/crates/ruff/src/rules/flake8_pyi/mod.rs index 783018fba8..6b8e6e4264 100644 --- a/crates/ruff/src/rules/flake8_pyi/mod.rs +++ b/crates/ruff/src/rules/flake8_pyi/mod.rs @@ -1,4 +1,5 @@ //! Rules from [flake8-pyi](https://pypi.org/project/flake8-pyi/). +mod helpers; pub(crate) mod rules; #[cfg(test)] @@ -18,10 +19,14 @@ mod tests { #[test_case(Rule::ArgumentDefaultInStub, Path::new("PYI014.pyi"))] #[test_case(Rule::AssignmentDefaultInStub, Path::new("PYI015.py"))] #[test_case(Rule::AssignmentDefaultInStub, Path::new("PYI015.pyi"))] + #[test_case(Rule::BadExitAnnotation, Path::new("PYI036.py"))] + #[test_case(Rule::BadExitAnnotation, Path::new("PYI036.pyi"))] #[test_case(Rule::BadVersionInfoComparison, Path::new("PYI006.py"))] #[test_case(Rule::BadVersionInfoComparison, Path::new("PYI006.pyi"))] #[test_case(Rule::CollectionsNamedTuple, Path::new("PYI024.py"))] #[test_case(Rule::CollectionsNamedTuple, Path::new("PYI024.pyi"))] + #[test_case(Rule::ComplexIfStatementInStub, Path::new("PYI002.py"))] + #[test_case(Rule::ComplexIfStatementInStub, Path::new("PYI002.pyi"))] #[test_case(Rule::DocstringInStub, Path::new("PYI021.py"))] #[test_case(Rule::DocstringInStub, Path::new("PYI021.pyi"))] #[test_case(Rule::DuplicateUnionMember, Path::new("PYI016.py"))] @@ -50,10 +55,16 @@ mod tests { #[test_case(Rule::UnassignedSpecialVariableInStub, Path::new("PYI035.pyi"))] #[test_case(Rule::StrOrReprDefinedInStub, Path::new("PYI029.py"))] #[test_case(Rule::StrOrReprDefinedInStub, Path::new("PYI029.pyi"))] + #[test_case(Rule::UnnecessaryLiteralUnion, Path::new("PYI030.py"))] + #[test_case(Rule::UnnecessaryLiteralUnion, Path::new("PYI030.pyi"))] #[test_case(Rule::StubBodyMultipleStatements, Path::new("PYI048.py"))] #[test_case(Rule::StubBodyMultipleStatements, Path::new("PYI048.pyi"))] #[test_case(Rule::TSuffixedTypeAlias, Path::new("PYI043.py"))] #[test_case(Rule::TSuffixedTypeAlias, Path::new("PYI043.pyi"))] + #[test_case(Rule::FutureAnnotationsInStub, Path::new("PYI044.py"))] + #[test_case(Rule::FutureAnnotationsInStub, Path::new("PYI044.pyi"))] + #[test_case(Rule::PatchVersionComparison, Path::new("PYI004.py"))] + #[test_case(Rule::PatchVersionComparison, Path::new("PYI004.pyi"))] #[test_case(Rule::TypeCommentInStub, Path::new("PYI033.py"))] #[test_case(Rule::TypeCommentInStub, Path::new("PYI033.pyi"))] #[test_case(Rule::TypedArgumentDefaultInStub, Path::new("PYI011.py"))] @@ -70,6 +81,10 @@ mod tests { #[test_case(Rule::UnrecognizedPlatformCheck, Path::new("PYI007.pyi"))] #[test_case(Rule::UnrecognizedPlatformName, Path::new("PYI008.py"))] #[test_case(Rule::UnrecognizedPlatformName, Path::new("PYI008.pyi"))] + #[test_case(Rule::UnrecognizedVersionInfoCheck, Path::new("PYI003.py"))] + #[test_case(Rule::UnrecognizedVersionInfoCheck, Path::new("PYI003.pyi"))] + #[test_case(Rule::WrongTupleLengthVersionComparison, Path::new("PYI005.py"))] + #[test_case(Rule::WrongTupleLengthVersionComparison, Path::new("PYI005.pyi"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); let diagnostics = test_path( diff --git a/crates/ruff/src/rules/flake8_pyi/rules/any_eq_ne_annotation.rs b/crates/ruff/src/rules/flake8_pyi/rules/any_eq_ne_annotation.rs index 03235b31be..43a47e5c24 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/any_eq_ne_annotation.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/any_eq_ne_annotation.rs @@ -33,7 +33,7 @@ use crate::registry::AsRule; /// ... /// ``` /// ## References -/// - [Python documentation](https://docs.python.org/3/library/typing.html#the-any-type) +/// - [Python documentation: The `Any` type](https://docs.python.org/3/library/typing.html#the-any-type) /// - [Mypy documentation](https://mypy.readthedocs.io/en/latest/dynamic_typing.html#any-vs-object) #[violation] pub struct AnyEqNeAnnotation { @@ -62,18 +62,15 @@ pub(crate) fn any_eq_ne_annotation(checker: &mut Checker, name: &str, args: &Arg return; } - let Some(annotation) = &args.args[1].annotation else { + let Some(annotation) = &args.args[1].def.annotation else { return; }; - if !checker.semantic_model().scope().kind.is_class() { + if !checker.semantic().scope().kind.is_class() { return; } - if checker - .semantic_model() - .match_typing_expr(annotation, "Any") - { + if checker.semantic().match_typing_expr(annotation, "Any") { let mut diagnostic = Diagnostic::new( AnyEqNeAnnotation { method_name: name.to_string(), @@ -82,7 +79,7 @@ pub(crate) fn any_eq_ne_annotation(checker: &mut Checker, name: &str, args: &Arg ); if checker.patch(diagnostic.kind.rule()) { // Ex) `def __eq__(self, obj: Any): ...` - if checker.semantic_model().is_builtin("object") { + if checker.semantic().is_builtin("object") { diagnostic.set_fix(Fix::automatic(Edit::range_replacement( "object".to_string(), annotation.range(), diff --git a/crates/ruff/src/rules/flake8_pyi/rules/bad_version_info_comparison.rs b/crates/ruff/src/rules/flake8_pyi/rules/bad_version_info_comparison.rs index 8d923d1cfa..332ddfc5f7 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/bad_version_info_comparison.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/bad_version_info_comparison.rs @@ -1,4 +1,4 @@ -use rustpython_parser::ast::{Cmpop, Expr, Ranged}; +use rustpython_parser::ast::{self, CmpOp, Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -6,7 +6,7 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; /// ## What it does -/// Checks for usages of comparators other than `<` and `>=` for +/// Checks for uses of comparators other than `<` and `>=` for /// `sys.version_info` checks in `.pyi` files. All other comparators, such /// as `>`, `<=`, and `==`, are banned. /// @@ -52,34 +52,41 @@ pub struct BadVersionInfoComparison; impl Violation for BadVersionInfoComparison { #[derive_message_formats] fn message(&self) -> String { - format!("Use `<` or `>=` for version info comparisons") + format!("Use `<` or `>=` for `sys.version_info` comparisons") } } /// PYI006 -pub(crate) fn bad_version_info_comparison( - checker: &mut Checker, - expr: &Expr, - left: &Expr, - ops: &[Cmpop], - comparators: &[Expr], -) { - let ([op], [_right]) = (ops, comparators) else { +pub(crate) fn bad_version_info_comparison(checker: &mut Checker, test: &Expr) { + let Expr::Compare(ast::ExprCompare { + left, + ops, + comparators, + .. + }) = test + else { + return; + }; + + let ([op], [_right]) = (ops.as_slice(), comparators.as_slice()) else { return; }; if !checker - .semantic_model() + .semantic() .resolve_call_path(left) .map_or(false, |call_path| { - call_path.as_slice() == ["sys", "version_info"] + matches!(call_path.as_slice(), ["sys", "version_info"]) }) { return; } - if !matches!(op, Cmpop::Lt | Cmpop::GtE) { - let diagnostic = Diagnostic::new(BadVersionInfoComparison, expr.range()); - checker.diagnostics.push(diagnostic); + if matches!(op, CmpOp::Lt | CmpOp::GtE) { + return; } + + checker + .diagnostics + .push(Diagnostic::new(BadVersionInfoComparison, test.range())); } diff --git a/crates/ruff/src/rules/flake8_pyi/rules/collections_named_tuple.rs b/crates/ruff/src/rules/flake8_pyi/rules/collections_named_tuple.rs index 7f171b7558..d9d32e81f2 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/collections_named_tuple.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/collections_named_tuple.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::Expr; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::prelude::Ranged; +use rustpython_parser::ast::Ranged; use crate::checkers::ast::Checker; @@ -51,7 +51,7 @@ impl Violation for CollectionsNamedTuple { /// PYI024 pub(crate) fn collections_named_tuple(checker: &mut Checker, expr: &Expr) { if checker - .semantic_model() + .semantic() .resolve_call_path(expr) .map_or(false, |call_path| { matches!(call_path.as_slice(), ["collections", "namedtuple"]) diff --git a/crates/ruff/src/rules/flake8_pyi/rules/complex_if_statement_in_stub.rs b/crates/ruff/src/rules/flake8_pyi/rules/complex_if_statement_in_stub.rs new file mode 100644 index 0000000000..2c7b1a5cfb --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/rules/complex_if_statement_in_stub.rs @@ -0,0 +1,80 @@ +use rustpython_parser::ast::{self, Expr, Ranged}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; + +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for `if` statements with complex conditionals in stubs. +/// +/// ## Why is this bad? +/// Stub files support simple conditionals to test for differences in Python +/// versions and platforms. However, type checkers only understand a limited +/// subset of these conditionals; complex conditionals may result in false +/// positives or false negatives. +/// +/// ## Example +/// ```python +/// import sys +/// +/// if (2, 7) < sys.version_info < (3, 5): +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// import sys +/// +/// if sys.version_info < (3, 5): +/// ... +/// ``` +#[violation] +pub struct ComplexIfStatementInStub; + +impl Violation for ComplexIfStatementInStub { + #[derive_message_formats] + fn message(&self) -> String { + format!( + "`if` test must be a simple comparison against `sys.platform` or `sys.version_info`" + ) + } +} + +/// PYI002 +pub(crate) fn complex_if_statement_in_stub(checker: &mut Checker, test: &Expr) { + let Expr::Compare(ast::ExprCompare { + left, comparators, .. + }) = test + else { + checker + .diagnostics + .push(Diagnostic::new(ComplexIfStatementInStub, test.range())); + return; + }; + + if comparators.len() != 1 { + checker + .diagnostics + .push(Diagnostic::new(ComplexIfStatementInStub, test.range())); + return; + } + + if left.is_subscript_expr() { + return; + } + + if checker + .semantic() + .resolve_call_path(left) + .map_or(false, |call_path| { + matches!(call_path.as_slice(), ["sys", "version_info" | "platform"]) + }) + { + return; + } + + checker + .diagnostics + .push(Diagnostic::new(ComplexIfStatementInStub, test.range())); +} diff --git a/crates/ruff/src/rules/flake8_pyi/rules/duplicate_union_member.rs b/crates/ruff/src/rules/flake8_pyi/rules/duplicate_union_member.rs index cdf2987c19..10f111ad10 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/duplicate_union_member.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/duplicate_union_member.rs @@ -1,90 +1,77 @@ use rustc_hash::FxHashSet; -use rustpython_parser::ast::{self, Expr, Operator, Ranged}; - -use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; -use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::comparable::ComparableExpr; +use rustpython_parser::ast::{self, Expr, Ranged}; +use std::collections::HashSet; use crate::checkers::ast::Checker; use crate::registry::AsRule; +use crate::rules::flake8_pyi::helpers::traverse_union; +use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::comparable::ComparableExpr; #[violation] pub struct DuplicateUnionMember { duplicate_name: String, } -impl AlwaysAutofixableViolation for DuplicateUnionMember { +impl Violation for DuplicateUnionMember { + const AUTOFIX: AutofixKind = AutofixKind::Sometimes; + #[derive_message_formats] fn message(&self) -> String { format!("Duplicate union member `{}`", self.duplicate_name) } - fn autofix_title(&self) -> String { - format!("Remove duplicate union member `{}`", self.duplicate_name) + fn autofix_title(&self) -> Option { + Some(format!( + "Remove duplicate union member `{}`", + self.duplicate_name + )) } } /// PYI016 -pub(crate) fn duplicate_union_member(checker: &mut Checker, expr: &Expr) { - let mut seen_nodes = FxHashSet::default(); - traverse_union(&mut seen_nodes, checker, expr, None); -} +pub(crate) fn duplicate_union_member<'a>(checker: &mut Checker, expr: &'a Expr) { + let mut seen_nodes: HashSet, _> = FxHashSet::default(); + let mut diagnostics: Vec = Vec::new(); -fn traverse_union<'a>( - seen_nodes: &mut FxHashSet>, - checker: &mut Checker, - expr: &'a Expr, - parent: Option<&'a Expr>, -) { - // The union data structure usually looks like this: - // a | b | c -> (a | b) | c - // - // However, parenthesized expressions can coerce it into any structure: - // a | (b | c) - // - // So we have to traverse both branches in order (left, then right), to report duplicates - // in the order they appear in the source code. - if let Expr::BinOp(ast::ExprBinOp { - op: Operator::BitOr, - left, - right, - range: _, - }) = expr - { - // Traverse left subtree, then the right subtree, propagating the previous node. - traverse_union(seen_nodes, checker, left, Some(expr)); - traverse_union(seen_nodes, checker, right, Some(expr)); - } + // Adds a member to `literal_exprs` if it is a `Literal` annotation + let mut check_for_duplicate_members = |expr: &'a Expr, parent: Option<&'a Expr>| { + // If we've already seen this union member, raise a violation. + if !seen_nodes.insert(expr.into()) { + let mut diagnostic = Diagnostic::new( + DuplicateUnionMember { + duplicate_name: checker.generator().expr(expr), + }, + expr.range(), + ); + if checker.patch(diagnostic.kind.rule()) { + // Delete the "|" character as well as the duplicate value by reconstructing the + // parent without the duplicate. - // If we've already seen this union member, raise a violation. - if !seen_nodes.insert(expr.into()) { - let mut diagnostic = Diagnostic::new( - DuplicateUnionMember { - duplicate_name: checker.generator().expr(expr), - }, - expr.range(), - ); - if checker.patch(diagnostic.kind.rule()) { - // Delete the "|" character as well as the duplicate value by reconstructing the - // parent without the duplicate. - - // SAFETY: impossible to have a duplicate without a `parent` node. - let parent = parent.expect("Parent node must exist"); - - // SAFETY: Parent node must have been a `BinOp` in order for us to have traversed it. - let Expr::BinOp(ast::ExprBinOp { left, right, .. }) = parent else { - panic!("Parent node must be a BinOp"); - }; - - // Replace the parent with its non-duplicate child. - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( - checker - .generator() - .expr(if expr == left.as_ref() { right } else { left }), - parent.range(), - ))); + // If the parent node is not a `BinOp` we will not perform a fix + if let Some(Expr::BinOp(ast::ExprBinOp { left, right, .. })) = parent { + // Replace the parent with its non-duplicate child. + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( + checker + .generator() + .expr(if expr == left.as_ref() { right } else { left }), + parent.unwrap().range(), + ))); + } + } + diagnostics.push(diagnostic); } - checker.diagnostics.push(diagnostic); - } + }; + + // Traverse the union, collect all diagnostic members + traverse_union( + &mut check_for_duplicate_members, + checker.semantic(), + expr, + None, + ); + + // Add all diagnostics to the checker + checker.diagnostics.append(&mut diagnostics); } diff --git a/crates/ruff/src/rules/flake8_pyi/rules/ellipsis_in_non_empty_class_body.rs b/crates/ruff/src/rules/flake8_pyi/rules/ellipsis_in_non_empty_class_body.rs index 3648d27345..5239611c0f 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/ellipsis_in_non_empty_class_body.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/ellipsis_in_non_empty_class_body.rs @@ -69,13 +69,8 @@ pub(crate) fn ellipsis_in_non_empty_class_body<'a>( let mut diagnostic = Diagnostic::new(EllipsisInNonEmptyClassBody, stmt.range()); if checker.patch(diagnostic.kind.rule()) { - let edit = autofix::edits::delete_stmt( - stmt, - Some(parent), - checker.locator, - checker.indexer, - checker.stylist, - ); + let edit = + autofix::edits::delete_stmt(stmt, Some(parent), checker.locator, checker.indexer); diagnostic.set_fix(Fix::automatic(edit).isolate(checker.isolation(Some(parent)))); } checker.diagnostics.push(diagnostic); diff --git a/crates/ruff/src/rules/flake8_pyi/rules/exit_annotations.rs b/crates/ruff/src/rules/flake8_pyi/rules/exit_annotations.rs new file mode 100644 index 0000000000..69887d75e3 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/rules/exit_annotations.rs @@ -0,0 +1,351 @@ +use std::fmt::{Display, Formatter}; + +use rustpython_parser::ast::{ + ArgWithDefault, Arguments, Expr, ExprBinOp, ExprSubscript, ExprTuple, Identifier, Operator, + Ranged, +}; +use smallvec::SmallVec; + +use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers::is_const_none; +use ruff_python_semantic::SemanticModel; + +use crate::checkers::ast::Checker; +use crate::registry::AsRule; + +/// ## What it does +/// Checks for incorrect function signatures on `__exit__` and `__aexit__` +/// methods. +/// +/// ## Why is this bad? +/// Improperly-annotated `__exit__` and `__aexit__` methods can cause +/// unexpected behavior when interacting with type checkers. +/// +/// ## Example +/// ```python +/// class Foo: +/// def __exit__(self, typ, exc, tb, extra_arg) -> None: +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// class Foo: +/// def __exit__( +/// self, +/// typ: type[BaseException] | None, +/// exc: BaseException | None, +/// tb: TracebackType | None, +/// extra_arg: int = 0, +/// ) -> None: +/// ... +/// ``` +#[violation] +pub struct BadExitAnnotation { + func_kind: FuncKind, + error_kind: ErrorKind, +} + +impl Violation for BadExitAnnotation { + const AUTOFIX: AutofixKind = AutofixKind::Sometimes; + + #[derive_message_formats] + fn message(&self) -> String { + let method_name = self.func_kind.to_string(); + match self.error_kind { + ErrorKind::StarArgsNotAnnotated => format!("Star-args in `{method_name}` should be annotated with `object`"), + ErrorKind::MissingArgs => format!("If there are no star-args, `{method_name}` should have at least 3 non-keyword-only args (excluding `self`)"), + ErrorKind::ArgsAfterFirstFourMustHaveDefault => format!("All arguments after the first four in `{method_name}` must have a default value"), + ErrorKind::AllKwargsMustHaveDefault => format!("All keyword-only arguments in `{method_name}` must have a default value"), + ErrorKind::FirstArgBadAnnotation => format!("The first argument in `{method_name}` should be annotated with `object` or `type[BaseException] | None`"), + ErrorKind::SecondArgBadAnnotation => format!("The second argument in `{method_name}` should be annotated with `object` or `BaseException | None`"), + ErrorKind::ThirdArgBadAnnotation => format!("The third argument in `{method_name}` should be annotated with `object` or `types.TracebackType | None`"), + } + } + + fn autofix_title(&self) -> Option { + if matches!(self.error_kind, ErrorKind::StarArgsNotAnnotated) { + Some("Annotate star-args with `object`".to_string()) + } else { + None + } + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +enum FuncKind { + Sync, + Async, +} + +impl Display for FuncKind { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + FuncKind::Sync => write!(f, "__exit__"), + FuncKind::Async => write!(f, "__aexit__"), + } + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +enum ErrorKind { + StarArgsNotAnnotated, + MissingArgs, + FirstArgBadAnnotation, + SecondArgBadAnnotation, + ThirdArgBadAnnotation, + ArgsAfterFirstFourMustHaveDefault, + AllKwargsMustHaveDefault, +} + +/// PYI036 +pub(crate) fn bad_exit_annotation( + checker: &mut Checker, + is_async: bool, + name: &Identifier, + args: &Arguments, +) { + let func_kind = match name.as_str() { + "__exit__" if !is_async => FuncKind::Sync, + "__aexit__" if is_async => FuncKind::Async, + _ => return, + }; + + let positional_args = args + .args + .iter() + .chain(args.posonlyargs.iter()) + .collect::>(); + + // If there are less than three positional arguments, at least one of them must be a star-arg, + // and it must be annotated with `object`. + if positional_args.len() < 4 { + check_short_args_list(checker, args, func_kind); + } + + // Every positional argument (beyond the first four) must have a default. + for arg_with_default in positional_args + .iter() + .skip(4) + .filter(|arg_with_default| arg_with_default.default.is_none()) + { + checker.diagnostics.push(Diagnostic::new( + BadExitAnnotation { + func_kind, + error_kind: ErrorKind::ArgsAfterFirstFourMustHaveDefault, + }, + arg_with_default.range(), + )); + } + + // ...as should all keyword-only arguments. + for arg_with_default in args.kwonlyargs.iter().filter(|arg| arg.default.is_none()) { + checker.diagnostics.push(Diagnostic::new( + BadExitAnnotation { + func_kind, + error_kind: ErrorKind::AllKwargsMustHaveDefault, + }, + arg_with_default.range(), + )); + } + + check_positional_args(checker, &positional_args, func_kind); +} + +/// Determine whether a "short" argument list (i.e., an argument list with less than four elements) +/// contains a star-args argument annotated with `object`. If not, report an error. +fn check_short_args_list(checker: &mut Checker, args: &Arguments, func_kind: FuncKind) { + if let Some(varargs) = &args.vararg { + if let Some(annotation) = varargs + .annotation + .as_ref() + .filter(|ann| !is_object_or_unused(ann, checker.semantic())) + { + let mut diagnostic = Diagnostic::new( + BadExitAnnotation { + func_kind, + error_kind: ErrorKind::StarArgsNotAnnotated, + }, + annotation.range(), + ); + + if checker.patch(diagnostic.kind.rule()) { + if checker.semantic().is_builtin("object") { + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( + "object".to_string(), + annotation.range(), + ))); + } + } + + checker.diagnostics.push(diagnostic); + } + } else { + checker.diagnostics.push(Diagnostic::new( + BadExitAnnotation { + func_kind, + error_kind: ErrorKind::MissingArgs, + }, + args.range(), + )); + } +} + +/// Determines whether the positional arguments of an `__exit__` or `__aexit__` method are +/// annotated correctly. +fn check_positional_args( + checker: &mut Checker, + positional_args: &[&ArgWithDefault], + kind: FuncKind, +) { + // For each argument, define the predicate against which to check the annotation. + type AnnotationValidator = fn(&Expr, &SemanticModel) -> bool; + + let validations: [(ErrorKind, AnnotationValidator); 3] = [ + (ErrorKind::FirstArgBadAnnotation, is_base_exception_type), + (ErrorKind::SecondArgBadAnnotation, is_base_exception), + (ErrorKind::ThirdArgBadAnnotation, is_traceback_type), + ]; + + for (arg, (error_info, predicate)) in positional_args + .iter() + .skip(1) + .take(3) + .zip(validations.into_iter()) + { + let Some(annotation) = arg.def.annotation.as_ref() else { + continue; + }; + + if is_object_or_unused(annotation, checker.semantic()) { + continue; + } + + // If there's an annotation that's not `object` or `Unused`, check that the annotated type + // matches the predicate. + if non_none_annotation_element(annotation, checker.semantic()) + .map_or(false, |elem| predicate(elem, checker.semantic())) + { + continue; + } + + checker.diagnostics.push(Diagnostic::new( + BadExitAnnotation { + func_kind: kind, + error_kind: error_info, + }, + annotation.range(), + )); + } +} + +/// Return the non-`None` annotation element of a PEP 604-style union or `Optional` annotation. +fn non_none_annotation_element<'a>( + annotation: &'a Expr, + model: &SemanticModel, +) -> Option<&'a Expr> { + // E.g., `typing.Union` or `typing.Optional` + if let Expr::Subscript(ExprSubscript { value, slice, .. }) = annotation { + if model.match_typing_expr(value, "Optional") { + return if is_const_none(slice) { + None + } else { + Some(slice) + }; + } + + if !model.match_typing_expr(value, "Union") { + return None; + } + + let Expr::Tuple(ExprTuple { elts, .. }) = slice.as_ref() else { + return None; + }; + + let [left, right] = elts.as_slice() else { + return None; + }; + + return match (is_const_none(left), is_const_none(right)) { + (false, true) => Some(left), + (true, false) => Some(right), + (true, true) => None, + (false, false) => None, + }; + } + + // PEP 604-style union (e.g., `int | None`) + if let Expr::BinOp(ExprBinOp { + op: Operator::BitOr, + left, + right, + .. + }) = annotation + { + if !is_const_none(left) { + return Some(left); + } + + if !is_const_none(right) { + return Some(right); + } + + return None; + } + + None +} + +/// Return `true` if the [`Expr`] is the `object` builtin or the `_typeshed.Unused` type. +fn is_object_or_unused(expr: &Expr, model: &SemanticModel) -> bool { + model + .resolve_call_path(expr) + .as_ref() + .map_or(false, |call_path| { + matches!( + call_path.as_slice(), + ["" | "builtins", "object"] | ["_typeshed", "Unused"] + ) + }) +} + +/// Return `true` if the [`Expr`] is `BaseException`. +fn is_base_exception(expr: &Expr, model: &SemanticModel) -> bool { + model + .resolve_call_path(expr) + .as_ref() + .map_or(false, |call_path| { + matches!(call_path.as_slice(), ["" | "builtins", "BaseException"]) + }) +} + +/// Return `true` if the [`Expr`] is the `types.TracebackType` type. +fn is_traceback_type(expr: &Expr, model: &SemanticModel) -> bool { + model + .resolve_call_path(expr) + .as_ref() + .map_or(false, |call_path| { + matches!(call_path.as_slice(), ["types", "TracebackType"]) + }) +} + +/// Return `true` if the [`Expr`] is, e.g., `Type[BaseException]`. +fn is_base_exception_type(expr: &Expr, model: &SemanticModel) -> bool { + let Expr::Subscript(ExprSubscript { value, slice, .. }) = expr else { + return false; + }; + + if model.match_typing_expr(value, "Type") + || model + .resolve_call_path(value) + .as_ref() + .map_or(false, |call_path| { + matches!(call_path.as_slice(), ["" | "builtins", "type"]) + }) + { + is_base_exception(slice, model) + } else { + false + } +} diff --git a/crates/ruff/src/rules/flake8_pyi/rules/future_annotations_in_stub.rs b/crates/ruff/src/rules/flake8_pyi/rules/future_annotations_in_stub.rs new file mode 100644 index 0000000000..2968980034 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/rules/future_annotations_in_stub.rs @@ -0,0 +1,33 @@ +use rustpython_parser::ast::StmtImportFrom; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; + +use crate::checkers::ast::Checker; + +#[violation] +pub struct FutureAnnotationsInStub; + +impl Violation for FutureAnnotationsInStub { + #[derive_message_formats] + fn message(&self) -> String { + format!("`from __future__ import annotations` has no effect in stub files, since type checkers automatically treat stubs as having those semantics") + } +} + +/// PYI044 +pub(crate) fn from_future_import(checker: &mut Checker, target: &StmtImportFrom) { + if let StmtImportFrom { + range, + module: Some(name), + names, + .. + } = target + { + if name == "__future__" && names.iter().any(|alias| &*alias.name == "annotations") { + checker + .diagnostics + .push(Diagnostic::new(FutureAnnotationsInStub, *range)); + } + } +} diff --git a/crates/ruff/src/rules/flake8_pyi/rules/iter_method_return_iterable.rs b/crates/ruff/src/rules/flake8_pyi/rules/iter_method_return_iterable.rs index ce9b587379..e4971016b1 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/iter_method_return_iterable.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/iter_method_return_iterable.rs @@ -3,8 +3,8 @@ use rustpython_parser::ast::{Ranged, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::prelude::Expr; -use ruff_python_semantic::definition::{Definition, Member, MemberKind}; +use ruff_python_semantic::{Definition, Member, MemberKind}; +use rustpython_parser::ast::Expr; use crate::checkers::ast::Checker; @@ -70,15 +70,12 @@ pub(crate) fn iter_method_return_iterable(checker: &mut Checker, definition: &De kind: MemberKind::Method, stmt, .. - }) = definition else { + }) = definition + else { return; }; - let Stmt::FunctionDef(ast::StmtFunctionDef { - name, - returns, - .. - }) = stmt else { + let Stmt::FunctionDef(ast::StmtFunctionDef { name, returns, .. }) = stmt else { return; }; @@ -101,7 +98,7 @@ pub(crate) fn iter_method_return_iterable(checker: &mut Checker, definition: &De }; if checker - .semantic_model() + .semantic() .resolve_call_path(annotation) .map_or(false, |call_path| { if async_ { diff --git a/crates/ruff/src/rules/flake8_pyi/rules/mod.rs b/crates/ruff/src/rules/flake8_pyi/rules/mod.rs index a754a518d2..5fda64cb32 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/mod.rs @@ -1,54 +1,41 @@ -pub(crate) use any_eq_ne_annotation::{any_eq_ne_annotation, AnyEqNeAnnotation}; -pub(crate) use bad_version_info_comparison::{ - bad_version_info_comparison, BadVersionInfoComparison, -}; -pub(crate) use collections_named_tuple::{collections_named_tuple, CollectionsNamedTuple}; -pub(crate) use docstring_in_stubs::{docstring_in_stubs, DocstringInStub}; -pub(crate) use duplicate_union_member::{duplicate_union_member, DuplicateUnionMember}; -pub(crate) use ellipsis_in_non_empty_class_body::{ - ellipsis_in_non_empty_class_body, EllipsisInNonEmptyClassBody, -}; -pub(crate) use iter_method_return_iterable::{ - iter_method_return_iterable, IterMethodReturnIterable, -}; -pub(crate) use no_return_argument_annotation::{ - no_return_argument_annotation, NoReturnArgumentAnnotationInStub, -}; -pub(crate) use non_empty_stub_body::{non_empty_stub_body, NonEmptyStubBody}; -pub(crate) use non_self_return_type::{non_self_return_type, NonSelfReturnType}; -pub(crate) use numeric_literal_too_long::{numeric_literal_too_long, NumericLiteralTooLong}; -pub(crate) use pass_in_class_body::{pass_in_class_body, PassInClassBody}; -pub(crate) use pass_statement_stub_body::{pass_statement_stub_body, PassStatementStubBody}; -pub(crate) use prefix_type_params::{prefix_type_params, UnprefixedTypeParam}; -pub(crate) use quoted_annotation_in_stub::{quoted_annotation_in_stub, QuotedAnnotationInStub}; -pub(crate) use simple_defaults::{ - annotated_assignment_default_in_stub, argument_simple_defaults, assignment_default_in_stub, - typed_argument_simple_defaults, unannotated_assignment_in_stub, - unassigned_special_variable_in_stub, ArgumentDefaultInStub, AssignmentDefaultInStub, - TypedArgumentDefaultInStub, UnannotatedAssignmentInStub, UnassignedSpecialVariableInStub, -}; -pub(crate) use str_or_repr_defined_in_stub::{str_or_repr_defined_in_stub, StrOrReprDefinedInStub}; -pub(crate) use string_or_bytes_too_long::{string_or_bytes_too_long, StringOrBytesTooLong}; -pub(crate) use stub_body_multiple_statements::{ - stub_body_multiple_statements, StubBodyMultipleStatements, -}; -pub(crate) use type_alias_naming::{ - snake_case_type_alias, t_suffixed_type_alias, SnakeCaseTypeAlias, TSuffixedTypeAlias, -}; -pub(crate) use type_comment_in_stub::{type_comment_in_stub, TypeCommentInStub}; -pub(crate) use unaliased_collections_abc_set_import::{ - unaliased_collections_abc_set_import, UnaliasedCollectionsAbcSetImport, -}; -pub(crate) use unrecognized_platform::{ - unrecognized_platform, UnrecognizedPlatformCheck, UnrecognizedPlatformName, -}; +pub(crate) use any_eq_ne_annotation::*; +pub(crate) use bad_version_info_comparison::*; +pub(crate) use collections_named_tuple::*; +pub(crate) use complex_if_statement_in_stub::*; +pub(crate) use docstring_in_stubs::*; +pub(crate) use duplicate_union_member::*; +pub(crate) use ellipsis_in_non_empty_class_body::*; +pub(crate) use exit_annotations::*; +pub(crate) use future_annotations_in_stub::*; +pub(crate) use iter_method_return_iterable::*; +pub(crate) use no_return_argument_annotation::*; +pub(crate) use non_empty_stub_body::*; +pub(crate) use non_self_return_type::*; +pub(crate) use numeric_literal_too_long::*; +pub(crate) use pass_in_class_body::*; +pub(crate) use pass_statement_stub_body::*; +pub(crate) use prefix_type_params::*; +pub(crate) use quoted_annotation_in_stub::*; +pub(crate) use simple_defaults::*; +pub(crate) use str_or_repr_defined_in_stub::*; +pub(crate) use string_or_bytes_too_long::*; +pub(crate) use stub_body_multiple_statements::*; +pub(crate) use type_alias_naming::*; +pub(crate) use type_comment_in_stub::*; +pub(crate) use unaliased_collections_abc_set_import::*; +pub(crate) use unnecessary_literal_union::*; +pub(crate) use unrecognized_platform::*; +pub(crate) use unrecognized_version_info::*; mod any_eq_ne_annotation; mod bad_version_info_comparison; mod collections_named_tuple; +mod complex_if_statement_in_stub; mod docstring_in_stubs; mod duplicate_union_member; mod ellipsis_in_non_empty_class_body; +mod exit_annotations; +mod future_annotations_in_stub; mod iter_method_return_iterable; mod no_return_argument_annotation; mod non_empty_stub_body; @@ -65,4 +52,6 @@ mod stub_body_multiple_statements; mod type_alias_naming; mod type_comment_in_stub; mod unaliased_collections_abc_set_import; +mod unnecessary_literal_union; mod unrecognized_platform; +mod unrecognized_version_info; diff --git a/crates/ruff/src/rules/flake8_pyi/rules/no_return_argument_annotation.rs b/crates/ruff/src/rules/flake8_pyi/rules/no_return_argument_annotation.rs index 8fcb45d403..669e3ba046 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/no_return_argument_annotation.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/no_return_argument_annotation.rs @@ -1,11 +1,10 @@ use std::fmt; -use itertools::chain; use rustpython_parser::ast::Ranged; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::prelude::Arguments; +use rustpython_parser::ast::Arguments; use crate::checkers::ast::Checker; use crate::settings::types::PythonVersion::Py311; @@ -49,17 +48,14 @@ impl Violation for NoReturnArgumentAnnotationInStub { /// PYI050 pub(crate) fn no_return_argument_annotation(checker: &mut Checker, args: &Arguments) { - for annotation in chain!( - args.args.iter(), - args.posonlyargs.iter(), - args.kwonlyargs.iter() - ) - .filter_map(|arg| arg.annotation.as_ref()) + for annotation in args + .posonlyargs + .iter() + .chain(&args.args) + .chain(&args.kwonlyargs) + .filter_map(|arg| arg.def.annotation.as_ref()) { - if checker - .semantic_model() - .match_typing_expr(annotation, "NoReturn") - { + if checker.semantic().match_typing_expr(annotation, "NoReturn") { checker.diagnostics.push(Diagnostic::new( NoReturnArgumentAnnotationInStub { module: if checker.settings.target_version >= Py311 { diff --git a/crates/ruff/src/rules/flake8_pyi/rules/non_self_return_type.rs b/crates/ruff/src/rules/flake8_pyi/rules/non_self_return_type.rs index a5dd877d44..56f4cd9da0 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/non_self_return_type.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/non_self_return_type.rs @@ -2,10 +2,10 @@ use rustpython_parser::ast::{self, Arguments, Decorator, Expr, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::{identifier_range, map_subscript}; +use ruff_python_ast::helpers::map_subscript; +use ruff_python_ast::identifier::Identifier; use ruff_python_semantic::analyze::visibility::{is_abstract, is_final, is_overload}; -use ruff_python_semantic::model::SemanticModel; -use ruff_python_semantic::scope::ScopeKind; +use ruff_python_semantic::{ScopeKind, SemanticModel}; use crate::checkers::ast::Checker; @@ -119,7 +119,7 @@ pub(crate) fn non_self_return_type( args: &Arguments, async_: bool, ) { - let ScopeKind::Class(class_def) = checker.semantic_model().scope().kind else { + let ScopeKind::Class(class_def) = checker.semantic().scope().kind else { return; }; @@ -132,8 +132,8 @@ pub(crate) fn non_self_return_type( }; // Skip any abstract or overloaded methods. - if is_abstract(checker.semantic_model(), decorator_list) - || is_overload(checker.semantic_model(), decorator_list) + if is_abstract(decorator_list, checker.semantic()) + || is_overload(decorator_list, checker.semantic()) { return; } @@ -141,28 +141,28 @@ pub(crate) fn non_self_return_type( if async_ { if name == "__aenter__" && is_name(returns, &class_def.name) - && !is_final(checker.semantic_model(), &class_def.decorator_list) + && !is_final(&class_def.decorator_list, checker.semantic()) { checker.diagnostics.push(Diagnostic::new( NonSelfReturnType { class_name: class_def.name.to_string(), method_name: name.to_string(), }, - identifier_range(stmt, checker.locator), + stmt.identifier(), )); } return; } // In-place methods that are expected to return `Self`. - if INPLACE_BINOP_METHODS.contains(&name) { - if !is_self(returns, checker.semantic_model()) { + if is_inplace_bin_op(name) { + if !is_self(returns, checker.semantic()) { checker.diagnostics.push(Diagnostic::new( NonSelfReturnType { class_name: class_def.name.to_string(), method_name: name.to_string(), }, - identifier_range(stmt, checker.locator), + stmt.identifier(), )); } return; @@ -170,14 +170,14 @@ pub(crate) fn non_self_return_type( if is_name(returns, &class_def.name) { if matches!(name, "__enter__" | "__new__") - && !is_final(checker.semantic_model(), &class_def.decorator_list) + && !is_final(&class_def.decorator_list, checker.semantic()) { checker.diagnostics.push(Diagnostic::new( NonSelfReturnType { class_name: class_def.name.to_string(), method_name: name.to_string(), }, - identifier_range(stmt, checker.locator), + stmt.identifier(), )); } return; @@ -185,28 +185,28 @@ pub(crate) fn non_self_return_type( match name { "__iter__" => { - if is_iterable(returns, checker.semantic_model()) - && is_iterator(&class_def.bases, checker.semantic_model()) + if is_iterable(returns, checker.semantic()) + && is_iterator(&class_def.bases, checker.semantic()) { checker.diagnostics.push(Diagnostic::new( NonSelfReturnType { class_name: class_def.name.to_string(), method_name: name.to_string(), }, - identifier_range(stmt, checker.locator), + stmt.identifier(), )); } } "__aiter__" => { - if is_async_iterable(returns, checker.semantic_model()) - && is_async_iterator(&class_def.bases, checker.semantic_model()) + if is_async_iterable(returns, checker.semantic()) + && is_async_iterator(&class_def.bases, checker.semantic()) { checker.diagnostics.push(Diagnostic::new( NonSelfReturnType { class_name: class_def.name.to_string(), method_name: name.to_string(), }, - identifier_range(stmt, checker.locator), + stmt.identifier(), )); } } @@ -214,21 +214,25 @@ pub(crate) fn non_self_return_type( } } -const INPLACE_BINOP_METHODS: &[&str] = &[ - "__iadd__", - "__isub__", - "__imul__", - "__imatmul__", - "__itruediv__", - "__ifloordiv__", - "__imod__", - "__ipow__", - "__ilshift__", - "__irshift__", - "__iand__", - "__ixor__", - "__ior__", -]; +/// Returns `true` if the method is an in-place binary operator. +fn is_inplace_bin_op(name: &str) -> bool { + matches!( + name, + "__iadd__" + | "__isub__" + | "__imul__" + | "__imatmul__" + | "__itruediv__" + | "__ifloordiv__" + | "__imod__" + | "__ipow__" + | "__ilshift__" + | "__irshift__" + | "__iand__" + | "__ixor__" + | "__ior__" + ) +} /// Return `true` if the given expression resolves to the given name. fn is_name(expr: &Expr, name: &str) -> bool { @@ -239,14 +243,14 @@ fn is_name(expr: &Expr, name: &str) -> bool { } /// Return `true` if the given expression resolves to `typing.Self`. -fn is_self(expr: &Expr, model: &SemanticModel) -> bool { - model.match_typing_expr(expr, "Self") +fn is_self(expr: &Expr, semantic: &SemanticModel) -> bool { + semantic.match_typing_expr(expr, "Self") } /// Return `true` if the given class extends `collections.abc.Iterator`. -fn is_iterator(bases: &[Expr], model: &SemanticModel) -> bool { +fn is_iterator(bases: &[Expr], semantic: &SemanticModel) -> bool { bases.iter().any(|expr| { - model + semantic .resolve_call_path(map_subscript(expr)) .map_or(false, |call_path| { matches!( @@ -258,8 +262,8 @@ fn is_iterator(bases: &[Expr], model: &SemanticModel) -> bool { } /// Return `true` if the given expression resolves to `collections.abc.Iterable`. -fn is_iterable(expr: &Expr, model: &SemanticModel) -> bool { - model +fn is_iterable(expr: &Expr, semantic: &SemanticModel) -> bool { + semantic .resolve_call_path(map_subscript(expr)) .map_or(false, |call_path| { matches!( @@ -271,9 +275,9 @@ fn is_iterable(expr: &Expr, model: &SemanticModel) -> bool { } /// Return `true` if the given class extends `collections.abc.AsyncIterator`. -fn is_async_iterator(bases: &[Expr], model: &SemanticModel) -> bool { +fn is_async_iterator(bases: &[Expr], semantic: &SemanticModel) -> bool { bases.iter().any(|expr| { - model + semantic .resolve_call_path(map_subscript(expr)) .map_or(false, |call_path| { matches!( @@ -285,8 +289,8 @@ fn is_async_iterator(bases: &[Expr], model: &SemanticModel) -> bool { } /// Return `true` if the given expression resolves to `collections.abc.AsyncIterable`. -fn is_async_iterable(expr: &Expr, model: &SemanticModel) -> bool { - model +fn is_async_iterable(expr: &Expr, semantic: &SemanticModel) -> bool { + semantic .resolve_call_path(map_subscript(expr)) .map_or(false, |call_path| { matches!( diff --git a/crates/ruff/src/rules/flake8_pyi/rules/pass_in_class_body.rs b/crates/ruff/src/rules/flake8_pyi/rules/pass_in_class_body.rs index 43f1829e27..d6f947fd34 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/pass_in_class_body.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/pass_in_class_body.rs @@ -39,13 +39,8 @@ pub(crate) fn pass_in_class_body<'a>( let mut diagnostic = Diagnostic::new(PassInClassBody, stmt.range()); if checker.patch(diagnostic.kind.rule()) { - let edit = autofix::edits::delete_stmt( - stmt, - Some(parent), - checker.locator, - checker.indexer, - checker.stylist, - ); + let edit = + autofix::edits::delete_stmt(stmt, Some(parent), checker.locator, checker.indexer); diagnostic.set_fix(Fix::automatic(edit).isolate(checker.isolation(Some(parent)))); } checker.diagnostics.push(diagnostic); diff --git a/crates/ruff/src/rules/flake8_pyi/rules/prefix_type_params.rs b/crates/ruff/src/rules/flake8_pyi/rules/prefix_type_params.rs index e6c561191e..5a17dddcf7 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/prefix_type_params.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/prefix_type_params.rs @@ -70,17 +70,30 @@ pub(crate) fn prefix_type_params(checker: &mut Checker, value: &Expr, targets: & }; if let Expr::Call(ast::ExprCall { func, .. }) = value { - let Some(kind) = checker.semantic_model().resolve_call_path(func).and_then(|call_path| { - if checker.semantic_model().match_typing_call_path(&call_path, "ParamSpec") { - Some(VarKind::ParamSpec) - } else if checker.semantic_model().match_typing_call_path(&call_path, "TypeVar") { - Some(VarKind::TypeVar) - } else if checker.semantic_model().match_typing_call_path(&call_path, "TypeVarTuple") { - Some(VarKind::TypeVarTuple) - } else { - None - } - }) else { + let Some(kind) = checker + .semantic() + .resolve_call_path(func) + .and_then(|call_path| { + if checker + .semantic() + .match_typing_call_path(&call_path, "ParamSpec") + { + Some(VarKind::ParamSpec) + } else if checker + .semantic() + .match_typing_call_path(&call_path, "TypeVar") + { + Some(VarKind::TypeVar) + } else if checker + .semantic() + .match_typing_call_path(&call_path, "TypeVarTuple") + { + Some(VarKind::TypeVarTuple) + } else { + None + } + }) + else { return; }; checker diff --git a/crates/ruff/src/rules/flake8_pyi/rules/quoted_annotation_in_stub.rs b/crates/ruff/src/rules/flake8_pyi/rules/quoted_annotation_in_stub.rs index 6041af59d2..49e624735a 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/quoted_annotation_in_stub.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/quoted_annotation_in_stub.rs @@ -24,8 +24,7 @@ impl AlwaysAutofixableViolation for QuotedAnnotationInStub { pub(crate) fn quoted_annotation_in_stub(checker: &mut Checker, annotation: &str, range: TextRange) { let mut diagnostic = Diagnostic::new(QuotedAnnotationInStub, range); if checker.patch(Rule::QuotedAnnotationInStub) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( annotation.to_string(), range, ))); diff --git a/crates/ruff/src/rules/flake8_pyi/rules/simple_defaults.rs b/crates/ruff/src/rules/flake8_pyi/rules/simple_defaults.rs index 609602beab..b1ed3595b0 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/simple_defaults.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/simple_defaults.rs @@ -1,10 +1,12 @@ -use rustpython_parser::ast::{self, Arguments, Constant, Expr, Operator, Ranged, Stmt, Unaryop}; +use rustpython_parser::ast::{ + self, ArgWithDefault, Arguments, Constant, Expr, Operator, Ranged, Stmt, UnaryOp, +}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::call_path::CallPath; use ruff_python_ast::source_code::Locator; -use ruff_python_semantic::model::SemanticModel; -use ruff_python_semantic::scope::ScopeKind; +use ruff_python_semantic::{ScopeKind, SemanticModel}; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -95,36 +97,39 @@ impl Violation for UnassignedSpecialVariableInStub { } } -const ALLOWED_MATH_ATTRIBUTES_IN_DEFAULTS: &[&[&str]] = &[ - &["math", "inf"], - &["math", "nan"], - &["math", "e"], - &["math", "pi"], - &["math", "tau"], -]; +fn is_allowed_negated_math_attribute(call_path: &CallPath) -> bool { + matches!(call_path.as_slice(), ["math", "inf" | "e" | "pi" | "tau"]) +} -const ALLOWED_ATTRIBUTES_IN_DEFAULTS: &[&[&str]] = &[ - &["sys", "stdin"], - &["sys", "stdout"], - &["sys", "stderr"], - &["sys", "version"], - &["sys", "version_info"], - &["sys", "platform"], - &["sys", "executable"], - &["sys", "prefix"], - &["sys", "exec_prefix"], - &["sys", "base_prefix"], - &["sys", "byteorder"], - &["sys", "maxsize"], - &["sys", "hexversion"], - &["sys", "winver"], -]; +fn is_allowed_math_attribute(call_path: &CallPath) -> bool { + matches!( + call_path.as_slice(), + ["math", "inf" | "nan" | "e" | "pi" | "tau"] + | [ + "sys", + "stdin" + | "stdout" + | "stderr" + | "version" + | "version_info" + | "platform" + | "executable" + | "prefix" + | "exec_prefix" + | "base_prefix" + | "byteorder" + | "maxsize" + | "hexversion" + | "winver" + ] + ) +} fn is_valid_default_value_with_annotation( default: &Expr, allow_container: bool, locator: &Locator, - model: &SemanticModel, + semantic: &SemanticModel, ) -> bool { match default { Expr::Constant(_) => { @@ -137,7 +142,7 @@ fn is_valid_default_value_with_annotation( && elts.len() <= 10 && elts .iter() - .all(|e| is_valid_default_value_with_annotation(e, false, locator, model)); + .all(|e| is_valid_default_value_with_annotation(e, false, locator, semantic)); } Expr::Dict(ast::ExprDict { keys, @@ -148,12 +153,12 @@ fn is_valid_default_value_with_annotation( && keys.len() <= 10 && keys.iter().zip(values).all(|(k, v)| { k.as_ref().map_or(false, |k| { - is_valid_default_value_with_annotation(k, false, locator, model) - }) && is_valid_default_value_with_annotation(v, false, locator, model) + is_valid_default_value_with_annotation(k, false, locator, semantic) + }) && is_valid_default_value_with_annotation(v, false, locator, semantic) }); } Expr::UnaryOp(ast::ExprUnaryOp { - op: Unaryop::USub, + op: UnaryOp::USub, operand, range: _, }) => { @@ -165,12 +170,11 @@ fn is_valid_default_value_with_annotation( }) => return true, // Ex) `-math.inf`, `-math.pi`, etc. Expr::Attribute(_) => { - if model.resolve_call_path(operand).map_or(false, |call_path| { - ALLOWED_MATH_ATTRIBUTES_IN_DEFAULTS.iter().any(|target| { - // reject `-math.nan` - call_path.as_slice() == *target && *target != ["math", "nan"] - }) - }) { + if semantic + .resolve_call_path(operand) + .as_ref() + .map_or(false, is_allowed_negated_math_attribute) + { return true; } } @@ -197,7 +201,7 @@ fn is_valid_default_value_with_annotation( { return locator.slice(left.range()).len() <= 10; } else if let Expr::UnaryOp(ast::ExprUnaryOp { - op: Unaryop::USub, + op: UnaryOp::USub, operand, range: _, }) = left.as_ref() @@ -215,12 +219,11 @@ fn is_valid_default_value_with_annotation( } // Ex) `math.inf`, `sys.stdin`, etc. Expr::Attribute(_) => { - if model.resolve_call_path(default).map_or(false, |call_path| { - ALLOWED_MATH_ATTRIBUTES_IN_DEFAULTS - .iter() - .chain(ALLOWED_ATTRIBUTES_IN_DEFAULTS.iter()) - .any(|target| call_path.as_slice() == *target) - }) { + if semantic + .resolve_call_path(default) + .as_ref() + .map_or(false, is_allowed_math_attribute) + { return true; } } @@ -266,11 +269,11 @@ fn is_valid_default_value_without_annotation(default: &Expr) -> bool { /// Returns `true` if an [`Expr`] appears to be `TypeVar`, `TypeVarTuple`, `NewType`, or `ParamSpec` /// call. -fn is_type_var_like_call(model: &SemanticModel, expr: &Expr) -> bool { - let Expr::Call(ast::ExprCall { func, .. } )= expr else { +fn is_type_var_like_call(expr: &Expr, semantic: &SemanticModel) -> bool { + let Expr::Call(ast::ExprCall { func, .. }) = expr else { return false; }; - model.resolve_call_path(func).map_or(false, |call_path| { + semantic.resolve_call_path(func).map_or(false, |call_path| { matches!( call_path.as_slice(), [ @@ -283,11 +286,11 @@ fn is_type_var_like_call(model: &SemanticModel, expr: &Expr) -> bool { /// Returns `true` if this is a "special" assignment which must have a value (e.g., an assignment to /// `__all__`). -fn is_special_assignment(model: &SemanticModel, target: &Expr) -> bool { +fn is_special_assignment(target: &Expr, semantic: &SemanticModel) -> bool { if let Expr::Name(ast::ExprName { id, .. }) = target { match id.as_str() { - "__all__" => model.scope().kind.is_module(), - "__match_args__" | "__slots__" => model.scope().kind.is_class(), + "__all__" => semantic.scope().kind.is_module(), + "__match_args__" | "__slots__" => semantic.scope().kind.is_class(), _ => false, } } else { @@ -295,10 +298,20 @@ fn is_special_assignment(model: &SemanticModel, target: &Expr) -> bool { } } +/// Returns `true` if this is an assignment to a simple `Final`-annotated variable. +fn is_final_assignment(annotation: &Expr, value: &Expr, semantic: &SemanticModel) -> bool { + if matches!(value, Expr::Name(_) | Expr::Attribute(_)) { + if semantic.match_typing_expr(annotation, "Final") { + return true; + } + } + false +} + /// Returns `true` if the a class is an enum, based on its base classes. -fn is_enum(model: &SemanticModel, bases: &[Expr]) -> bool { +fn is_enum(bases: &[Expr], semantic: &SemanticModel) -> bool { return bases.iter().any(|expr| { - model.resolve_call_path(expr).map_or(false, |call_path| { + semantic.resolve_call_path(expr).map_or(false, |call_path| { matches!( call_path.as_slice(), [ @@ -311,130 +324,74 @@ fn is_enum(model: &SemanticModel, bases: &[Expr]) -> bool { } /// PYI011 -pub(crate) fn typed_argument_simple_defaults(checker: &mut Checker, args: &Arguments) { - if !args.defaults.is_empty() { - let defaults_start = args.posonlyargs.len() + args.args.len() - args.defaults.len(); - for (i, arg) in args.posonlyargs.iter().chain(&args.args).enumerate() { - if let Some(default) = i - .checked_sub(defaults_start) - .and_then(|i| args.defaults.get(i)) - { - if arg.annotation.is_some() { - if !is_valid_default_value_with_annotation( - default, - true, - checker.locator, - checker.semantic_model(), - ) { - let mut diagnostic = - Diagnostic::new(TypedArgumentDefaultInStub, default.range()); +pub(crate) fn typed_argument_simple_defaults(checker: &mut Checker, arguments: &Arguments) { + for ArgWithDefault { + def, + default, + range: _, + } in arguments + .posonlyargs + .iter() + .chain(&arguments.args) + .chain(&arguments.kwonlyargs) + { + let Some(default) = default else { + continue; + }; + if def.annotation.is_some() { + if !is_valid_default_value_with_annotation( + default, + true, + checker.locator, + checker.semantic(), + ) { + let mut diagnostic = Diagnostic::new(TypedArgumentDefaultInStub, default.range()); - if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Fix::suggested(Edit::range_replacement( - "...".to_string(), - default.range(), - ))); - } - - checker.diagnostics.push(diagnostic); - } + if checker.patch(diagnostic.kind.rule()) { + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( + "...".to_string(), + default.range(), + ))); } - } - } - } - if !args.kw_defaults.is_empty() { - let defaults_start = args.kwonlyargs.len() - args.kw_defaults.len(); - for (i, kwarg) in args.kwonlyargs.iter().enumerate() { - if let Some(default) = i - .checked_sub(defaults_start) - .and_then(|i| args.kw_defaults.get(i)) - { - if kwarg.annotation.is_some() { - if !is_valid_default_value_with_annotation( - default, - true, - checker.locator, - checker.semantic_model(), - ) { - let mut diagnostic = - Diagnostic::new(TypedArgumentDefaultInStub, default.range()); - - if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Fix::suggested(Edit::range_replacement( - "...".to_string(), - default.range(), - ))); - } - - checker.diagnostics.push(diagnostic); - } - } + checker.diagnostics.push(diagnostic); } } } } /// PYI014 -pub(crate) fn argument_simple_defaults(checker: &mut Checker, args: &Arguments) { - if !args.defaults.is_empty() { - let defaults_start = args.posonlyargs.len() + args.args.len() - args.defaults.len(); - for (i, arg) in args.posonlyargs.iter().chain(&args.args).enumerate() { - if let Some(default) = i - .checked_sub(defaults_start) - .and_then(|i| args.defaults.get(i)) - { - if arg.annotation.is_none() { - if !is_valid_default_value_with_annotation( - default, - true, - checker.locator, - checker.semantic_model(), - ) { - let mut diagnostic = - Diagnostic::new(ArgumentDefaultInStub, default.range()); +pub(crate) fn argument_simple_defaults(checker: &mut Checker, arguments: &Arguments) { + for ArgWithDefault { + def, + default, + range: _, + } in arguments + .posonlyargs + .iter() + .chain(&arguments.args) + .chain(&arguments.kwonlyargs) + { + let Some(default) = default else { + continue; + }; + if def.annotation.is_none() { + if !is_valid_default_value_with_annotation( + default, + true, + checker.locator, + checker.semantic(), + ) { + let mut diagnostic = Diagnostic::new(ArgumentDefaultInStub, default.range()); - if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Fix::suggested(Edit::range_replacement( - "...".to_string(), - default.range(), - ))); - } - - checker.diagnostics.push(diagnostic); - } + if checker.patch(diagnostic.kind.rule()) { + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( + "...".to_string(), + default.range(), + ))); } - } - } - } - if !args.kw_defaults.is_empty() { - let defaults_start = args.kwonlyargs.len() - args.kw_defaults.len(); - for (i, kwarg) in args.kwonlyargs.iter().enumerate() { - if let Some(default) = i - .checked_sub(defaults_start) - .and_then(|i| args.kw_defaults.get(i)) - { - if kwarg.annotation.is_none() { - if !is_valid_default_value_with_annotation( - default, - true, - checker.locator, - checker.semantic_model(), - ) { - let mut diagnostic = - Diagnostic::new(ArgumentDefaultInStub, default.range()); - - if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(Fix::suggested(Edit::range_replacement( - "...".to_string(), - default.range(), - ))); - } - - checker.diagnostics.push(diagnostic); - } - } + checker.diagnostics.push(diagnostic); } } } @@ -449,21 +406,16 @@ pub(crate) fn assignment_default_in_stub(checker: &mut Checker, targets: &[Expr] if !target.is_name_expr() { return; } - if is_special_assignment(checker.semantic_model(), target) { + if is_special_assignment(target, checker.semantic()) { return; } - if is_type_var_like_call(checker.semantic_model(), value) { + if is_type_var_like_call(value, checker.semantic()) { return; } if is_valid_default_value_without_annotation(value) { return; } - if is_valid_default_value_with_annotation( - value, - true, - checker.locator, - checker.semantic_model(), - ) { + if is_valid_default_value_with_annotation(value, true, checker.locator, checker.semantic()) { return; } @@ -485,23 +437,21 @@ pub(crate) fn annotated_assignment_default_in_stub( annotation: &Expr, ) { if checker - .semantic_model() + .semantic() .match_typing_expr(annotation, "TypeAlias") { return; } - if is_special_assignment(checker.semantic_model(), target) { + if is_special_assignment(target, checker.semantic()) { return; } - if is_type_var_like_call(checker.semantic_model(), value) { + if is_type_var_like_call(value, checker.semantic()) { return; } - if is_valid_default_value_with_annotation( - value, - true, - checker.locator, - checker.semantic_model(), - ) { + if is_final_assignment(annotation, value, checker.semantic()) { + return; + } + if is_valid_default_value_with_annotation(value, true, checker.locator, checker.semantic()) { return; } @@ -528,27 +478,21 @@ pub(crate) fn unannotated_assignment_in_stub( let Expr::Name(ast::ExprName { id, .. }) = target else { return; }; - if is_special_assignment(checker.semantic_model(), target) { + if is_special_assignment(target, checker.semantic()) { return; } - if is_type_var_like_call(checker.semantic_model(), value) { + if is_type_var_like_call(value, checker.semantic()) { return; } if is_valid_default_value_without_annotation(value) { return; } - if !is_valid_default_value_with_annotation( - value, - true, - checker.locator, - checker.semantic_model(), - ) { + if !is_valid_default_value_with_annotation(value, true, checker.locator, checker.semantic()) { return; } - if let ScopeKind::Class(ast::StmtClassDef { bases, .. }) = checker.semantic_model().scope().kind - { - if is_enum(checker.semantic_model(), bases) { + if let ScopeKind::Class(ast::StmtClassDef { bases, .. }) = checker.semantic().scope().kind { + if is_enum(bases, checker.semantic()) { return; } } @@ -570,7 +514,7 @@ pub(crate) fn unassigned_special_variable_in_stub( return; }; - if !is_special_assignment(checker.semantic_model(), target) { + if !is_special_assignment(target, checker.semantic()) { return; } diff --git a/crates/ruff/src/rules/flake8_pyi/rules/str_or_repr_defined_in_stub.rs b/crates/ruff/src/rules/flake8_pyi/rules/str_or_repr_defined_in_stub.rs index 88f204fcc3..daa63da702 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/str_or_repr_defined_in_stub.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/str_or_repr_defined_in_stub.rs @@ -3,7 +3,7 @@ use rustpython_parser::ast::Stmt; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::identifier_range; +use ruff_python_ast::identifier::Identifier; use ruff_python_semantic::analyze::visibility::is_abstract; use crate::autofix::edits::delete_stmt; @@ -50,8 +50,9 @@ pub(crate) fn str_or_repr_defined_in_stub(checker: &mut Checker, stmt: &Stmt) { returns, args, .. - }) = stmt else { - return + }) = stmt + else { + return; }; let Some(returns) = returns else { @@ -62,7 +63,7 @@ pub(crate) fn str_or_repr_defined_in_stub(checker: &mut Checker, stmt: &Stmt) { return; } - if !checker.semantic_model().scope().kind.is_class() { + if !checker.semantic().scope().kind.is_class() { return; } @@ -72,12 +73,12 @@ pub(crate) fn str_or_repr_defined_in_stub(checker: &mut Checker, stmt: &Stmt) { return; } - if is_abstract(checker.semantic_model(), decorator_list) { + if is_abstract(decorator_list, checker.semantic()) { return; } if checker - .semantic_model() + .semantic() .resolve_call_path(returns) .map_or(true, |call_path| { !matches!(call_path.as_slice(), ["" | "builtins", "str"]) @@ -90,20 +91,14 @@ pub(crate) fn str_or_repr_defined_in_stub(checker: &mut Checker, stmt: &Stmt) { StrOrReprDefinedInStub { name: name.to_string(), }, - identifier_range(stmt, checker.locator), + stmt.identifier(), ); if checker.patch(diagnostic.kind.rule()) { - let stmt = checker.semantic_model().stmt(); - let parent = checker.semantic_model().stmt_parent(); - let edit = delete_stmt( - stmt, - parent, - checker.locator, - checker.indexer, - checker.stylist, - ); + let stmt = checker.semantic().stmt(); + let parent = checker.semantic().stmt_parent(); + let edit = delete_stmt(stmt, parent, checker.locator, checker.indexer); diagnostic.set_fix( - Fix::automatic(edit).isolate(checker.isolation(checker.semantic_model().stmt_parent())), + Fix::automatic(edit).isolate(checker.isolation(checker.semantic().stmt_parent())), ); } checker.diagnostics.push(diagnostic); diff --git a/crates/ruff/src/rules/flake8_pyi/rules/string_or_bytes_too_long.rs b/crates/ruff/src/rules/flake8_pyi/rules/string_or_bytes_too_long.rs index cecaed2685..ff5ab572e8 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/string_or_bytes_too_long.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/string_or_bytes_too_long.rs @@ -2,6 +2,7 @@ use rustpython_parser::ast::{self, Constant, Expr, Ranged}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers::is_docstring_stmt; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -41,6 +42,11 @@ impl AlwaysAutofixableViolation for StringOrBytesTooLong { /// PYI053 pub(crate) fn string_or_bytes_too_long(checker: &mut Checker, expr: &Expr) { + // Ignore docstrings. + if is_docstring_stmt(checker.semantic().stmt()) { + return; + } + let length = match expr { Expr::Constant(ast::ExprConstant { value: Constant::Str(s), diff --git a/crates/ruff/src/rules/flake8_pyi/rules/stub_body_multiple_statements.rs b/crates/ruff/src/rules/flake8_pyi/rules/stub_body_multiple_statements.rs index 3ab3613237..62872ecc9a 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/stub_body_multiple_statements.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/stub_body_multiple_statements.rs @@ -2,8 +2,8 @@ use rustpython_parser::ast::Stmt; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers; use ruff_python_ast::helpers::is_docstring_stmt; +use ruff_python_ast::identifier::Identifier; use crate::checkers::ast::Checker; @@ -32,6 +32,6 @@ pub(crate) fn stub_body_multiple_statements(checker: &mut Checker, stmt: &Stmt, checker.diagnostics.push(Diagnostic::new( StubBodyMultipleStatements, - helpers::identifier_range(stmt, checker.locator), + stmt.identifier(), )); } diff --git a/crates/ruff/src/rules/flake8_pyi/rules/type_comment_in_stub.rs b/crates/ruff/src/rules/flake8_pyi/rules/type_comment_in_stub.rs index a4ca3bbc14..d7f8fda8ce 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/type_comment_in_stub.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/type_comment_in_stub.rs @@ -34,9 +34,11 @@ impl Violation for TypeCommentInStub { } /// PYI033 -pub(crate) fn type_comment_in_stub(indexer: &Indexer, locator: &Locator) -> Vec { - let mut diagnostics = vec![]; - +pub(crate) fn type_comment_in_stub( + diagnostics: &mut Vec, + locator: &Locator, + indexer: &Indexer, +) { for range in indexer.comment_ranges() { let comment = locator.slice(*range); @@ -44,8 +46,6 @@ pub(crate) fn type_comment_in_stub(indexer: &Indexer, locator: &Locator) -> Vec< diagnostics.push(Diagnostic::new(TypeCommentInStub, *range)); } } - - diagnostics } static TYPE_COMMENT_REGEX: Lazy = diff --git a/crates/ruff/src/rules/flake8_pyi/rules/unaliased_collections_abc_set_import.rs b/crates/ruff/src/rules/flake8_pyi/rules/unaliased_collections_abc_set_import.rs index db17d9c272..5fb0a3f5f6 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/unaliased_collections_abc_set_import.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/unaliased_collections_abc_set_import.rs @@ -1,9 +1,10 @@ -use rustpython_parser::ast::StmtImportFrom; - -use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_diagnostics::{AutofixKind, Diagnostic, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_semantic::{BindingKind, FromImport, Scope}; use crate::checkers::ast::Checker; +use crate::registry::AsRule; +use crate::renamer::Renamer; /// ## What it does /// Checks for `from collections.abc import Set` imports that do not alias @@ -30,6 +31,8 @@ use crate::checkers::ast::Checker; pub struct UnaliasedCollectionsAbcSetImport; impl Violation for UnaliasedCollectionsAbcSetImport { + const AUTOFIX: AutofixKind = AutofixKind::Sometimes; + #[derive_message_formats] fn message(&self) -> String { format!( @@ -43,20 +46,33 @@ impl Violation for UnaliasedCollectionsAbcSetImport { } /// PYI025 -pub(crate) fn unaliased_collections_abc_set_import(checker: &mut Checker, stmt: &StmtImportFrom) { - let Some(module_id) = &stmt.module else { - return; - }; - if module_id.as_str() != "collections.abc" { - return; - } - - for name in &stmt.names { - if name.name.as_str() == "Set" && name.asname.is_none() { - checker.diagnostics.push(Diagnostic::new( - UnaliasedCollectionsAbcSetImport, - name.range, - )); +pub(crate) fn unaliased_collections_abc_set_import( + checker: &Checker, + scope: &Scope, + diagnostics: &mut Vec, +) { + for (name, binding_id) in scope.all_bindings() { + let binding = checker.semantic().binding(binding_id); + let BindingKind::FromImport(FromImport { qualified_name }) = &binding.kind else { + continue; + }; + if qualified_name.as_str() != "collections.abc.Set" { + continue; } + if name == "AbstractSet" { + continue; + } + + let mut diagnostic = Diagnostic::new(UnaliasedCollectionsAbcSetImport, binding.range); + if checker.patch(diagnostic.kind.rule()) { + if checker.semantic().is_available("AbstractSet") { + diagnostic.try_set_fix(|| { + let (edit, rest) = + Renamer::rename(name, "AbstractSet", scope, checker.semantic())?; + Ok(Fix::suggested_edits(edit, rest)) + }); + } + } + diagnostics.push(diagnostic); } } diff --git a/crates/ruff/src/rules/flake8_pyi/rules/unnecessary_literal_union.rs b/crates/ruff/src/rules/flake8_pyi/rules/unnecessary_literal_union.rs new file mode 100644 index 0000000000..16f21549a9 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/rules/unnecessary_literal_union.rs @@ -0,0 +1,75 @@ +use rustpython_parser::ast::{self, Expr, Ranged}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use smallvec::SmallVec; + +use crate::checkers::ast::Checker; +use crate::rules::flake8_pyi::helpers::traverse_union; + +/// ## What it does +/// Checks for the presence of multiple literal types in a union. +/// +/// ## Why is this bad? +/// Literal types accept multiple arguments and it is clearer to specify them +/// as a single literal. +/// +/// ## Example +/// ```python +/// from typing import Literal +/// +/// field: Literal[1] | Literal[2] +/// ``` +/// +/// Use instead: +/// ```python +/// from typing import Literal +/// +/// field: Literal[1, 2] +/// ``` +#[violation] +pub struct UnnecessaryLiteralUnion { + members: Vec, +} + +impl Violation for UnnecessaryLiteralUnion { + #[derive_message_formats] + fn message(&self) -> String { + format!( + "Multiple literal members in a union. Use a single literal, e.g. `Literal[{}]`", + self.members.join(", ") + ) + } +} + +/// PYI030 +pub(crate) fn unnecessary_literal_union<'a>(checker: &mut Checker, expr: &'a Expr) { + let mut literal_exprs = SmallVec::<[&Box; 1]>::new(); + + // Adds a member to `literal_exprs` if it is a `Literal` annotation + let mut collect_literal_expr = |expr: &'a Expr, _| { + if let Expr::Subscript(ast::ExprSubscript { value, slice, .. }) = expr { + if checker.semantic().match_typing_expr(value, "Literal") { + literal_exprs.push(slice); + } + } + }; + + // Traverse the union, collect all literal members + traverse_union(&mut collect_literal_expr, checker.semantic(), expr, None); + + // Raise a violation if more than one + if literal_exprs.len() > 1 { + let diagnostic = Diagnostic::new( + UnnecessaryLiteralUnion { + members: literal_exprs + .into_iter() + .map(|literal_expr| checker.locator.slice(literal_expr.range()).to_string()) + .collect(), + }, + expr.range(), + ); + + checker.diagnostics.push(diagnostic); + } +} diff --git a/crates/ruff/src/rules/flake8_pyi/rules/unrecognized_platform.rs b/crates/ruff/src/rules/flake8_pyi/rules/unrecognized_platform.rs index 1efb416a0a..418c0c55bf 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/unrecognized_platform.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/unrecognized_platform.rs @@ -1,4 +1,4 @@ -use rustpython_parser::ast::{self, Cmpop, Constant, Expr, Ranged}; +use rustpython_parser::ast::{self, CmpOp, Constant, Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -89,47 +89,50 @@ impl Violation for UnrecognizedPlatformName { } /// PYI007, PYI008 -pub(crate) fn unrecognized_platform( - checker: &mut Checker, - expr: &Expr, - left: &Expr, - ops: &[Cmpop], - comparators: &[Expr], -) { - let ([op], [right]) = (ops, comparators) else { +pub(crate) fn unrecognized_platform(checker: &mut Checker, test: &Expr) { + let Expr::Compare(ast::ExprCompare { + left, + ops, + comparators, + .. + }) = test + else { + return; + }; + + let ([op], [right]) = (ops.as_slice(), comparators.as_slice()) else { return; }; - let diagnostic_unrecognized_platform_check = - Diagnostic::new(UnrecognizedPlatformCheck, expr.range()); if !checker - .semantic_model() + .semantic() .resolve_call_path(left) .map_or(false, |call_path| { - call_path.as_slice() == ["sys", "platform"] + matches!(call_path.as_slice(), ["sys", "platform"]) }) { return; } // "in" might also make sense but we don't currently have one. - if !matches!(op, Cmpop::Eq | Cmpop::NotEq) && checker.enabled(Rule::UnrecognizedPlatformCheck) { - checker - .diagnostics - .push(diagnostic_unrecognized_platform_check); + if !matches!(op, CmpOp::Eq | CmpOp::NotEq) { + if checker.enabled(Rule::UnrecognizedPlatformCheck) { + checker + .diagnostics + .push(Diagnostic::new(UnrecognizedPlatformCheck, test.range())); + } return; } - match right { - Expr::Constant(ast::ExprConstant { - value: Constant::Str(value), - .. - }) => { - // Other values are possible but we don't need them right now. - // This protects against typos. - if !["linux", "win32", "cygwin", "darwin"].contains(&value.as_str()) - && checker.enabled(Rule::UnrecognizedPlatformName) - { + if let Expr::Constant(ast::ExprConstant { + value: Constant::Str(value), + .. + }) = right + { + // Other values are possible but we don't need them right now. + // This protects against typos. + if checker.enabled(Rule::UnrecognizedPlatformName) { + if !matches!(value.as_str(), "linux" | "win32" | "cygwin" | "darwin") { checker.diagnostics.push(Diagnostic::new( UnrecognizedPlatformName { platform: value.clone(), @@ -138,12 +141,11 @@ pub(crate) fn unrecognized_platform( )); } } - _ => { - if checker.enabled(Rule::UnrecognizedPlatformCheck) { - checker - .diagnostics - .push(diagnostic_unrecognized_platform_check); - } + } else { + if checker.enabled(Rule::UnrecognizedPlatformCheck) { + checker + .diagnostics + .push(Diagnostic::new(UnrecognizedPlatformCheck, test.range())); } } } diff --git a/crates/ruff/src/rules/flake8_pyi/rules/unrecognized_version_info.rs b/crates/ruff/src/rules/flake8_pyi/rules/unrecognized_version_info.rs new file mode 100644 index 0000000000..dfb288154c --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/rules/unrecognized_version_info.rs @@ -0,0 +1,283 @@ +use num_bigint::BigInt; +use num_traits::{One, Zero}; +use rustpython_parser::ast::{self, CmpOp, Constant, Expr, Ranged}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers::map_subscript; + +use crate::checkers::ast::Checker; +use crate::registry::Rule; + +/// ## What it does +/// Checks for problematic `sys.version_info`-related conditions in stubs. +/// +/// ## Why is this bad? +/// Stub files support simple conditionals to test for differences in Python +/// versions using `sys.version_info`. However, there are a number of common +/// mistakes involving `sys.version_info` comparisons that should be avoided. +/// For example, comparing against a string can lead to unexpected behavior. +/// +/// ## Example +/// ```python +/// import sys +/// +/// if sys.version_info[0] == "2": +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// import sys +/// +/// if sys.version_info[0] == 2: +/// ... +/// ``` +#[violation] +pub struct UnrecognizedVersionInfoCheck; + +impl Violation for UnrecognizedVersionInfoCheck { + #[derive_message_formats] + fn message(&self) -> String { + format!("Unrecognized `sys.version_info` check") + } +} + +/// ## What it does +/// Checks for Python version comparisons in stubs that compare against patch +/// versions (e.g., Python 3.8.3) instead of major and minor versions (e.g., +/// Python 3.8). +/// +/// ## Why is this bad? +/// Stub files support simple conditionals to test for differences in Python +/// versions and platforms. However, type checkers only understand a limited +/// subset of these conditionals. In particular, type checkers don't support +/// patch versions (e.g., Python 3.8.3), only major and minor versions (e.g., +/// Python 3.8). Therefore, version checks in stubs should only use the major +/// and minor versions. +/// +/// ## Example +/// ```python +/// import sys +/// +/// if sys.version_info >= (3, 4, 3): +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// import sys +/// +/// if sys.version_info >= (3, 4): +/// ... +/// ``` +#[violation] +pub struct PatchVersionComparison; + +impl Violation for PatchVersionComparison { + #[derive_message_formats] + fn message(&self) -> String { + format!("Version comparison must use only major and minor version") + } +} + +/// ## What it does +/// Checks for Python version comparisons that compare against a tuple of the +/// wrong length. +/// +/// ## Why is this bad? +/// Stub files support simple conditionals to test for differences in Python +/// versions and platforms. When comparing against `sys.version_info`, avoid +/// comparing against tuples of the wrong length, which can lead to unexpected +/// behavior. +/// +/// ## Example +/// ```python +/// import sys +/// +/// if sys.version_info[:2] == (3,): +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// import sys +/// +/// if sys.version_info[0] == 3: +/// ... +/// ``` +#[violation] +pub struct WrongTupleLengthVersionComparison { + expected_length: usize, +} + +impl Violation for WrongTupleLengthVersionComparison { + #[derive_message_formats] + fn message(&self) -> String { + let WrongTupleLengthVersionComparison { expected_length } = self; + format!("Version comparison must be against a length-{expected_length} tuple") + } +} + +/// PYI003, PYI004, PYI005 +pub(crate) fn unrecognized_version_info(checker: &mut Checker, test: &Expr) { + let Expr::Compare(ast::ExprCompare { + left, + ops, + comparators, + .. + }) = test + else { + return; + }; + + let ([op], [comparator]) = (ops.as_slice(), comparators.as_slice()) else { + return; + }; + + if !checker + .semantic() + .resolve_call_path(map_subscript(left)) + .map_or(false, |call_path| { + matches!(call_path.as_slice(), ["sys", "version_info"]) + }) + { + return; + } + + if let Some(expected) = ExpectedComparator::try_from(left) { + version_check(checker, expected, test, *op, comparator); + } else { + if checker.enabled(Rule::UnrecognizedVersionInfoCheck) { + checker + .diagnostics + .push(Diagnostic::new(UnrecognizedVersionInfoCheck, test.range())); + } + } +} + +fn version_check( + checker: &mut Checker, + expected: ExpectedComparator, + test: &Expr, + op: CmpOp, + comparator: &Expr, +) { + // Single digit comparison, e.g., `sys.version_info[0] == 2`. + if expected == ExpectedComparator::MajorDigit { + if !is_int_constant(comparator) { + if checker.enabled(Rule::UnrecognizedVersionInfoCheck) { + checker + .diagnostics + .push(Diagnostic::new(UnrecognizedVersionInfoCheck, test.range())); + } + } + return; + } + + // Tuple comparison, e.g., `sys.version_info == (3, 4)`. + let Expr::Tuple(ast::ExprTuple { elts, .. }) = comparator else { + if checker.enabled(Rule::UnrecognizedVersionInfoCheck) { + checker + .diagnostics + .push(Diagnostic::new(UnrecognizedVersionInfoCheck, test.range())); + } + return; + }; + + if !elts.iter().all(is_int_constant) { + // All tuple elements must be integers, e.g., `sys.version_info == (3, 4)` instead of + // `sys.version_info == (3.0, 4)`. + if checker.enabled(Rule::UnrecognizedVersionInfoCheck) { + checker + .diagnostics + .push(Diagnostic::new(UnrecognizedVersionInfoCheck, test.range())); + } + } else if elts.len() > 2 { + // Must compare against major and minor version only, e.g., `sys.version_info == (3, 4)` + // instead of `sys.version_info == (3, 4, 0)`. + if checker.enabled(Rule::PatchVersionComparison) { + checker + .diagnostics + .push(Diagnostic::new(PatchVersionComparison, test.range())); + } + } + + if checker.enabled(Rule::WrongTupleLengthVersionComparison) { + if op == CmpOp::Eq || op == CmpOp::NotEq { + let expected_length = match expected { + ExpectedComparator::MajorTuple => 1, + ExpectedComparator::MajorMinorTuple => 2, + _ => return, + }; + + if elts.len() != expected_length { + checker.diagnostics.push(Diagnostic::new( + WrongTupleLengthVersionComparison { expected_length }, + test.range(), + )); + } + } + } +} + +#[derive(Copy, Clone, Eq, PartialEq)] +enum ExpectedComparator { + MajorDigit, + MajorTuple, + MajorMinorTuple, + AnyTuple, +} + +impl ExpectedComparator { + /// Returns the expected comparator for the given expression, if any. + fn try_from(expr: &Expr) -> Option { + let Expr::Subscript(ast::ExprSubscript { slice, .. }) = expr else { + return Some(ExpectedComparator::AnyTuple); + }; + + // Only allow: (1) simple slices of the form `[:n]`, or (2) explicit indexing into the first + // element (major version) of the tuple. + match slice.as_ref() { + Expr::Slice(ast::ExprSlice { + lower: None, + upper: Some(upper), + step: None, + .. + }) => { + if let Expr::Constant(ast::ExprConstant { + value: Constant::Int(upper), + .. + }) = upper.as_ref() + { + if *upper == BigInt::one() { + return Some(ExpectedComparator::MajorTuple); + } + if *upper == BigInt::from(2) { + return Some(ExpectedComparator::MajorMinorTuple); + } + } + } + Expr::Constant(ast::ExprConstant { + value: Constant::Int(n), + .. + }) if n.is_zero() => { + return Some(ExpectedComparator::MajorDigit); + } + _ => (), + } + + None + } +} + +/// Returns `true` if the given expression is an integer constant. +fn is_int_constant(expr: &Expr) -> bool { + matches!( + expr, + Expr::Constant(ast::ExprConstant { + value: ast::Constant::Int(_), + .. + }) + ) +} diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI002_PYI002.py.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI002_PYI002.py.snap new file mode 100644 index 0000000000..d1aa2e9116 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI002_PYI002.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- + diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI002_PYI002.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI002_PYI002.pyi.snap new file mode 100644 index 0000000000..d5de9a5682 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI002_PYI002.pyi.snap @@ -0,0 +1,40 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- +PYI002.pyi:3:4: PYI002 `if` test must be a simple comparison against `sys.platform` or `sys.version_info` + | +1 | import sys +2 | +3 | if sys.version == 'Python 2.7.10': ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI002 +4 | if 'linux' == sys.platform: ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info +5 | if hasattr(sys, 'maxint'): ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info + | + +PYI002.pyi:4:4: PYI002 `if` test must be a simple comparison against `sys.platform` or `sys.version_info` + | +3 | if sys.version == 'Python 2.7.10': ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info +4 | if 'linux' == sys.platform: ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info + | ^^^^^^^^^^^^^^^^^^^^^^^ PYI002 +5 | if hasattr(sys, 'maxint'): ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info +6 | if sys.maxsize == 42: ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info + | + +PYI002.pyi:5:4: PYI002 `if` test must be a simple comparison against `sys.platform` or `sys.version_info` + | +3 | if sys.version == 'Python 2.7.10': ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info +4 | if 'linux' == sys.platform: ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info +5 | if hasattr(sys, 'maxint'): ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info + | ^^^^^^^^^^^^^^^^^^^^^^ PYI002 +6 | if sys.maxsize == 42: ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info + | + +PYI002.pyi:6:4: PYI002 `if` test must be a simple comparison against `sys.platform` or `sys.version_info` + | +4 | if 'linux' == sys.platform: ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info +5 | if hasattr(sys, 'maxint'): ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info +6 | if sys.maxsize == 42: ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info + | ^^^^^^^^^^^^^^^^^ PYI002 + | + + diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI003_PYI003.py.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI003_PYI003.py.snap new file mode 100644 index 0000000000..d1aa2e9116 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI003_PYI003.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- + diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI003_PYI003.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI003_PYI003.pyi.snap new file mode 100644 index 0000000000..2ce520c09b --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI003_PYI003.pyi.snap @@ -0,0 +1,173 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- +PYI003.pyi:4:4: PYI003 Unrecognized `sys.version_info` check + | +3 | if sys.version_info[0] == 2: ... +4 | if sys.version_info[0] == True: ... # Y003 Unrecognized sys.version_info check # E712 comparison to True should be 'if cond is True:' or 'if cond:' + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +5 | if sys.version_info[0.0] == 2: ... # Y003 Unrecognized sys.version_info check +6 | if sys.version_info[False] == 2: ... # Y003 Unrecognized sys.version_info check + | + +PYI003.pyi:5:4: PYI003 Unrecognized `sys.version_info` check + | +3 | if sys.version_info[0] == 2: ... +4 | if sys.version_info[0] == True: ... # Y003 Unrecognized sys.version_info check # E712 comparison to True should be 'if cond is True:' or 'if cond:' +5 | if sys.version_info[0.0] == 2: ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +6 | if sys.version_info[False] == 2: ... # Y003 Unrecognized sys.version_info check +7 | if sys.version_info[0j] == 2: ... # Y003 Unrecognized sys.version_info check + | + +PYI003.pyi:6:4: PYI003 Unrecognized `sys.version_info` check + | +4 | if sys.version_info[0] == True: ... # Y003 Unrecognized sys.version_info check # E712 comparison to True should be 'if cond is True:' or 'if cond:' +5 | if sys.version_info[0.0] == 2: ... # Y003 Unrecognized sys.version_info check +6 | if sys.version_info[False] == 2: ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +7 | if sys.version_info[0j] == 2: ... # Y003 Unrecognized sys.version_info check +8 | if sys.version_info[0] == (2, 7): ... # Y003 Unrecognized sys.version_info check + | + +PYI003.pyi:7:4: PYI003 Unrecognized `sys.version_info` check + | +5 | if sys.version_info[0.0] == 2: ... # Y003 Unrecognized sys.version_info check +6 | if sys.version_info[False] == 2: ... # Y003 Unrecognized sys.version_info check +7 | if sys.version_info[0j] == 2: ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +8 | if sys.version_info[0] == (2, 7): ... # Y003 Unrecognized sys.version_info check +9 | if sys.version_info[0] == '2': ... # Y003 Unrecognized sys.version_info check + | + +PYI003.pyi:8:4: PYI003 Unrecognized `sys.version_info` check + | + 6 | if sys.version_info[False] == 2: ... # Y003 Unrecognized sys.version_info check + 7 | if sys.version_info[0j] == 2: ... # Y003 Unrecognized sys.version_info check + 8 | if sys.version_info[0] == (2, 7): ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 + 9 | if sys.version_info[0] == '2': ... # Y003 Unrecognized sys.version_info check +10 | if sys.version_info[1:] >= (7, 11): ... # Y003 Unrecognized sys.version_info check + | + +PYI003.pyi:9:4: PYI003 Unrecognized `sys.version_info` check + | + 7 | if sys.version_info[0j] == 2: ... # Y003 Unrecognized sys.version_info check + 8 | if sys.version_info[0] == (2, 7): ... # Y003 Unrecognized sys.version_info check + 9 | if sys.version_info[0] == '2': ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +10 | if sys.version_info[1:] >= (7, 11): ... # Y003 Unrecognized sys.version_info check +11 | if sys.version_info[::-1] < (11, 7): ... # Y003 Unrecognized sys.version_info check + | + +PYI003.pyi:10:4: PYI003 Unrecognized `sys.version_info` check + | + 8 | if sys.version_info[0] == (2, 7): ... # Y003 Unrecognized sys.version_info check + 9 | if sys.version_info[0] == '2': ... # Y003 Unrecognized sys.version_info check +10 | if sys.version_info[1:] >= (7, 11): ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +11 | if sys.version_info[::-1] < (11, 7): ... # Y003 Unrecognized sys.version_info check +12 | if sys.version_info[:3] >= (2, 7): ... # Y003 Unrecognized sys.version_info check + | + +PYI003.pyi:11:4: PYI003 Unrecognized `sys.version_info` check + | + 9 | if sys.version_info[0] == '2': ... # Y003 Unrecognized sys.version_info check +10 | if sys.version_info[1:] >= (7, 11): ... # Y003 Unrecognized sys.version_info check +11 | if sys.version_info[::-1] < (11, 7): ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +12 | if sys.version_info[:3] >= (2, 7): ... # Y003 Unrecognized sys.version_info check +13 | if sys.version_info[:True] >= (2, 7): ... # Y003 Unrecognized sys.version_info check + | + +PYI003.pyi:12:4: PYI003 Unrecognized `sys.version_info` check + | +10 | if sys.version_info[1:] >= (7, 11): ... # Y003 Unrecognized sys.version_info check +11 | if sys.version_info[::-1] < (11, 7): ... # Y003 Unrecognized sys.version_info check +12 | if sys.version_info[:3] >= (2, 7): ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +13 | if sys.version_info[:True] >= (2, 7): ... # Y003 Unrecognized sys.version_info check +14 | if sys.version_info[:1] == (2,): ... + | + +PYI003.pyi:13:4: PYI003 Unrecognized `sys.version_info` check + | +11 | if sys.version_info[::-1] < (11, 7): ... # Y003 Unrecognized sys.version_info check +12 | if sys.version_info[:3] >= (2, 7): ... # Y003 Unrecognized sys.version_info check +13 | if sys.version_info[:True] >= (2, 7): ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +14 | if sys.version_info[:1] == (2,): ... +15 | if sys.version_info[:1] == (True,): ... # Y003 Unrecognized sys.version_info check + | + +PYI003.pyi:15:4: PYI003 Unrecognized `sys.version_info` check + | +13 | if sys.version_info[:True] >= (2, 7): ... # Y003 Unrecognized sys.version_info check +14 | if sys.version_info[:1] == (2,): ... +15 | if sys.version_info[:1] == (True,): ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +16 | if sys.version_info[:1] == (2, 7): ... # Y005 Version comparison must be against a length-1 tuple +17 | if sys.version_info[:2] == (2, 7): ... + | + +PYI003.pyi:19:4: PYI003 Unrecognized `sys.version_info` check + | +17 | if sys.version_info[:2] == (2, 7): ... +18 | if sys.version_info[:2] == (2,): ... # Y005 Version comparison must be against a length-2 tuple +19 | if sys.version_info[:2] == "lol": ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +20 | if sys.version_info[:2.0] >= (3, 9): ... # Y003 Unrecognized sys.version_info check +21 | if sys.version_info[:2j] >= (3, 9): ... # Y003 Unrecognized sys.version_info check + | + +PYI003.pyi:20:4: PYI003 Unrecognized `sys.version_info` check + | +18 | if sys.version_info[:2] == (2,): ... # Y005 Version comparison must be against a length-2 tuple +19 | if sys.version_info[:2] == "lol": ... # Y003 Unrecognized sys.version_info check +20 | if sys.version_info[:2.0] >= (3, 9): ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +21 | if sys.version_info[:2j] >= (3, 9): ... # Y003 Unrecognized sys.version_info check +22 | if sys.version_info[:, :] >= (2, 7): ... # Y003 Unrecognized sys.version_info check + | + +PYI003.pyi:21:4: PYI003 Unrecognized `sys.version_info` check + | +19 | if sys.version_info[:2] == "lol": ... # Y003 Unrecognized sys.version_info check +20 | if sys.version_info[:2.0] >= (3, 9): ... # Y003 Unrecognized sys.version_info check +21 | if sys.version_info[:2j] >= (3, 9): ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +22 | if sys.version_info[:, :] >= (2, 7): ... # Y003 Unrecognized sys.version_info check +23 | if sys.version_info < [3, 0]: ... # Y003 Unrecognized sys.version_info check + | + +PYI003.pyi:22:4: PYI003 Unrecognized `sys.version_info` check + | +20 | if sys.version_info[:2.0] >= (3, 9): ... # Y003 Unrecognized sys.version_info check +21 | if sys.version_info[:2j] >= (3, 9): ... # Y003 Unrecognized sys.version_info check +22 | if sys.version_info[:, :] >= (2, 7): ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +23 | if sys.version_info < [3, 0]: ... # Y003 Unrecognized sys.version_info check +24 | if sys.version_info < ('3', '0'): ... # Y003 Unrecognized sys.version_info check + | + +PYI003.pyi:23:4: PYI003 Unrecognized `sys.version_info` check + | +21 | if sys.version_info[:2j] >= (3, 9): ... # Y003 Unrecognized sys.version_info check +22 | if sys.version_info[:, :] >= (2, 7): ... # Y003 Unrecognized sys.version_info check +23 | if sys.version_info < [3, 0]: ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +24 | if sys.version_info < ('3', '0'): ... # Y003 Unrecognized sys.version_info check +25 | if sys.version_info >= (3, 4, 3): ... # Y004 Version comparison must use only major and minor version + | + +PYI003.pyi:24:4: PYI003 Unrecognized `sys.version_info` check + | +22 | if sys.version_info[:, :] >= (2, 7): ... # Y003 Unrecognized sys.version_info check +23 | if sys.version_info < [3, 0]: ... # Y003 Unrecognized sys.version_info check +24 | if sys.version_info < ('3', '0'): ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +25 | if sys.version_info >= (3, 4, 3): ... # Y004 Version comparison must use only major and minor version +26 | if sys.version_info == (3, 4): ... # Y006 Use only < and >= for version comparisons + | + + diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI004_PYI004.py.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI004_PYI004.py.snap new file mode 100644 index 0000000000..d1aa2e9116 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI004_PYI004.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- + diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI004_PYI004.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI004_PYI004.pyi.snap new file mode 100644 index 0000000000..ddb37572e5 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI004_PYI004.pyi.snap @@ -0,0 +1,42 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- +PYI004.pyi:4:4: PYI004 Version comparison must use only major and minor version + | +2 | from sys import version_info +3 | +4 | if sys.version_info >= (3, 4, 3): ... # PYI004 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI004 +5 | if sys.version_info < (3, 4, 3): ... # PYI004 +6 | if sys.version_info == (3, 4, 3): ... # PYI004 + | + +PYI004.pyi:5:4: PYI004 Version comparison must use only major and minor version + | +4 | if sys.version_info >= (3, 4, 3): ... # PYI004 +5 | if sys.version_info < (3, 4, 3): ... # PYI004 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI004 +6 | if sys.version_info == (3, 4, 3): ... # PYI004 +7 | if sys.version_info != (3, 4, 3): ... # PYI004 + | + +PYI004.pyi:6:4: PYI004 Version comparison must use only major and minor version + | +4 | if sys.version_info >= (3, 4, 3): ... # PYI004 +5 | if sys.version_info < (3, 4, 3): ... # PYI004 +6 | if sys.version_info == (3, 4, 3): ... # PYI004 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI004 +7 | if sys.version_info != (3, 4, 3): ... # PYI004 + | + +PYI004.pyi:7:4: PYI004 Version comparison must use only major and minor version + | +5 | if sys.version_info < (3, 4, 3): ... # PYI004 +6 | if sys.version_info == (3, 4, 3): ... # PYI004 +7 | if sys.version_info != (3, 4, 3): ... # PYI004 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI004 +8 | +9 | if sys.version_info[0] == 2: ... + | + + diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI005_PYI005.py.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI005_PYI005.py.snap new file mode 100644 index 0000000000..d1aa2e9116 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI005_PYI005.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- + diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI005_PYI005.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI005_PYI005.pyi.snap new file mode 100644 index 0000000000..1641bce44c --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI005_PYI005.pyi.snap @@ -0,0 +1,20 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- +PYI005.pyi:4:4: PYI005 Version comparison must be against a length-1 tuple + | +2 | from sys import platform, version_info +3 | +4 | if sys.version_info[:1] == (2, 7): ... # Y005 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI005 +5 | if sys.version_info[:2] == (2,): ... # Y005 + | + +PYI005.pyi:5:4: PYI005 Version comparison must be against a length-2 tuple + | +4 | if sys.version_info[:1] == (2, 7): ... # Y005 +5 | if sys.version_info[:2] == (2,): ... # Y005 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI005 + | + + diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI006_PYI006.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI006_PYI006.pyi.snap index 3c0293215f..8dbd74304f 100644 --- a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI006_PYI006.pyi.snap +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI006_PYI006.pyi.snap @@ -1,7 +1,7 @@ --- source: crates/ruff/src/rules/flake8_pyi/mod.rs --- -PYI006.pyi:8:4: PYI006 Use `<` or `>=` for version info comparisons +PYI006.pyi:8:4: PYI006 Use `<` or `>=` for `sys.version_info` comparisons | 6 | if sys.version_info >= (3, 9): ... # OK 7 | @@ -11,7 +11,7 @@ PYI006.pyi:8:4: PYI006 Use `<` or `>=` for version info comparisons 10 | if sys.version_info == (3, 9): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons | -PYI006.pyi:10:4: PYI006 Use `<` or `>=` for version info comparisons +PYI006.pyi:10:4: PYI006 Use `<` or `>=` for `sys.version_info` comparisons | 8 | if sys.version_info == (3, 9): ... # OK 9 | @@ -21,7 +21,7 @@ PYI006.pyi:10:4: PYI006 Use `<` or `>=` for version info comparisons 12 | if sys.version_info <= (3, 10): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons | -PYI006.pyi:12:4: PYI006 Use `<` or `>=` for version info comparisons +PYI006.pyi:12:4: PYI006 Use `<` or `>=` for `sys.version_info` comparisons | 10 | if sys.version_info == (3, 9): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons 11 | @@ -31,7 +31,7 @@ PYI006.pyi:12:4: PYI006 Use `<` or `>=` for version info comparisons 14 | if sys.version_info <= (3, 10): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons | -PYI006.pyi:14:4: PYI006 Use `<` or `>=` for version info comparisons +PYI006.pyi:14:4: PYI006 Use `<` or `>=` for `sys.version_info` comparisons | 12 | if sys.version_info <= (3, 10): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons 13 | @@ -41,7 +41,7 @@ PYI006.pyi:14:4: PYI006 Use `<` or `>=` for version info comparisons 16 | if sys.version_info > (3, 10): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons | -PYI006.pyi:16:4: PYI006 Use `<` or `>=` for version info comparisons +PYI006.pyi:16:4: PYI006 Use `<` or `>=` for `sys.version_info` comparisons | 14 | if sys.version_info <= (3, 10): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons 15 | @@ -51,7 +51,7 @@ PYI006.pyi:16:4: PYI006 Use `<` or `>=` for version info comparisons 18 | if python_version > (3, 10): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons | -PYI006.pyi:18:4: PYI006 Use `<` or `>=` for version info comparisons +PYI006.pyi:18:4: PYI006 Use `<` or `>=` for `sys.version_info` comparisons | 16 | if sys.version_info > (3, 10): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons 17 | diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI016_PYI016.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI016_PYI016.pyi.snap index 031cd8143b..6cfac77210 100644 --- a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI016_PYI016.pyi.snap +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI016_PYI016.pyi.snap @@ -1,219 +1,462 @@ --- source: crates/ruff/src/rules/flake8_pyi/mod.rs --- -PYI016.pyi:5:15: PYI016 [*] Duplicate union member `str` +PYI016.pyi:7:15: PYI016 [*] Duplicate union member `str` | -4 | # Should emit for duplicate field types. -5 | field2: str | str # PYI016: Duplicate union member `str` +6 | # Should emit for duplicate field types. +7 | field2: str | str # PYI016: Duplicate union member `str` | ^^^ PYI016 -6 | -7 | # Should emit for union types in arguments. +8 | +9 | # Should emit for union types in arguments. | = help: Remove duplicate union member `str` -ℹ Suggested fix -2 2 | field1: str -3 3 | -4 4 | # Should emit for duplicate field types. -5 |-field2: str | str # PYI016: Duplicate union member `str` - 5 |+field2: str # PYI016: Duplicate union member `str` -6 6 | -7 7 | # Should emit for union types in arguments. -8 8 | def func1(arg1: int | int): # PYI016: Duplicate union member `int` +ℹ Fix +4 4 | field1: str +5 5 | +6 6 | # Should emit for duplicate field types. +7 |-field2: str | str # PYI016: Duplicate union member `str` + 7 |+field2: str # PYI016: Duplicate union member `str` +8 8 | +9 9 | # Should emit for union types in arguments. +10 10 | def func1(arg1: int | int): # PYI016: Duplicate union member `int` -PYI016.pyi:8:23: PYI016 [*] Duplicate union member `int` - | -7 | # Should emit for union types in arguments. -8 | def func1(arg1: int | int): # PYI016: Duplicate union member `int` - | ^^^ PYI016 -9 | print(arg1) - | - = help: Remove duplicate union member `int` - -ℹ Suggested fix -5 5 | field2: str | str # PYI016: Duplicate union member `str` -6 6 | -7 7 | # Should emit for union types in arguments. -8 |-def func1(arg1: int | int): # PYI016: Duplicate union member `int` - 8 |+def func1(arg1: int): # PYI016: Duplicate union member `int` -9 9 | print(arg1) -10 10 | -11 11 | # Should emit for unions in return types. - -PYI016.pyi:12:22: PYI016 [*] Duplicate union member `str` +PYI016.pyi:10:23: PYI016 [*] Duplicate union member `int` | -11 | # Should emit for unions in return types. -12 | def func2() -> str | str: # PYI016: Duplicate union member `str` - | ^^^ PYI016 -13 | return "my string" - | - = help: Remove duplicate union member `str` - -ℹ Suggested fix -9 9 | print(arg1) -10 10 | -11 11 | # Should emit for unions in return types. -12 |-def func2() -> str | str: # PYI016: Duplicate union member `str` - 12 |+def func2() -> str: # PYI016: Duplicate union member `str` -13 13 | return "my string" -14 14 | -15 15 | # Should emit in longer unions, even if not directly adjacent. - -PYI016.pyi:16:15: PYI016 [*] Duplicate union member `str` - | -15 | # Should emit in longer unions, even if not directly adjacent. -16 | field3: str | str | int # PYI016: Duplicate union member `str` - | ^^^ PYI016 -17 | field4: int | int | str # PYI016: Duplicate union member `int` -18 | field5: str | int | str # PYI016: Duplicate union member `str` - | - = help: Remove duplicate union member `str` - -ℹ Suggested fix -13 13 | return "my string" -14 14 | -15 15 | # Should emit in longer unions, even if not directly adjacent. -16 |-field3: str | str | int # PYI016: Duplicate union member `str` - 16 |+field3: str | int # PYI016: Duplicate union member `str` -17 17 | field4: int | int | str # PYI016: Duplicate union member `int` -18 18 | field5: str | int | str # PYI016: Duplicate union member `str` -19 19 | field6: int | bool | str | int # PYI016: Duplicate union member `int` - -PYI016.pyi:17:15: PYI016 [*] Duplicate union member `int` - | -15 | # Should emit in longer unions, even if not directly adjacent. -16 | field3: str | str | int # PYI016: Duplicate union member `str` -17 | field4: int | int | str # PYI016: Duplicate union member `int` - | ^^^ PYI016 -18 | field5: str | int | str # PYI016: Duplicate union member `str` -19 | field6: int | bool | str | int # PYI016: Duplicate union member `int` + 9 | # Should emit for union types in arguments. +10 | def func1(arg1: int | int): # PYI016: Duplicate union member `int` + | ^^^ PYI016 +11 | print(arg1) | = help: Remove duplicate union member `int` -ℹ Suggested fix -14 14 | -15 15 | # Should emit in longer unions, even if not directly adjacent. -16 16 | field3: str | str | int # PYI016: Duplicate union member `str` -17 |-field4: int | int | str # PYI016: Duplicate union member `int` - 17 |+field4: int | str # PYI016: Duplicate union member `int` -18 18 | field5: str | int | str # PYI016: Duplicate union member `str` -19 19 | field6: int | bool | str | int # PYI016: Duplicate union member `int` -20 20 | +ℹ Fix +7 7 | field2: str | str # PYI016: Duplicate union member `str` +8 8 | +9 9 | # Should emit for union types in arguments. +10 |-def func1(arg1: int | int): # PYI016: Duplicate union member `int` + 10 |+def func1(arg1: int): # PYI016: Duplicate union member `int` +11 11 | print(arg1) +12 12 | +13 13 | # Should emit for unions in return types. -PYI016.pyi:18:21: PYI016 [*] Duplicate union member `str` +PYI016.pyi:14:22: PYI016 [*] Duplicate union member `str` | -16 | field3: str | str | int # PYI016: Duplicate union member `str` -17 | field4: int | int | str # PYI016: Duplicate union member `int` -18 | field5: str | int | str # PYI016: Duplicate union member `str` - | ^^^ PYI016 -19 | field6: int | bool | str | int # PYI016: Duplicate union member `int` +13 | # Should emit for unions in return types. +14 | def func2() -> str | str: # PYI016: Duplicate union member `str` + | ^^^ PYI016 +15 | return "my string" | = help: Remove duplicate union member `str` -ℹ Suggested fix -15 15 | # Should emit in longer unions, even if not directly adjacent. -16 16 | field3: str | str | int # PYI016: Duplicate union member `str` -17 17 | field4: int | int | str # PYI016: Duplicate union member `int` -18 |-field5: str | int | str # PYI016: Duplicate union member `str` - 18 |+field5: str | int # PYI016: Duplicate union member `str` -19 19 | field6: int | bool | str | int # PYI016: Duplicate union member `int` -20 20 | -21 21 | # Shouldn't emit for non-type unions. +ℹ Fix +11 11 | print(arg1) +12 12 | +13 13 | # Should emit for unions in return types. +14 |-def func2() -> str | str: # PYI016: Duplicate union member `str` + 14 |+def func2() -> str: # PYI016: Duplicate union member `str` +15 15 | return "my string" +16 16 | +17 17 | # Should emit in longer unions, even if not directly adjacent. -PYI016.pyi:19:28: PYI016 [*] Duplicate union member `int` +PYI016.pyi:18:15: PYI016 [*] Duplicate union member `str` | -17 | field4: int | int | str # PYI016: Duplicate union member `int` -18 | field5: str | int | str # PYI016: Duplicate union member `str` -19 | field6: int | bool | str | int # PYI016: Duplicate union member `int` +17 | # Should emit in longer unions, even if not directly adjacent. +18 | field3: str | str | int # PYI016: Duplicate union member `str` + | ^^^ PYI016 +19 | field4: int | int | str # PYI016: Duplicate union member `int` +20 | field5: str | int | str # PYI016: Duplicate union member `str` + | + = help: Remove duplicate union member `str` + +ℹ Fix +15 15 | return "my string" +16 16 | +17 17 | # Should emit in longer unions, even if not directly adjacent. +18 |-field3: str | str | int # PYI016: Duplicate union member `str` + 18 |+field3: str | int # PYI016: Duplicate union member `str` +19 19 | field4: int | int | str # PYI016: Duplicate union member `int` +20 20 | field5: str | int | str # PYI016: Duplicate union member `str` +21 21 | field6: int | bool | str | int # PYI016: Duplicate union member `int` + +PYI016.pyi:19:15: PYI016 [*] Duplicate union member `int` + | +17 | # Should emit in longer unions, even if not directly adjacent. +18 | field3: str | str | int # PYI016: Duplicate union member `str` +19 | field4: int | int | str # PYI016: Duplicate union member `int` + | ^^^ PYI016 +20 | field5: str | int | str # PYI016: Duplicate union member `str` +21 | field6: int | bool | str | int # PYI016: Duplicate union member `int` + | + = help: Remove duplicate union member `int` + +ℹ Fix +16 16 | +17 17 | # Should emit in longer unions, even if not directly adjacent. +18 18 | field3: str | str | int # PYI016: Duplicate union member `str` +19 |-field4: int | int | str # PYI016: Duplicate union member `int` + 19 |+field4: int | str # PYI016: Duplicate union member `int` +20 20 | field5: str | int | str # PYI016: Duplicate union member `str` +21 21 | field6: int | bool | str | int # PYI016: Duplicate union member `int` +22 22 | + +PYI016.pyi:20:21: PYI016 [*] Duplicate union member `str` + | +18 | field3: str | str | int # PYI016: Duplicate union member `str` +19 | field4: int | int | str # PYI016: Duplicate union member `int` +20 | field5: str | int | str # PYI016: Duplicate union member `str` + | ^^^ PYI016 +21 | field6: int | bool | str | int # PYI016: Duplicate union member `int` + | + = help: Remove duplicate union member `str` + +ℹ Fix +17 17 | # Should emit in longer unions, even if not directly adjacent. +18 18 | field3: str | str | int # PYI016: Duplicate union member `str` +19 19 | field4: int | int | str # PYI016: Duplicate union member `int` +20 |-field5: str | int | str # PYI016: Duplicate union member `str` + 20 |+field5: str | int # PYI016: Duplicate union member `str` +21 21 | field6: int | bool | str | int # PYI016: Duplicate union member `int` +22 22 | +23 23 | # Shouldn't emit for non-type unions. + +PYI016.pyi:21:28: PYI016 [*] Duplicate union member `int` + | +19 | field4: int | int | str # PYI016: Duplicate union member `int` +20 | field5: str | int | str # PYI016: Duplicate union member `str` +21 | field6: int | bool | str | int # PYI016: Duplicate union member `int` | ^^^ PYI016 -20 | -21 | # Shouldn't emit for non-type unions. +22 | +23 | # Shouldn't emit for non-type unions. | = help: Remove duplicate union member `int` -ℹ Suggested fix -16 16 | field3: str | str | int # PYI016: Duplicate union member `str` -17 17 | field4: int | int | str # PYI016: Duplicate union member `int` -18 18 | field5: str | int | str # PYI016: Duplicate union member `str` -19 |-field6: int | bool | str | int # PYI016: Duplicate union member `int` - 19 |+field6: int | bool | str # PYI016: Duplicate union member `int` -20 20 | -21 21 | # Shouldn't emit for non-type unions. -22 22 | field7 = str | str +ℹ Fix +18 18 | field3: str | str | int # PYI016: Duplicate union member `str` +19 19 | field4: int | int | str # PYI016: Duplicate union member `int` +20 20 | field5: str | int | str # PYI016: Duplicate union member `str` +21 |-field6: int | bool | str | int # PYI016: Duplicate union member `int` + 21 |+field6: int | bool | str # PYI016: Duplicate union member `int` +22 22 | +23 23 | # Shouldn't emit for non-type unions. +24 24 | field7 = str | str -PYI016.pyi:25:22: PYI016 [*] Duplicate union member `int` +PYI016.pyi:27:22: PYI016 [*] Duplicate union member `int` | -24 | # Should emit for strangely-bracketed unions. -25 | field8: int | (str | int) # PYI016: Duplicate union member `int` +26 | # Should emit for strangely-bracketed unions. +27 | field8: int | (str | int) # PYI016: Duplicate union member `int` | ^^^ PYI016 -26 | -27 | # Should handle user brackets when fixing. +28 | +29 | # Should handle user brackets when fixing. | = help: Remove duplicate union member `int` -ℹ Suggested fix -22 22 | field7 = str | str -23 23 | -24 24 | # Should emit for strangely-bracketed unions. -25 |-field8: int | (str | int) # PYI016: Duplicate union member `int` - 25 |+field8: int | (str) # PYI016: Duplicate union member `int` -26 26 | -27 27 | # Should handle user brackets when fixing. -28 28 | field9: int | (int | str) # PYI016: Duplicate union member `int` +ℹ Fix +24 24 | field7 = str | str +25 25 | +26 26 | # Should emit for strangely-bracketed unions. +27 |-field8: int | (str | int) # PYI016: Duplicate union member `int` + 27 |+field8: int | (str) # PYI016: Duplicate union member `int` +28 28 | +29 29 | # Should handle user brackets when fixing. +30 30 | field9: int | (int | str) # PYI016: Duplicate union member `int` -PYI016.pyi:28:16: PYI016 [*] Duplicate union member `int` +PYI016.pyi:30:16: PYI016 [*] Duplicate union member `int` | -27 | # Should handle user brackets when fixing. -28 | field9: int | (int | str) # PYI016: Duplicate union member `int` +29 | # Should handle user brackets when fixing. +30 | field9: int | (int | str) # PYI016: Duplicate union member `int` | ^^^ PYI016 -29 | field10: (str | int) | str # PYI016: Duplicate union member `str` +31 | field10: (str | int) | str # PYI016: Duplicate union member `str` | = help: Remove duplicate union member `int` -ℹ Suggested fix -25 25 | field8: int | (str | int) # PYI016: Duplicate union member `int` -26 26 | -27 27 | # Should handle user brackets when fixing. -28 |-field9: int | (int | str) # PYI016: Duplicate union member `int` - 28 |+field9: int | (str) # PYI016: Duplicate union member `int` -29 29 | field10: (str | int) | str # PYI016: Duplicate union member `str` -30 30 | -31 31 | # Should emit for nested unions. +ℹ Fix +27 27 | field8: int | (str | int) # PYI016: Duplicate union member `int` +28 28 | +29 29 | # Should handle user brackets when fixing. +30 |-field9: int | (int | str) # PYI016: Duplicate union member `int` + 30 |+field9: int | (str) # PYI016: Duplicate union member `int` +31 31 | field10: (str | int) | str # PYI016: Duplicate union member `str` +32 32 | +33 33 | # Should emit for nested unions. -PYI016.pyi:29:24: PYI016 [*] Duplicate union member `str` +PYI016.pyi:31:24: PYI016 [*] Duplicate union member `str` | -27 | # Should handle user brackets when fixing. -28 | field9: int | (int | str) # PYI016: Duplicate union member `int` -29 | field10: (str | int) | str # PYI016: Duplicate union member `str` +29 | # Should handle user brackets when fixing. +30 | field9: int | (int | str) # PYI016: Duplicate union member `int` +31 | field10: (str | int) | str # PYI016: Duplicate union member `str` | ^^^ PYI016 -30 | -31 | # Should emit for nested unions. +32 | +33 | # Should emit for nested unions. | = help: Remove duplicate union member `str` -ℹ Suggested fix -26 26 | -27 27 | # Should handle user brackets when fixing. -28 28 | field9: int | (int | str) # PYI016: Duplicate union member `int` -29 |-field10: (str | int) | str # PYI016: Duplicate union member `str` - 29 |+field10: str | int # PYI016: Duplicate union member `str` -30 30 | -31 31 | # Should emit for nested unions. -32 32 | field11: dict[int | int, str] +ℹ Fix +28 28 | +29 29 | # Should handle user brackets when fixing. +30 30 | field9: int | (int | str) # PYI016: Duplicate union member `int` +31 |-field10: (str | int) | str # PYI016: Duplicate union member `str` + 31 |+field10: str | int # PYI016: Duplicate union member `str` +32 32 | +33 33 | # Should emit for nested unions. +34 34 | field11: dict[int | int, str] -PYI016.pyi:32:21: PYI016 [*] Duplicate union member `int` +PYI016.pyi:34:21: PYI016 [*] Duplicate union member `int` | -31 | # Should emit for nested unions. -32 | field11: dict[int | int, str] +33 | # Should emit for nested unions. +34 | field11: dict[int | int, str] | ^^^ PYI016 +35 | +36 | # Should emit for unions with more than two cases | = help: Remove duplicate union member `int` -ℹ Suggested fix -29 29 | field10: (str | int) | str # PYI016: Duplicate union member `str` -30 30 | -31 31 | # Should emit for nested unions. -32 |-field11: dict[int | int, str] - 32 |+field11: dict[int, str] +ℹ Fix +31 31 | field10: (str | int) | str # PYI016: Duplicate union member `str` +32 32 | +33 33 | # Should emit for nested unions. +34 |-field11: dict[int | int, str] + 34 |+field11: dict[int, str] +35 35 | +36 36 | # Should emit for unions with more than two cases +37 37 | field12: int | int | int # Error + +PYI016.pyi:37:16: PYI016 [*] Duplicate union member `int` + | +36 | # Should emit for unions with more than two cases +37 | field12: int | int | int # Error + | ^^^ PYI016 +38 | field13: int | int | int | int # Error + | + = help: Remove duplicate union member `int` + +ℹ Fix +34 34 | field11: dict[int | int, str] +35 35 | +36 36 | # Should emit for unions with more than two cases +37 |-field12: int | int | int # Error + 37 |+field12: int | int # Error +38 38 | field13: int | int | int | int # Error +39 39 | +40 40 | # Should emit for unions with more than two cases, even if not directly adjacent + +PYI016.pyi:37:22: PYI016 [*] Duplicate union member `int` + | +36 | # Should emit for unions with more than two cases +37 | field12: int | int | int # Error + | ^^^ PYI016 +38 | field13: int | int | int | int # Error + | + = help: Remove duplicate union member `int` + +ℹ Fix +34 34 | field11: dict[int | int, str] +35 35 | +36 36 | # Should emit for unions with more than two cases +37 |-field12: int | int | int # Error + 37 |+field12: int | int # Error +38 38 | field13: int | int | int | int # Error +39 39 | +40 40 | # Should emit for unions with more than two cases, even if not directly adjacent + +PYI016.pyi:38:16: PYI016 [*] Duplicate union member `int` + | +36 | # Should emit for unions with more than two cases +37 | field12: int | int | int # Error +38 | field13: int | int | int | int # Error + | ^^^ PYI016 +39 | +40 | # Should emit for unions with more than two cases, even if not directly adjacent + | + = help: Remove duplicate union member `int` + +ℹ Fix +35 35 | +36 36 | # Should emit for unions with more than two cases +37 37 | field12: int | int | int # Error +38 |-field13: int | int | int | int # Error + 38 |+field13: int | int | int # Error +39 39 | +40 40 | # Should emit for unions with more than two cases, even if not directly adjacent +41 41 | field14: int | int | str | int # Error + +PYI016.pyi:38:22: PYI016 [*] Duplicate union member `int` + | +36 | # Should emit for unions with more than two cases +37 | field12: int | int | int # Error +38 | field13: int | int | int | int # Error + | ^^^ PYI016 +39 | +40 | # Should emit for unions with more than two cases, even if not directly adjacent + | + = help: Remove duplicate union member `int` + +ℹ Fix +35 35 | +36 36 | # Should emit for unions with more than two cases +37 37 | field12: int | int | int # Error +38 |-field13: int | int | int | int # Error + 38 |+field13: int | int | int # Error +39 39 | +40 40 | # Should emit for unions with more than two cases, even if not directly adjacent +41 41 | field14: int | int | str | int # Error + +PYI016.pyi:38:28: PYI016 [*] Duplicate union member `int` + | +36 | # Should emit for unions with more than two cases +37 | field12: int | int | int # Error +38 | field13: int | int | int | int # Error + | ^^^ PYI016 +39 | +40 | # Should emit for unions with more than two cases, even if not directly adjacent + | + = help: Remove duplicate union member `int` + +ℹ Fix +35 35 | +36 36 | # Should emit for unions with more than two cases +37 37 | field12: int | int | int # Error +38 |-field13: int | int | int | int # Error + 38 |+field13: int | int | int # Error +39 39 | +40 40 | # Should emit for unions with more than two cases, even if not directly adjacent +41 41 | field14: int | int | str | int # Error + +PYI016.pyi:41:16: PYI016 [*] Duplicate union member `int` + | +40 | # Should emit for unions with more than two cases, even if not directly adjacent +41 | field14: int | int | str | int # Error + | ^^^ PYI016 +42 | +43 | # Should emit for duplicate literal types; also covered by PYI030 + | + = help: Remove duplicate union member `int` + +ℹ Fix +38 38 | field13: int | int | int | int # Error +39 39 | +40 40 | # Should emit for unions with more than two cases, even if not directly adjacent +41 |-field14: int | int | str | int # Error + 41 |+field14: int | str | int # Error +42 42 | +43 43 | # Should emit for duplicate literal types; also covered by PYI030 +44 44 | field15: typing.Literal[1] | typing.Literal[1] # Error + +PYI016.pyi:41:28: PYI016 [*] Duplicate union member `int` + | +40 | # Should emit for unions with more than two cases, even if not directly adjacent +41 | field14: int | int | str | int # Error + | ^^^ PYI016 +42 | +43 | # Should emit for duplicate literal types; also covered by PYI030 + | + = help: Remove duplicate union member `int` + +ℹ Fix +38 38 | field13: int | int | int | int # Error +39 39 | +40 40 | # Should emit for unions with more than two cases, even if not directly adjacent +41 |-field14: int | int | str | int # Error + 41 |+field14: int | int | str # Error +42 42 | +43 43 | # Should emit for duplicate literal types; also covered by PYI030 +44 44 | field15: typing.Literal[1] | typing.Literal[1] # Error + +PYI016.pyi:44:30: PYI016 [*] Duplicate union member `typing.Literal[1]` + | +43 | # Should emit for duplicate literal types; also covered by PYI030 +44 | field15: typing.Literal[1] | typing.Literal[1] # Error + | ^^^^^^^^^^^^^^^^^ PYI016 +45 | +46 | # Shouldn't emit if in new parent type + | + = help: Remove duplicate union member `typing.Literal[1]` + +ℹ Fix +41 41 | field14: int | int | str | int # Error +42 42 | +43 43 | # Should emit for duplicate literal types; also covered by PYI030 +44 |-field15: typing.Literal[1] | typing.Literal[1] # Error + 44 |+field15: typing.Literal[1] # Error +45 45 | +46 46 | # Shouldn't emit if in new parent type +47 47 | field16: int | dict[int, str] # OK + +PYI016.pyi:57:5: PYI016 Duplicate union member `set[int]` + | +55 | int # foo +56 | ], +57 | set[ + | _____^ +58 | | int # bar +59 | | ], + | |_____^ PYI016 +60 | ] # Error, newline and comment will not be emitted in message + | + = help: Remove duplicate union member `set[int]` + +PYI016.pyi:64:28: PYI016 Duplicate union member `int` + | +63 | # Should emit in cases with `typing.Union` instead of `|` +64 | field19: typing.Union[int, int] # Error + | ^^^ PYI016 +65 | +66 | # Should emit in cases with nested `typing.Union` + | + = help: Remove duplicate union member `int` + +PYI016.pyi:67:41: PYI016 Duplicate union member `int` + | +66 | # Should emit in cases with nested `typing.Union` +67 | field20: typing.Union[int, typing.Union[int, str]] # Error + | ^^^ PYI016 +68 | +69 | # Should emit in cases with mixed `typing.Union` and `|` + | + = help: Remove duplicate union member `int` + +PYI016.pyi:70:28: PYI016 [*] Duplicate union member `int` + | +69 | # Should emit in cases with mixed `typing.Union` and `|` +70 | field21: typing.Union[int, int | str] # Error + | ^^^ PYI016 +71 | +72 | # Should emit only once in cases with multiple nested `typing.Union` + | + = help: Remove duplicate union member `int` + +ℹ Fix +67 67 | field20: typing.Union[int, typing.Union[int, str]] # Error +68 68 | +69 69 | # Should emit in cases with mixed `typing.Union` and `|` +70 |-field21: typing.Union[int, int | str] # Error + 70 |+field21: typing.Union[int, str] # Error +71 71 | +72 72 | # Should emit only once in cases with multiple nested `typing.Union` +73 73 | field22: typing.Union[int, typing.Union[int, typing.Union[int, int]]] # Error + +PYI016.pyi:73:41: PYI016 Duplicate union member `int` + | +72 | # Should emit only once in cases with multiple nested `typing.Union` +73 | field22: typing.Union[int, typing.Union[int, typing.Union[int, int]]] # Error + | ^^^ PYI016 + | + = help: Remove duplicate union member `int` + +PYI016.pyi:73:59: PYI016 Duplicate union member `int` + | +72 | # Should emit only once in cases with multiple nested `typing.Union` +73 | field22: typing.Union[int, typing.Union[int, typing.Union[int, int]]] # Error + | ^^^ PYI016 + | + = help: Remove duplicate union member `int` + +PYI016.pyi:73:64: PYI016 Duplicate union member `int` + | +72 | # Should emit only once in cases with multiple nested `typing.Union` +73 | field22: typing.Union[int, typing.Union[int, typing.Union[int, int]]] # Error + | ^^^ PYI016 + | + = help: Remove duplicate union member `int` diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI020_PYI020.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI020_PYI020.pyi.snap index 6231707c54..09bd4a54d8 100644 --- a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI020_PYI020.pyi.snap +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI020_PYI020.pyi.snap @@ -12,7 +12,7 @@ PYI020.pyi:7:10: PYI020 [*] Quoted annotations should not be included in stubs | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 4 4 | 5 5 | import typing_extensions 6 6 | @@ -31,7 +31,7 @@ PYI020.pyi:8:15: PYI020 [*] Quoted annotations should not be included in stubs | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 5 5 | import typing_extensions 6 6 | 7 7 | def f(x: "int"): ... # Y020 Quoted annotations should never be used in stubs @@ -52,7 +52,7 @@ PYI020.pyi:9:26: PYI020 [*] Quoted annotations should not be included in stubs | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 6 6 | 7 7 | def f(x: "int"): ... # Y020 Quoted annotations should never be used in stubs 8 8 | def g(x: list["int"]): ... # Y020 Quoted annotations should never be used in stubs @@ -72,7 +72,7 @@ PYI020.pyi:13:12: PYI020 [*] Quoted annotations should not be included in stubs | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 10 10 | 11 11 | def h(w: Literal["a", "b"], x: typing.Literal["c"], y: typing_extensions.Literal["d"], z: _T) -> _T: ... 12 12 | @@ -92,7 +92,7 @@ PYI020.pyi:14:25: PYI020 [*] Quoted annotations should not be included in stubs | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 11 11 | def h(w: Literal["a", "b"], x: typing.Literal["c"], y: typing_extensions.Literal["d"], z: _T) -> _T: ... 12 12 | 13 13 | def j() -> "int": ... # Y020 Quoted annotations should never be used in stubs @@ -112,7 +112,7 @@ PYI020.pyi:16:18: PYI020 [*] Quoted annotations should not be included in stubs | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 13 13 | def j() -> "int": ... # Y020 Quoted annotations should never be used in stubs 14 14 | Alias: TypeAlias = list["int"] # Y020 Quoted annotations should never be used in stubs 15 15 | @@ -132,7 +132,7 @@ PYI020.pyi:20:8: PYI020 [*] Quoted annotations should not be included in stubs | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 17 17 | """Documented and guaranteed useful.""" # Y021 Docstrings should not be included in stubs 18 18 | 19 19 | if sys.platform == "linux": @@ -153,7 +153,7 @@ PYI020.pyi:22:8: PYI020 [*] Quoted annotations should not be included in stubs | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 19 19 | if sys.platform == "linux": 20 20 | f: "int" # Y020 Quoted annotations should never be used in stubs 21 21 | elif sys.platform == "win32": @@ -174,7 +174,7 @@ PYI020.pyi:24:8: PYI020 [*] Quoted annotations should not be included in stubs | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 21 21 | elif sys.platform == "win32": 22 22 | f: "str" # Y020 Quoted annotations should never be used in stubs 23 23 | else: diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI025_PYI025.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI025_PYI025.pyi.snap index 5c0dc3649d..6da7686105 100644 --- a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI025_PYI025.pyi.snap +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI025_PYI025.pyi.snap @@ -1,22 +1,149 @@ --- source: crates/ruff/src/rules/flake8_pyi/mod.rs --- -PYI025.pyi:4:29: PYI025 Use `from collections.abc import Set as AbstractSet` to avoid confusion with the `set` builtin - | -4 | from collections.abc import Set # PYI025 - | ^^^ PYI025 - | - = help: Alias `Set` to `AbstractSet` - -PYI025.pyi:10:5: PYI025 Use `from collections.abc import Set as AbstractSet` to avoid confusion with the `set` builtin +PYI025.pyi:8:33: PYI025 [*] Use `from collections.abc import Set as AbstractSet` to avoid confusion with the `set` builtin | - 8 | Container, - 9 | Sized, -10 | Set, # PYI025 - | ^^^ PYI025 -11 | ValuesView -12 | ) + 7 | def f(): + 8 | from collections.abc import Set # PYI025 + | ^^^ PYI025 + 9 | +10 | def f(): | = help: Alias `Set` to `AbstractSet` +ℹ Suggested fix +5 5 | from collections.abc import Container, Sized, Set as AbstractSet, ValuesView # Ok +6 6 | +7 7 | def f(): +8 |- from collections.abc import Set # PYI025 + 8 |+ from collections.abc import Set as AbstractSet # PYI025 +9 9 | +10 10 | def f(): +11 11 | from collections.abc import Container, Sized, Set, ValuesView # PYI025 + +PYI025.pyi:11:51: PYI025 [*] Use `from collections.abc import Set as AbstractSet` to avoid confusion with the `set` builtin + | +10 | def f(): +11 | from collections.abc import Container, Sized, Set, ValuesView # PYI025 + | ^^^ PYI025 +12 | +13 | def f(): + | + = help: Alias `Set` to `AbstractSet` + +ℹ Suggested fix +8 8 | from collections.abc import Set # PYI025 +9 9 | +10 10 | def f(): +11 |- from collections.abc import Container, Sized, Set, ValuesView # PYI025 + 11 |+ from collections.abc import Container, Sized, Set as AbstractSet, ValuesView # PYI025 +12 12 | +13 13 | def f(): +14 14 | """Test: local symbol renaming.""" + +PYI025.pyi:16:37: PYI025 [*] Use `from collections.abc import Set as AbstractSet` to avoid confusion with the `set` builtin + | +14 | """Test: local symbol renaming.""" +15 | if True: +16 | from collections.abc import Set + | ^^^ PYI025 +17 | else: +18 | Set = 1 + | + = help: Alias `Set` to `AbstractSet` + +ℹ Suggested fix +13 13 | def f(): +14 14 | """Test: local symbol renaming.""" +15 15 | if True: +16 |- from collections.abc import Set + 16 |+ from collections.abc import Set as AbstractSet +17 17 | else: +18 |- Set = 1 + 18 |+ AbstractSet = 1 +19 19 | +20 20 | x: Set = set() +21 21 | +22 22 | x: Set +23 23 | +24 |- del Set + 24 |+ del AbstractSet +25 25 | +26 26 | def f(): +27 |- print(Set) + 27 |+ print(AbstractSet) +28 28 | +29 29 | def Set(): +30 30 | pass + +PYI025.pyi:33:29: PYI025 [*] Use `from collections.abc import Set as AbstractSet` to avoid confusion with the `set` builtin + | +31 | print(Set) +32 | +33 | from collections.abc import Set + | ^^^ PYI025 +34 | +35 | def f(): + | + = help: Alias `Set` to `AbstractSet` + +ℹ Suggested fix +17 17 | else: +18 18 | Set = 1 +19 19 | +20 |- x: Set = set() + 20 |+ x: AbstractSet = set() +21 21 | +22 |- x: Set + 22 |+ x: AbstractSet +23 23 | +24 24 | del Set +25 25 | +-------------------------------------------------------------------------------- +30 30 | pass +31 31 | print(Set) +32 32 | +33 |-from collections.abc import Set + 33 |+from collections.abc import Set as AbstractSet +34 34 | +35 35 | def f(): +36 36 | """Test: global symbol renaming.""" +37 |- global Set + 37 |+ global AbstractSet +38 38 | +39 |- Set = 1 +40 |- print(Set) + 39 |+ AbstractSet = 1 + 40 |+ print(AbstractSet) +41 41 | +42 42 | def f(): +43 43 | """Test: nonlocal symbol renaming.""" + +PYI025.pyi:44:33: PYI025 [*] Use `from collections.abc import Set as AbstractSet` to avoid confusion with the `set` builtin + | +42 | def f(): +43 | """Test: nonlocal symbol renaming.""" +44 | from collections.abc import Set + | ^^^ PYI025 +45 | +46 | def g(): + | + = help: Alias `Set` to `AbstractSet` + +ℹ Suggested fix +41 41 | +42 42 | def f(): +43 43 | """Test: nonlocal symbol renaming.""" +44 |- from collections.abc import Set + 44 |+ from collections.abc import Set as AbstractSet +45 45 | +46 46 | def g(): +47 |- nonlocal Set + 47 |+ nonlocal AbstractSet +48 48 | +49 |- Set = 1 +50 |- print(Set) + 49 |+ AbstractSet = 1 + 50 |+ print(AbstractSet) + diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI030_PYI030.py.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI030_PYI030.py.snap new file mode 100644 index 0000000000..d1aa2e9116 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI030_PYI030.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- + diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI030_PYI030.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI030_PYI030.pyi.snap new file mode 100644 index 0000000000..42a1e51719 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI030_PYI030.pyi.snap @@ -0,0 +1,233 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- +PYI030.pyi:9:9: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]` + | + 8 | # Should emit for duplicate field types. + 9 | field2: Literal[1] | Literal[2] # Error + | ^^^^^^^^^^^^^^^^^^^^^^^ PYI030 +10 | +11 | # Should emit for union types in arguments. + | + +PYI030.pyi:12:17: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]` + | +11 | # Should emit for union types in arguments. +12 | def func1(arg1: Literal[1] | Literal[2]): # Error + | ^^^^^^^^^^^^^^^^^^^^^^^ PYI030 +13 | print(arg1) + | + +PYI030.pyi:17:16: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]` + | +16 | # Should emit for unions in return types. +17 | def func2() -> Literal[1] | Literal[2]: # Error + | ^^^^^^^^^^^^^^^^^^^^^^^ PYI030 +18 | return "my Literal[1]ing" + | + +PYI030.pyi:22:9: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]` + | +21 | # Should emit in longer unions, even if not directly adjacent. +22 | field3: Literal[1] | Literal[2] | str # Error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030 +23 | field4: str | Literal[1] | Literal[2] # Error +24 | field5: Literal[1] | str | Literal[2] # Error + | + +PYI030.pyi:23:9: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]` + | +21 | # Should emit in longer unions, even if not directly adjacent. +22 | field3: Literal[1] | Literal[2] | str # Error +23 | field4: str | Literal[1] | Literal[2] # Error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030 +24 | field5: Literal[1] | str | Literal[2] # Error +25 | field6: Literal[1] | bool | Literal[2] | str # Error + | + +PYI030.pyi:24:9: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]` + | +22 | field3: Literal[1] | Literal[2] | str # Error +23 | field4: str | Literal[1] | Literal[2] # Error +24 | field5: Literal[1] | str | Literal[2] # Error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030 +25 | field6: Literal[1] | bool | Literal[2] | str # Error + | + +PYI030.pyi:25:9: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]` + | +23 | field4: str | Literal[1] | Literal[2] # Error +24 | field5: Literal[1] | str | Literal[2] # Error +25 | field6: Literal[1] | bool | Literal[2] | str # Error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030 +26 | +27 | # Should emit for non-type unions. + | + +PYI030.pyi:28:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]` + | +27 | # Should emit for non-type unions. +28 | field7 = Literal[1] | Literal[2] # Error + | ^^^^^^^^^^^^^^^^^^^^^^^ PYI030 +29 | +30 | # Should emit for parenthesized unions. + | + +PYI030.pyi:31:9: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]` + | +30 | # Should emit for parenthesized unions. +31 | field8: Literal[1] | (Literal[2] | str) # Error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030 +32 | +33 | # Should handle user parentheses when fixing. + | + +PYI030.pyi:34:9: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]` + | +33 | # Should handle user parentheses when fixing. +34 | field9: Literal[1] | (Literal[2] | str) # Error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030 +35 | field10: (Literal[1] | str) | Literal[2] # Error + | + +PYI030.pyi:35:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]` + | +33 | # Should handle user parentheses when fixing. +34 | field9: Literal[1] | (Literal[2] | str) # Error +35 | field10: (Literal[1] | str) | Literal[2] # Error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030 +36 | +37 | # Should emit for union in generic parent type. + | + +PYI030.pyi:38:15: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]` + | +37 | # Should emit for union in generic parent type. +38 | field11: dict[Literal[1] | Literal[2], str] # Error + | ^^^^^^^^^^^^^^^^^^^^^^^ PYI030 +39 | +40 | # Should emit for unions with more than two cases + | + +PYI030.pyi:41:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2, 3]` + | +40 | # Should emit for unions with more than two cases +41 | field12: Literal[1] | Literal[2] | Literal[3] # Error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030 +42 | field13: Literal[1] | Literal[2] | Literal[3] | Literal[4] # Error + | + +PYI030.pyi:42:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2, 3, 4]` + | +40 | # Should emit for unions with more than two cases +41 | field12: Literal[1] | Literal[2] | Literal[3] # Error +42 | field13: Literal[1] | Literal[2] | Literal[3] | Literal[4] # Error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030 +43 | +44 | # Should emit for unions with more than two cases, even if not directly adjacent + | + +PYI030.pyi:45:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2, 3]` + | +44 | # Should emit for unions with more than two cases, even if not directly adjacent +45 | field14: Literal[1] | Literal[2] | str | Literal[3] # Error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030 +46 | +47 | # Should emit for unions with mixed literal internal types + | + +PYI030.pyi:48:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, "foo", True]` + | +47 | # Should emit for unions with mixed literal internal types +48 | field15: Literal[1] | Literal["foo"] | Literal[True] # Error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030 +49 | +50 | # Shouldn't emit for duplicate field types with same value; covered by Y016 + | + +PYI030.pyi:51:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 1]` + | +50 | # Shouldn't emit for duplicate field types with same value; covered by Y016 +51 | field16: Literal[1] | Literal[1] # OK + | ^^^^^^^^^^^^^^^^^^^^^^^ PYI030 +52 | +53 | # Shouldn't emit if in new parent type + | + +PYI030.pyi:60:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]` + | +59 | # Should respect name of literal type used +60 | field19: typing.Literal[1] | typing.Literal[2] # Error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030 +61 | +62 | # Should emit in cases with newlines + | + +PYI030.pyi:63:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]` + | +62 | # Should emit in cases with newlines +63 | field20: typing.Union[ + | __________^ +64 | | Literal[ +65 | | 1 # test +66 | | ], +67 | | Literal[2], +68 | | ] # Error, newline and comment will not be emitted in message + | |_^ PYI030 +69 | +70 | # Should handle multiple unions with multiple members + | + +PYI030.pyi:71:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2, 3, 4]` + | +70 | # Should handle multiple unions with multiple members +71 | field21: Literal[1, 2] | Literal[3, 4] # Error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030 +72 | +73 | # Should emit in cases with `typing.Union` instead of `|` + | + +PYI030.pyi:74:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]` + | +73 | # Should emit in cases with `typing.Union` instead of `|` +74 | field22: typing.Union[Literal[1], Literal[2]] # Error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030 +75 | +76 | # Should emit in cases with `typing_extensions.Literal` + | + +PYI030.pyi:77:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]` + | +76 | # Should emit in cases with `typing_extensions.Literal` +77 | field23: typing_extensions.Literal[1] | typing_extensions.Literal[2] # Error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030 +78 | +79 | # Should emit in cases with nested `typing.Union` + | + +PYI030.pyi:80:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]` + | +79 | # Should emit in cases with nested `typing.Union` +80 | field24: typing.Union[Literal[1], typing.Union[Literal[2], str]] # Error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030 +81 | +82 | # Should emit in cases with mixed `typing.Union` and `|` + | + +PYI030.pyi:83:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2]` + | +82 | # Should emit in cases with mixed `typing.Union` and `|` +83 | field25: typing.Union[Literal[1], Literal[2] | str] # Error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030 +84 | +85 | # Should emit only once in cases with multiple nested `typing.Union` + | + +PYI030.pyi:86:10: PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[1, 2, 3, 4]` + | +85 | # Should emit only once in cases with multiple nested `typing.Union` +86 | field24: typing.Union[Literal[1], typing.Union[Literal[2], typing.Union[Literal[3], Literal[4]]]] # Error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030 + | + + diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI036_PYI036.py.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI036_PYI036.py.snap new file mode 100644 index 0000000000..d1aa2e9116 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI036_PYI036.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- + diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI036_PYI036.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI036_PYI036.pyi.snap new file mode 100644 index 0000000000..2c3a0dc7b6 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI036_PYI036.pyi.snap @@ -0,0 +1,171 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- +PYI036.pyi:54:31: PYI036 [*] Star-args in `__exit__` should be annotated with `object` + | +53 | class BadOne: +54 | def __exit__(self, *args: Any) -> None: ... # PYI036: Bad star-args annotation + | ^^^ PYI036 +55 | async def __aexit__(self) -> None: ... # PYI036: Missing args + | + = help: Annotate star-args with `object` + +ℹ Fix +51 51 | +52 52 | +53 53 | class BadOne: +54 |- def __exit__(self, *args: Any) -> None: ... # PYI036: Bad star-args annotation + 54 |+ def __exit__(self, *args: object) -> None: ... # PYI036: Bad star-args annotation +55 55 | async def __aexit__(self) -> None: ... # PYI036: Missing args +56 56 | +57 57 | class BadTwo: + +PYI036.pyi:55:24: PYI036 If there are no star-args, `__aexit__` should have at least 3 non-keyword-only args (excluding `self`) + | +53 | class BadOne: +54 | def __exit__(self, *args: Any) -> None: ... # PYI036: Bad star-args annotation +55 | async def __aexit__(self) -> None: ... # PYI036: Missing args + | ^^^^^^ PYI036 +56 | +57 | class BadTwo: + | + +PYI036.pyi:58:38: PYI036 All arguments after the first four in `__exit__` must have a default value + | +57 | class BadTwo: +58 | def __exit__(self, typ, exc, tb, weird_extra_arg) -> None: ... # PYI036: Extra arg must have default + | ^^^^^^^^^^^^^^^ PYI036 +59 | async def __aexit__(self, typ, exc, tb, *, weird_extra_arg1, weird_extra_arg2) -> None: ...# PYI036: kwargs must have default + | + +PYI036.pyi:59:48: PYI036 All keyword-only arguments in `__aexit__` must have a default value + | +57 | class BadTwo: +58 | def __exit__(self, typ, exc, tb, weird_extra_arg) -> None: ... # PYI036: Extra arg must have default +59 | async def __aexit__(self, typ, exc, tb, *, weird_extra_arg1, weird_extra_arg2) -> None: ...# PYI036: kwargs must have default + | ^^^^^^^^^^^^^^^^ PYI036 +60 | +61 | class BadThree: + | + +PYI036.pyi:59:66: PYI036 All keyword-only arguments in `__aexit__` must have a default value + | +57 | class BadTwo: +58 | def __exit__(self, typ, exc, tb, weird_extra_arg) -> None: ... # PYI036: Extra arg must have default +59 | async def __aexit__(self, typ, exc, tb, *, weird_extra_arg1, weird_extra_arg2) -> None: ...# PYI036: kwargs must have default + | ^^^^^^^^^^^^^^^^ PYI036 +60 | +61 | class BadThree: + | + +PYI036.pyi:62:29: PYI036 The first argument in `__exit__` should be annotated with `object` or `type[BaseException] | None` + | +61 | class BadThree: +62 | def __exit__(self, typ: type[BaseException], exc: BaseException | None, tb: TracebackType | None) -> None: ... # PYI036: First arg has bad annotation + | ^^^^^^^^^^^^^^^^^^^ PYI036 +63 | async def __aexit__(self, __typ: type[BaseException] | None, __exc: BaseException, __tb: TracebackType) -> bool | None: ... # PYI036: Second arg has bad annotation + | + +PYI036.pyi:63:73: PYI036 The second argument in `__aexit__` should be annotated with `object` or `BaseException | None` + | +61 | class BadThree: +62 | def __exit__(self, typ: type[BaseException], exc: BaseException | None, tb: TracebackType | None) -> None: ... # PYI036: First arg has bad annotation +63 | async def __aexit__(self, __typ: type[BaseException] | None, __exc: BaseException, __tb: TracebackType) -> bool | None: ... # PYI036: Second arg has bad annotation + | ^^^^^^^^^^^^^ PYI036 +64 | +65 | class BadFour: + | + +PYI036.pyi:63:94: PYI036 The third argument in `__aexit__` should be annotated with `object` or `types.TracebackType | None` + | +61 | class BadThree: +62 | def __exit__(self, typ: type[BaseException], exc: BaseException | None, tb: TracebackType | None) -> None: ... # PYI036: First arg has bad annotation +63 | async def __aexit__(self, __typ: type[BaseException] | None, __exc: BaseException, __tb: TracebackType) -> bool | None: ... # PYI036: Second arg has bad annotation + | ^^^^^^^^^^^^^ PYI036 +64 | +65 | class BadFour: + | + +PYI036.pyi:66:111: PYI036 The third argument in `__exit__` should be annotated with `object` or `types.TracebackType | None` + | +65 | class BadFour: +66 | def __exit__(self, typ: typing.Optional[type[BaseException]], exc: typing.Union[BaseException, None], tb: TracebackType) -> None: ... # PYI036: Third arg has bad annotation + | ^^^^^^^^^^^^^ PYI036 +67 | async def __aexit__(self, __typ: type[BaseException] | None, __exc: BaseException | None, __tb: typing.Union[TracebackType, None, int]) -> bool | None: ... # PYI036: Third arg has bad annotation + | + +PYI036.pyi:67:101: PYI036 The third argument in `__aexit__` should be annotated with `object` or `types.TracebackType | None` + | +65 | class BadFour: +66 | def __exit__(self, typ: typing.Optional[type[BaseException]], exc: typing.Union[BaseException, None], tb: TracebackType) -> None: ... # PYI036: Third arg has bad annotation +67 | async def __aexit__(self, __typ: type[BaseException] | None, __exc: BaseException | None, __tb: typing.Union[TracebackType, None, int]) -> bool | None: ... # PYI036: Third arg has bad annotation + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI036 +68 | +69 | class BadFive: + | + +PYI036.pyi:70:29: PYI036 The first argument in `__exit__` should be annotated with `object` or `type[BaseException] | None` + | +69 | class BadFive: +70 | def __exit__(self, typ: BaseException | None, *args: list[str]) -> bool: ... # PYI036: Bad star-args annotation + | ^^^^^^^^^^^^^^^^^^^^ PYI036 +71 | async def __aexit__(self, /, typ: type[BaseException] | None, *args: Any) -> Awaitable[None]: ... # PYI036: Bad star-args annotation + | + +PYI036.pyi:70:58: PYI036 [*] Star-args in `__exit__` should be annotated with `object` + | +69 | class BadFive: +70 | def __exit__(self, typ: BaseException | None, *args: list[str]) -> bool: ... # PYI036: Bad star-args annotation + | ^^^^^^^^^ PYI036 +71 | async def __aexit__(self, /, typ: type[BaseException] | None, *args: Any) -> Awaitable[None]: ... # PYI036: Bad star-args annotation + | + = help: Annotate star-args with `object` + +ℹ Fix +67 67 | async def __aexit__(self, __typ: type[BaseException] | None, __exc: BaseException | None, __tb: typing.Union[TracebackType, None, int]) -> bool | None: ... # PYI036: Third arg has bad annotation +68 68 | +69 69 | class BadFive: +70 |- def __exit__(self, typ: BaseException | None, *args: list[str]) -> bool: ... # PYI036: Bad star-args annotation + 70 |+ def __exit__(self, typ: BaseException | None, *args: object) -> bool: ... # PYI036: Bad star-args annotation +71 71 | async def __aexit__(self, /, typ: type[BaseException] | None, *args: Any) -> Awaitable[None]: ... # PYI036: Bad star-args annotation +72 72 | +73 73 | class BadSix: + +PYI036.pyi:71:74: PYI036 [*] Star-args in `__aexit__` should be annotated with `object` + | +69 | class BadFive: +70 | def __exit__(self, typ: BaseException | None, *args: list[str]) -> bool: ... # PYI036: Bad star-args annotation +71 | async def __aexit__(self, /, typ: type[BaseException] | None, *args: Any) -> Awaitable[None]: ... # PYI036: Bad star-args annotation + | ^^^ PYI036 +72 | +73 | class BadSix: + | + = help: Annotate star-args with `object` + +ℹ Fix +68 68 | +69 69 | class BadFive: +70 70 | def __exit__(self, typ: BaseException | None, *args: list[str]) -> bool: ... # PYI036: Bad star-args annotation +71 |- async def __aexit__(self, /, typ: type[BaseException] | None, *args: Any) -> Awaitable[None]: ... # PYI036: Bad star-args annotation + 71 |+ async def __aexit__(self, /, typ: type[BaseException] | None, *args: object) -> Awaitable[None]: ... # PYI036: Bad star-args annotation +72 72 | +73 73 | class BadSix: +74 74 | def __exit__(self, typ, exc, tb, weird_extra_arg, extra_arg2 = None) -> None: ... # PYI036: Extra arg must have default + +PYI036.pyi:74:38: PYI036 All arguments after the first four in `__exit__` must have a default value + | +73 | class BadSix: +74 | def __exit__(self, typ, exc, tb, weird_extra_arg, extra_arg2 = None) -> None: ... # PYI036: Extra arg must have default + | ^^^^^^^^^^^^^^^ PYI036 +75 | async def __aexit__(self, typ, exc, tb, *, weird_extra_arg) -> None: ... # PYI036: kwargs must have default + | + +PYI036.pyi:75:48: PYI036 All keyword-only arguments in `__aexit__` must have a default value + | +73 | class BadSix: +74 | def __exit__(self, typ, exc, tb, weird_extra_arg, extra_arg2 = None) -> None: ... # PYI036: Extra arg must have default +75 | async def __aexit__(self, typ, exc, tb, *, weird_extra_arg) -> None: ... # PYI036: kwargs must have default + | ^^^^^^^^^^^^^^^ PYI036 + | + + diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI044_PYI044.py.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI044_PYI044.py.snap new file mode 100644 index 0000000000..d1aa2e9116 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI044_PYI044.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- + diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI044_PYI044.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI044_PYI044.pyi.snap new file mode 100644 index 0000000000..5e52d03969 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI044_PYI044.pyi.snap @@ -0,0 +1,13 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- +PYI044.pyi:2:1: PYI044 `from __future__ import annotations` has no effect in stub files, since type checkers automatically treat stubs as having those semantics + | +1 | # Bad import. +2 | from __future__ import annotations # PYI044. + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI044 +3 | +4 | # Good imports. + | + + diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI053_PYI053.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI053_PYI053.pyi.snap index 275d11d8b4..951ca3acb1 100644 --- a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI053_PYI053.pyi.snap +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI053_PYI053.pyi.snap @@ -89,6 +89,8 @@ PYI053.pyi:30:14: PYI053 [*] String and bytes literals longer than 50 characters 29 | 30 | qux: bytes = b"51 character byte stringggggggggggggggggggggggggggg\xff" # Error: PYI053 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI053 +31 | +32 | class Demo: | = help: Replace with `...` @@ -98,5 +100,8 @@ PYI053.pyi:30:14: PYI053 [*] String and bytes literals longer than 50 characters 29 29 | 30 |-qux: bytes = b"51 character byte stringggggggggggggggggggggggggggg\xff" # Error: PYI053 30 |+qux: bytes = ... # Error: PYI053 +31 31 | +32 32 | class Demo: +33 33 | """Docstrings are excluded from this rule. Some padding.""" # OK diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs index ee2f4f7c10..363b327c8c 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs @@ -3,13 +3,12 @@ use std::borrow::Cow; use anyhow::bail; use anyhow::Result; use libcst_native::{ - Assert, BooleanOp, CompoundStatement, Expression, ParenthesizableWhitespace, ParenthesizedNode, - SimpleStatementLine, SimpleWhitespace, SmallStatement, Statement, TrailingWhitespace, UnaryOp, - UnaryOperation, + self, Assert, BooleanOp, CompoundStatement, Expression, ParenthesizableWhitespace, + ParenthesizedNode, SimpleStatementLine, SimpleWhitespace, SmallStatement, Statement, + TrailingWhitespace, UnaryOperation, }; -use rustpython_parser::ast::{self, Boolop, Excepthandler, Expr, Keyword, Ranged, Stmt, Unaryop}; +use rustpython_parser::ast::{self, BoolOp, ExceptHandler, Expr, Keyword, Ranged, Stmt, UnaryOp}; -use crate::autofix::codemods::CodegenStylist; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::{has_comments_in, Truthiness}; @@ -17,6 +16,7 @@ use ruff_python_ast::source_code::{Locator, Stylist}; use ruff_python_ast::visitor::Visitor; use ruff_python_ast::{visitor, whitespace}; +use crate::autofix::codemods::CodegenStylist; use crate::checkers::ast::Checker; use crate::cst::matchers::match_indented_block; use crate::cst::matchers::match_module; @@ -194,14 +194,13 @@ pub(crate) fn unittest_assertion( if checker.patch(diagnostic.kind.rule()) { // We're converting an expression to a statement, so avoid applying the fix if // the assertion is part of a larger expression. - if checker.semantic_model().stmt().is_expr_stmt() - && checker.semantic_model().expr_parent().is_none() - && !checker.semantic_model().scope().kind.is_lambda() + if checker.semantic().stmt().is_expr_stmt() + && checker.semantic().expr_parent().is_none() + && !checker.semantic().scope().kind.is_lambda() && !has_comments_in(expr.range(), checker.locator) { if let Ok(stmt) = unittest_assert.generate_assert(args, keywords) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( checker.generator().stmt(&stmt), expr.range(), ))); @@ -219,7 +218,7 @@ pub(crate) fn unittest_assertion( /// PT015 pub(crate) fn assert_falsy(checker: &mut Checker, stmt: &Stmt, test: &Expr) { - if Truthiness::from_expr(test, |id| checker.semantic_model().is_builtin(id)).is_falsey() { + if Truthiness::from_expr(test, |id| checker.semantic().is_builtin(id)).is_falsey() { checker .diagnostics .push(Diagnostic::new(PytestAssertAlwaysFalse, stmt.range())); @@ -227,11 +226,11 @@ pub(crate) fn assert_falsy(checker: &mut Checker, stmt: &Stmt, test: &Expr) { } /// PT017 -pub(crate) fn assert_in_exception_handler(handlers: &[Excepthandler]) -> Vec { - handlers - .iter() - .flat_map(|handler| match handler { - Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { +pub(crate) fn assert_in_exception_handler(checker: &mut Checker, handlers: &[ExceptHandler]) { + checker + .diagnostics + .extend(handlers.iter().flat_map(|handler| match handler { + ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { name, body, .. }) => { if let Some(name) = name { @@ -240,8 +239,7 @@ pub(crate) fn assert_in_exception_handler(handlers: &[Excepthandler]) -> Vec CompositionKind { match test { Expr::BoolOp(ast::ExprBoolOp { - op: Boolop::And, .. + op: BoolOp::And, .. }) => { return CompositionKind::Simple; } Expr::UnaryOp(ast::ExprUnaryOp { - op: Unaryop::Not, + op: UnaryOp::Not, operand, range: _, }) => { if let Expr::BoolOp(ast::ExprBoolOp { - op: Boolop::Or, + op: BoolOp::Or, values, range: _, }) = operand.as_ref() @@ -282,7 +280,7 @@ fn is_composite_condition(test: &Expr) -> CompositionKind { !matches!( expr, Expr::BoolOp(ast::ExprBoolOp { - op: Boolop::And, + op: BoolOp::And, .. }) ) @@ -301,12 +299,12 @@ fn is_composite_condition(test: &Expr) -> CompositionKind { /// Negate a condition, i.e., `a` => `not a` and `not a` => `a`. fn negate<'a>(expression: &Expression<'a>) -> Expression<'a> { if let Expression::UnaryOperation(ref expression) = expression { - if matches!(expression.operator, UnaryOp::Not { .. }) { + if matches!(expression.operator, libcst_native::UnaryOp::Not { .. }) { return *expression.expression.clone(); } } Expression::UnaryOperation(Box::new(UnaryOperation { - operator: UnaryOp::Not { + operator: libcst_native::UnaryOp::Not { whitespace_after: ParenthesizableWhitespace::SimpleWhitespace(SimpleWhitespace(" ")), }, expression: Box::new(expression.clone()), @@ -315,6 +313,54 @@ fn negate<'a>(expression: &Expression<'a>) -> Expression<'a> { })) } +/// Propagate parentheses from a parent to a child expression, if necessary. +/// +/// For example, when splitting: +/// ```python +/// assert (a and b == +/// """) +/// ``` +/// +/// The parentheses need to be propagated to the right-most expression: +/// ```python +/// assert a +/// assert (b == +/// "") +/// ``` +fn parenthesize<'a>(expression: Expression<'a>, parent: &Expression<'a>) -> Expression<'a> { + if matches!( + expression, + Expression::Comparison(_) + | Expression::UnaryOperation(_) + | Expression::BinaryOperation(_) + | Expression::BooleanOperation(_) + | Expression::Attribute(_) + | Expression::Tuple(_) + | Expression::Call(_) + | Expression::GeneratorExp(_) + | Expression::ListComp(_) + | Expression::SetComp(_) + | Expression::DictComp(_) + | Expression::List(_) + | Expression::Set(_) + | Expression::Dict(_) + | Expression::Subscript(_) + | Expression::StarredElement(_) + | Expression::IfExp(_) + | Expression::Lambda(_) + | Expression::Yield(_) + | Expression::Await(_) + | Expression::ConcatenatedString(_) + | Expression::FormattedString(_) + | Expression::NamedExpr(_) + ) { + if let (Some(left), Some(right)) = (parent.lpar().first(), parent.rpar().first()) { + return expression.with_parens(left.clone(), right.clone()); + } + } + expression +} + /// Replace composite condition `assert a == "hello" and b == "world"` with two statements /// `assert a == "hello"` and `assert b == "world"`. fn fix_composite_condition(stmt: &Stmt, locator: &Locator, stylist: &Stylist) -> Result { @@ -345,7 +391,8 @@ fn fix_composite_condition(stmt: &Stmt, locator: &Locator, stylist: &Stylist) -> let statements = if outer_indent.is_empty() { &mut tree.body } else { - let [Statement::Compound(CompoundStatement::FunctionDef(embedding))] = &mut *tree.body else { + let [Statement::Compound(CompoundStatement::FunctionDef(embedding))] = &mut *tree.body + else { bail!("Expected statement to be embedded in a function definition") }; @@ -363,19 +410,15 @@ fn fix_composite_condition(stmt: &Stmt, locator: &Locator, stylist: &Stylist) -> bail!("Expected simple statement to be an assert") }; - if !(assert_statement.test.lpar().is_empty() && assert_statement.test.rpar().is_empty()) { - bail!("Unable to split parenthesized condition"); - } - // Extract the individual conditions. let mut conditions: Vec = Vec::with_capacity(2); match &assert_statement.test { Expression::UnaryOperation(op) => { - if matches!(op.operator, UnaryOp::Not { .. }) { + if matches!(op.operator, libcst_native::UnaryOp::Not { .. }) { if let Expression::BooleanOperation(op) = &*op.expression { if matches!(op.operator, BooleanOp::Or { .. }) { - conditions.push(negate(&op.left)); - conditions.push(negate(&op.right)); + conditions.push(parenthesize(negate(&op.left), &assert_statement.test)); + conditions.push(parenthesize(negate(&op.right), &assert_statement.test)); } else { bail!("Expected assert statement to be a composite condition"); } @@ -386,8 +429,8 @@ fn fix_composite_condition(stmt: &Stmt, locator: &Locator, stylist: &Stylist) -> } Expression::BooleanOperation(op) => { if matches!(op.operator, BooleanOp::And { .. }) { - conditions.push(*op.left.clone()); - conditions.push(*op.right.clone()); + conditions.push(parenthesize(*op.left.clone(), &assert_statement.test)); + conditions.push(parenthesize(*op.right.clone(), &assert_statement.test)); } else { bail!("Expected assert statement to be a composite condition"); } diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/fail.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/fail.rs index 6faa0915f4..7c840db2a1 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/fail.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/fail.rs @@ -19,9 +19,14 @@ impl Violation for PytestFailWithoutMessage { } pub(crate) fn fail_call(checker: &mut Checker, func: &Expr, args: &[Expr], keywords: &[Keyword]) { - if is_pytest_fail(checker.semantic_model(), func) { + if is_pytest_fail(func, checker.semantic()) { let call_args = SimpleCallArgs::new(args, keywords); - let msg = call_args.argument("msg", 0); + + // Allow either `pytest.fail(reason="...")` (introduced in pytest 7.0) or + // `pytest.fail(msg="...")` (deprecated in pytest 7.0) + let msg = call_args + .argument("reason", 0) + .or_else(|| call_args.argument("msg", 0)); if let Some(msg) = msg { if is_empty_or_null_string(msg) { diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/fixture.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/fixture.rs index c0c53e8e3a..70aad572bc 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/fixture.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/fixture.rs @@ -1,20 +1,19 @@ use std::fmt; -use anyhow::Result; -use ruff_text_size::{TextLen, TextRange, TextSize}; -use rustpython_parser::ast::{self, Arguments, Expr, Keyword, Ranged, Stmt}; +use ruff_text_size::{TextLen, TextRange}; +use rustpython_parser::ast::Decorator; +use rustpython_parser::ast::{self, ArgWithDefault, Arguments, Expr, Ranged, Stmt}; use ruff_diagnostics::{AlwaysAutofixableViolation, Violation}; use ruff_diagnostics::{Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::call_path::collect_call_path; -use ruff_python_ast::helpers::collect_arg_names; -use ruff_python_ast::prelude::Decorator; -use ruff_python_ast::source_code::Locator; +use ruff_python_ast::helpers::includes_arg_name; +use ruff_python_ast::identifier::Identifier; +use ruff_python_ast::visitor; use ruff_python_ast::visitor::Visitor; -use ruff_python_ast::{helpers, visitor}; use ruff_python_semantic::analyze::visibility::is_abstract; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; use crate::autofix::edits::remove_argument; use crate::checkers::ast::Checker; @@ -24,21 +23,6 @@ use super::helpers::{ get_mark_decorators, is_pytest_fixture, is_pytest_yield_fixture, keyword_is_literal, }; -#[derive(Debug, PartialEq, Eq)] -pub(crate) enum Parentheses { - None, - Empty, -} - -impl fmt::Display for Parentheses { - fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { - match self { - Parentheses::None => fmt.write_str(""), - Parentheses::Empty => fmt.write_str("()"), - } - } -} - #[violation] pub struct PytestFixtureIncorrectParenthesesStyle { expected: Parentheses, @@ -195,8 +179,23 @@ impl AlwaysAutofixableViolation for PytestUnnecessaryAsyncioMarkOnFixture { } } -#[derive(Default)] +#[derive(Debug, PartialEq, Eq)] +enum Parentheses { + None, + Empty, +} + +impl fmt::Display for Parentheses { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match self { + Parentheses::None => fmt.write_str(""), + Parentheses::Empty => fmt.write_str("()"), + } + } +} + /// Visitor that skips functions +#[derive(Debug, Default)] struct SkipFunctionsVisitor<'a> { has_return_with_value: bool, has_yield_from: bool, @@ -233,7 +232,7 @@ where } Expr::Call(ast::ExprCall { func, .. }) => { if collect_call_path(func).map_or(false, |call_path| { - call_path.as_slice() == ["request", "addfinalizer"] + matches!(call_path.as_slice(), ["request", "addfinalizer"]) }) { self.addfinalizer_call = Some(expr); }; @@ -244,12 +243,12 @@ where } } -fn get_fixture_decorator<'a>( - model: &SemanticModel, +fn fixture_decorator<'a>( decorators: &'a [Decorator], + semantic: &SemanticModel, ) -> Option<&'a Decorator> { decorators.iter().find(|decorator| { - is_pytest_fixture(model, decorator) || is_pytest_yield_fixture(model, decorator) + is_pytest_fixture(decorator, semantic) || is_pytest_yield_fixture(decorator, semantic) }) } @@ -270,16 +269,6 @@ fn pytest_fixture_parentheses( checker.diagnostics.push(diagnostic); } -pub(crate) fn fix_extraneous_scope_function( - locator: &Locator, - stmt_at: TextSize, - expr_range: TextRange, - args: &[Expr], - keywords: &[Keyword], -) -> Result { - remove_argument(locator, stmt_at, expr_range, args, keywords, false) -} - /// PT001, PT002, PT003 fn check_fixture_decorator(checker: &mut Checker, func_name: &str, decorator: &Decorator) { match &decorator.expression { @@ -289,29 +278,31 @@ fn check_fixture_decorator(checker: &mut Checker, func_name: &str, decorator: &D keywords, range: _, }) => { - if checker.enabled(Rule::PytestFixtureIncorrectParenthesesStyle) - && !checker.settings.flake8_pytest_style.fixture_parentheses - && args.is_empty() - && keywords.is_empty() - { - #[allow(deprecated)] - let fix = Fix::unspecified(Edit::deletion(func.end(), decorator.end())); - pytest_fixture_parentheses( - checker, - decorator, - fix, - Parentheses::None, - Parentheses::Empty, - ); + if checker.enabled(Rule::PytestFixtureIncorrectParenthesesStyle) { + if !checker.settings.flake8_pytest_style.fixture_parentheses + && args.is_empty() + && keywords.is_empty() + { + let fix = Fix::automatic(Edit::deletion(func.end(), decorator.end())); + pytest_fixture_parentheses( + checker, + decorator, + fix, + Parentheses::None, + Parentheses::Empty, + ); + } } - if checker.enabled(Rule::PytestFixturePositionalArgs) && !args.is_empty() { - checker.diagnostics.push(Diagnostic::new( - PytestFixturePositionalArgs { - function: func_name.to_string(), - }, - decorator.range(), - )); + if checker.enabled(Rule::PytestFixturePositionalArgs) { + if !args.is_empty() { + checker.diagnostics.push(Diagnostic::new( + PytestFixturePositionalArgs { + function: func_name.to_string(), + }, + decorator.range(), + )); + } } if checker.enabled(Rule::PytestExtraneousScopeFunction) { @@ -324,16 +315,16 @@ fn check_fixture_decorator(checker: &mut Checker, func_name: &str, decorator: &D let mut diagnostic = Diagnostic::new(PytestExtraneousScopeFunction, scope_keyword.range()); if checker.patch(diagnostic.kind.rule()) { - let expr_range = diagnostic.range(); - #[allow(deprecated)] - diagnostic.try_set_fix_from_edit(|| { - fix_extraneous_scope_function( + diagnostic.try_set_fix(|| { + remove_argument( checker.locator, - decorator.start(), - expr_range, + func.end(), + scope_keyword.range, args, keywords, + false, ) + .map(Fix::suggested) }); } checker.diagnostics.push(diagnostic); @@ -342,21 +333,20 @@ fn check_fixture_decorator(checker: &mut Checker, func_name: &str, decorator: &D } } _ => { - if checker.enabled(Rule::PytestFixtureIncorrectParenthesesStyle) - && checker.settings.flake8_pytest_style.fixture_parentheses - { - #[allow(deprecated)] - let fix = Fix::unspecified(Edit::insertion( - Parentheses::Empty.to_string(), - decorator.end(), - )); - pytest_fixture_parentheses( - checker, - decorator, - fix, - Parentheses::Empty, - Parentheses::None, - ); + if checker.enabled(Rule::PytestFixtureIncorrectParenthesesStyle) { + if checker.settings.flake8_pytest_style.fixture_parentheses { + let fix = Fix::automatic(Edit::insertion( + Parentheses::Empty.to_string(), + decorator.end(), + )); + pytest_fixture_parentheses( + checker, + decorator, + fix, + Parentheses::Empty, + Parentheses::None, + ); + } } } } @@ -378,7 +368,7 @@ fn check_fixture_returns(checker: &mut Checker, stmt: &Stmt, name: &str, body: & PytestIncorrectFixtureNameUnderscore { function: name.to_string(), }, - helpers::identifier_range(stmt, checker.locator), + stmt.identifier(), )); } else if checker.enabled(Rule::PytestMissingFixtureNameUnderscore) && !visitor.has_return_with_value @@ -389,7 +379,7 @@ fn check_fixture_returns(checker: &mut Checker, stmt: &Stmt, name: &str, body: & PytestMissingFixtureNameUnderscore { function: name.to_string(), }, - helpers::identifier_range(stmt, checker.locator), + stmt.identifier(), )); } @@ -405,8 +395,7 @@ fn check_fixture_returns(checker: &mut Checker, stmt: &Stmt, name: &str, body: & stmt.range(), ); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( "return".to_string(), TextRange::at(stmt.start(), "yield".text_len()), ))); @@ -420,23 +409,34 @@ fn check_fixture_returns(checker: &mut Checker, stmt: &Stmt, name: &str, body: & } /// PT019 -fn check_test_function_args(checker: &mut Checker, args: &Arguments) { - args.args.iter().chain(&args.kwonlyargs).for_each(|arg| { - let name = &arg.arg; - if name.starts_with('_') { - checker.diagnostics.push(Diagnostic::new( - PytestFixtureParamWithoutValue { - name: name.to_string(), - }, - arg.range(), - )); - } - }); +fn check_test_function_args(checker: &mut Checker, arguments: &Arguments) { + arguments + .posonlyargs + .iter() + .chain(&arguments.args) + .chain(&arguments.kwonlyargs) + .for_each( + |ArgWithDefault { + def, + default: _, + range: _, + }| { + let name = &def.arg; + if name.starts_with('_') { + checker.diagnostics.push(Diagnostic::new( + PytestFixtureParamWithoutValue { + name: name.to_string(), + }, + def.range(), + )); + } + }, + ); } /// PT020 fn check_fixture_decorator_name(checker: &mut Checker, decorator: &Decorator) { - if is_pytest_yield_fixture(checker.semantic_model(), decorator) { + if is_pytest_yield_fixture(decorator, checker.semantic()) { checker.diagnostics.push(Diagnostic::new( PytestDeprecatedYieldFixture, decorator.range(), @@ -446,7 +446,7 @@ fn check_fixture_decorator_name(checker: &mut Checker, decorator: &Decorator) { /// PT021 fn check_fixture_addfinalizer(checker: &mut Checker, args: &Arguments, body: &[Stmt]) { - if !collect_arg_names(args).contains(&"request") { + if !includes_arg_name("request", args) { return; } @@ -474,8 +474,7 @@ fn check_fixture_marks(checker: &mut Checker, decorators: &[Decorator]) { Diagnostic::new(PytestUnnecessaryAsyncioMarkOnFixture, expr.range()); if checker.patch(diagnostic.kind.rule()) { let range = checker.locator.full_lines_range(expr.range()); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_deletion(range))); + diagnostic.set_fix(Fix::automatic(Edit::range_deletion(range))); } checker.diagnostics.push(diagnostic); } @@ -487,8 +486,7 @@ fn check_fixture_marks(checker: &mut Checker, decorators: &[Decorator]) { Diagnostic::new(PytestErroneousUseFixturesOnFixture, expr.range()); if checker.patch(diagnostic.kind.rule()) { let line_range = checker.locator.full_lines_range(expr.range()); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_deletion(line_range))); + diagnostic.set_fix(Fix::automatic(Edit::range_deletion(line_range))); } checker.diagnostics.push(diagnostic); } @@ -504,7 +502,7 @@ pub(crate) fn fixture( decorators: &[Decorator], body: &[Stmt], ) { - let decorator = get_fixture_decorator(checker.semantic_model(), decorators); + let decorator = fixture_decorator(decorators, checker.semantic()); if let Some(decorator) = decorator { if checker.enabled(Rule::PytestFixtureIncorrectParenthesesStyle) || checker.enabled(Rule::PytestFixturePositionalArgs) @@ -522,7 +520,7 @@ pub(crate) fn fixture( if (checker.enabled(Rule::PytestMissingFixtureNameUnderscore) || checker.enabled(Rule::PytestIncorrectFixtureNameUnderscore) || checker.enabled(Rule::PytestUselessYieldFixture)) - && !is_abstract(checker.semantic_model(), decorators) + && !is_abstract(decorators, checker.semantic()) { check_fixture_returns(checker, stmt, name, body); } diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/helpers.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/helpers.rs index f753163036..7c175d3039 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/helpers.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/helpers.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::{self, Constant, Decorator, Expr, Keyword}; use ruff_python_ast::call_path::{collect_call_path, CallPath}; use ruff_python_ast::helpers::map_callable; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; use ruff_python_whitespace::PythonWhitespace; pub(super) fn get_mark_decorators( @@ -20,41 +20,41 @@ pub(super) fn get_mark_decorators( }) } -pub(super) fn is_pytest_fail(model: &SemanticModel, call: &Expr) -> bool { - model.resolve_call_path(call).map_or(false, |call_path| { - call_path.as_slice() == ["pytest", "fail"] +pub(super) fn is_pytest_fail(call: &Expr, semantic: &SemanticModel) -> bool { + semantic.resolve_call_path(call).map_or(false, |call_path| { + matches!(call_path.as_slice(), ["pytest", "fail"]) }) } -pub(super) fn is_pytest_fixture(model: &SemanticModel, decorator: &Decorator) -> bool { - model +pub(super) fn is_pytest_fixture(decorator: &Decorator, semantic: &SemanticModel) -> bool { + semantic .resolve_call_path(map_callable(&decorator.expression)) .map_or(false, |call_path| { - call_path.as_slice() == ["pytest", "fixture"] + matches!(call_path.as_slice(), ["pytest", "fixture"]) }) } -pub(super) fn is_pytest_yield_fixture(model: &SemanticModel, decorator: &Decorator) -> bool { - model +pub(super) fn is_pytest_yield_fixture(decorator: &Decorator, semantic: &SemanticModel) -> bool { + semantic .resolve_call_path(map_callable(&decorator.expression)) .map_or(false, |call_path| { - call_path.as_slice() == ["pytest", "yield_fixture"] + matches!(call_path.as_slice(), ["pytest", "yield_fixture"]) }) } -pub(super) fn is_pytest_parametrize(model: &SemanticModel, decorator: &Decorator) -> bool { - model +pub(super) fn is_pytest_parametrize(decorator: &Decorator, semantic: &SemanticModel) -> bool { + semantic .resolve_call_path(map_callable(&decorator.expression)) .map_or(false, |call_path| { - call_path.as_slice() == ["pytest", "mark", "parametrize"] + matches!(call_path.as_slice(), ["pytest", "mark", "parametrize"]) }) } -pub(super) fn keyword_is_literal(kw: &Keyword, literal: &str) -> bool { +pub(super) fn keyword_is_literal(keyword: &Keyword, literal: &str) -> bool { if let Expr::Constant(ast::ExprConstant { value: Constant::Str(string), .. - }) = &kw.value + }) = &keyword.value { string == literal } else { diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/marks.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/marks.rs index 40b34ea309..f13040575f 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/marks.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/marks.rs @@ -83,15 +83,13 @@ fn check_mark_parentheses(checker: &mut Checker, decorator: &Decorator, call_pat && args.is_empty() && keywords.is_empty() { - #[allow(deprecated)] - let fix = Fix::unspecified(Edit::deletion(func.end(), decorator.end())); + let fix = Fix::automatic(Edit::deletion(func.end(), decorator.end())); pytest_mark_parentheses(checker, decorator, call_path, fix, "", "()"); } } _ => { if checker.settings.flake8_pytest_style.mark_parentheses { - #[allow(deprecated)] - let fix = Fix::unspecified(Edit::insertion("()".to_string(), decorator.end())); + let fix = Fix::automatic(Edit::insertion("()".to_string(), decorator.end())); pytest_mark_parentheses(checker, decorator, call_path, fix, "()", ""); } } @@ -114,8 +112,7 @@ fn check_useless_usefixtures(checker: &mut Checker, decorator: &Decorator, call_ if !has_parameters { let mut diagnostic = Diagnostic::new(PytestUseFixturesWithoutParameters, decorator.range()); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_deletion(decorator.range()))); + diagnostic.set_fix(Fix::suggested(Edit::range_deletion(decorator.range()))); } checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/mod.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/mod.rs index dcf1277056..783431e396 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/mod.rs @@ -1,29 +1,11 @@ -pub(crate) use assertion::{ - assert_falsy, assert_in_exception_handler, composite_condition, unittest_assertion, - PytestAssertAlwaysFalse, PytestAssertInExcept, PytestCompositeAssertion, - PytestUnittestAssertion, -}; -pub(crate) use fail::{fail_call, PytestFailWithoutMessage}; -pub(crate) use fixture::{ - fixture, PytestDeprecatedYieldFixture, PytestErroneousUseFixturesOnFixture, - PytestExtraneousScopeFunction, PytestFixtureFinalizerCallback, - PytestFixtureIncorrectParenthesesStyle, PytestFixtureParamWithoutValue, - PytestFixturePositionalArgs, PytestIncorrectFixtureNameUnderscore, - PytestMissingFixtureNameUnderscore, PytestUnnecessaryAsyncioMarkOnFixture, - PytestUselessYieldFixture, -}; -pub(crate) use imports::{import, import_from, PytestIncorrectPytestImport}; -pub(crate) use marks::{ - marks, PytestIncorrectMarkParenthesesStyle, PytestUseFixturesWithoutParameters, -}; -pub(crate) use parametrize::{ - parametrize, PytestParametrizeNamesWrongType, PytestParametrizeValuesWrongType, -}; -pub(crate) use patch::{patch_with_lambda, PytestPatchWithLambda}; -pub(crate) use raises::{ - complex_raises, raises_call, PytestRaisesTooBroad, PytestRaisesWithMultipleStatements, - PytestRaisesWithoutException, -}; +pub(crate) use assertion::*; +pub(crate) use fail::*; +pub(crate) use fixture::*; +pub(crate) use imports::*; +pub(crate) use marks::*; +pub(crate) use parametrize::*; +pub(crate) use patch::*; +pub(crate) use raises::*; mod assertion; mod fail; diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/parametrize.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/parametrize.rs index a0a3b2e75b..848b5175a3 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/parametrize.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/parametrize.rs @@ -163,8 +163,7 @@ fn check_names(checker: &mut Checker, decorator: &Decorator, expr: &Expr) { ctx: ExprContext::Load, range: TextRange::default(), }); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( format!("({})", checker.generator().expr(&node)), name_range, ))); @@ -195,8 +194,7 @@ fn check_names(checker: &mut Checker, decorator: &Decorator, expr: &Expr) { ctx: ExprContext::Load, range: TextRange::default(), }); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( checker.generator().expr(&node), name_range, ))); @@ -228,8 +226,7 @@ fn check_names(checker: &mut Checker, decorator: &Decorator, expr: &Expr) { ctx: ExprContext::Load, range: TextRange::default(), }); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( checker.generator().expr(&node), expr.range(), ))); @@ -245,8 +242,7 @@ fn check_names(checker: &mut Checker, decorator: &Decorator, expr: &Expr) { ); if checker.patch(diagnostic.kind.rule()) { if let Some(content) = elts_to_csv(elts, checker.generator()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( content, expr.range(), ))); @@ -278,8 +274,7 @@ fn check_names(checker: &mut Checker, decorator: &Decorator, expr: &Expr) { ctx: ExprContext::Load, range: TextRange::default(), }); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( format!("({})", checker.generator().expr(&node)), expr.range(), ))); @@ -295,8 +290,7 @@ fn check_names(checker: &mut Checker, decorator: &Decorator, expr: &Expr) { ); if checker.patch(diagnostic.kind.rule()) { if let Some(content) = elts_to_csv(elts, checker.generator()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( content, expr.range(), ))); @@ -373,8 +367,7 @@ fn handle_single_name(checker: &mut Checker, expr: &Expr, value: &Expr) { if checker.patch(diagnostic.kind.rule()) { let node = value.clone(); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( checker.generator().expr(&node), expr.range(), ))); @@ -419,7 +412,7 @@ fn handle_value_rows( pub(crate) fn parametrize(checker: &mut Checker, decorators: &[Decorator]) { for decorator in decorators { - if is_pytest_parametrize(checker.semantic_model(), decorator) { + if is_pytest_parametrize(decorator, checker.semantic()) { if let Expr::Call(ast::ExprCall { args, .. }) = &decorator.expression { if checker.enabled(Rule::PytestParametrizeNamesWrongType) { if let Some(names) = args.get(0) { diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/patch.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/patch.rs index 2bc47f6952..2e09517978 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/patch.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/patch.rs @@ -1,10 +1,9 @@ -use rustc_hash::FxHashSet; -use rustpython_parser::ast::{self, Expr, Keyword, Ranged}; +use rustpython_parser::ast::{self, Arguments, Expr, Keyword, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::call_path::compose_call_path; -use ruff_python_ast::helpers::{collect_arg_names, SimpleCallArgs}; +use ruff_python_ast::call_path::collect_call_path; +use ruff_python_ast::helpers::{includes_arg_name, SimpleCallArgs}; use ruff_python_ast::visitor; use ruff_python_ast::visitor::Visitor; @@ -18,30 +17,10 @@ impl Violation for PytestPatchWithLambda { } } -const PATCH_NAMES: &[&str] = &[ - "mocker.patch", - "class_mocker.patch", - "module_mocker.patch", - "package_mocker.patch", - "session_mocker.patch", - "mock.patch", - "unittest.mock.patch", -]; - -const PATCH_OBJECT_NAMES: &[&str] = &[ - "mocker.patch.object", - "class_mocker.patch.object", - "module_mocker.patch.object", - "package_mocker.patch.object", - "session_mocker.patch.object", - "mock.patch.object", - "unittest.mock.patch.object", -]; - -#[derive(Default)] /// Visitor that checks references the argument names in the lambda body. +#[derive(Debug)] struct LambdaBodyVisitor<'a> { - names: FxHashSet<&'a str>, + arguments: &'a Arguments, uses_args: bool, } @@ -52,11 +31,15 @@ where fn visit_expr(&mut self, expr: &'b Expr) { match expr { Expr::Name(ast::ExprName { id, .. }) => { - if self.names.contains(&id.as_str()) { + if includes_arg_name(id, self.arguments) { self.uses_args = true; } } - _ => visitor::walk_expr(self, expr), + _ => { + if !self.uses_args { + visitor::walk_expr(self, expr); + } + } } } } @@ -80,7 +63,7 @@ fn check_patch_call( { // Walk the lambda body. let mut visitor = LambdaBodyVisitor { - names: collect_arg_names(args), + arguments: args, uses_args: false, }; visitor.visit_expr(body); @@ -98,14 +81,35 @@ pub(crate) fn patch_with_lambda( args: &[Expr], keywords: &[Keyword], ) -> Option { - if let Some(call_path) = compose_call_path(call) { - if PATCH_NAMES.contains(&call_path.as_str()) { - check_patch_call(call, args, keywords, 1) - } else if PATCH_OBJECT_NAMES.contains(&call_path.as_str()) { - check_patch_call(call, args, keywords, 2) - } else { - None - } + let call_path = collect_call_path(call)?; + + if matches!( + call_path.as_slice(), + [ + "mocker" + | "class_mocker" + | "module_mocker" + | "package_mocker" + | "session_mocker" + | "mock", + "patch" + ] | ["unittest", "mock", "patch"] + ) { + check_patch_call(call, args, keywords, 1) + } else if matches!( + call_path.as_slice(), + [ + "mocker" + | "class_mocker" + | "module_mocker" + | "package_mocker" + | "session_mocker" + | "mock", + "patch", + "object" + ] | ["unittest", "mock", "patch", "object"] + ) { + check_patch_call(call, args, keywords, 2) } else { None } diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/raises.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/raises.rs index dc79b82839..74e13767a7 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/raises.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/raises.rs @@ -1,10 +1,10 @@ -use rustpython_parser::ast::{self, Expr, Identifier, Keyword, Ranged, Stmt, Withitem}; +use rustpython_parser::ast::{self, Expr, Keyword, Ranged, Stmt, WithItem}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::call_path::format_call_path; use ruff_python_ast::call_path::from_qualified_name; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; use crate::checkers::ast::Checker; use crate::registry::Rule; @@ -47,9 +47,9 @@ impl Violation for PytestRaisesWithoutException { } } -fn is_pytest_raises(func: &Expr, model: &SemanticModel) -> bool { - model.resolve_call_path(func).map_or(false, |call_path| { - call_path.as_slice() == ["pytest", "raises"] +fn is_pytest_raises(func: &Expr, semantic: &SemanticModel) -> bool { + semantic.resolve_call_path(func).map_or(false, |call_path| { + matches!(call_path.as_slice(), ["pytest", "raises"]) }) } @@ -64,7 +64,7 @@ const fn is_non_trivial_with_body(body: &[Stmt]) -> bool { } pub(crate) fn raises_call(checker: &mut Checker, func: &Expr, args: &[Expr], keywords: &[Keyword]) { - if is_pytest_raises(func, checker.semantic_model()) { + if is_pytest_raises(func, checker.semantic()) { if checker.enabled(Rule::PytestRaisesWithoutException) { if args.is_empty() && keywords.is_empty() { checker @@ -74,9 +74,12 @@ pub(crate) fn raises_call(checker: &mut Checker, func: &Expr, args: &[Expr], key } if checker.enabled(Rule::PytestRaisesTooBroad) { - let match_keyword = keywords - .iter() - .find(|kw| kw.arg == Some(Identifier::new("match"))); + let match_keyword = keywords.iter().find(|keyword| { + keyword + .arg + .as_ref() + .map_or(false, |arg| arg.as_str() == "match") + }); if let Some(exception) = args.first() { if let Some(match_keyword) = match_keyword { @@ -94,13 +97,13 @@ pub(crate) fn raises_call(checker: &mut Checker, func: &Expr, args: &[Expr], key pub(crate) fn complex_raises( checker: &mut Checker, stmt: &Stmt, - items: &[Withitem], + items: &[WithItem], body: &[Stmt], ) { let mut is_too_complex = false; let raises_called = items.iter().any(|item| match &item.context_expr { - Expr::Call(ast::ExprCall { func, .. }) => is_pytest_raises(func, checker.semantic_model()), + Expr::Call(ast::ExprCall { func, .. }) => is_pytest_raises(func, checker.semantic()), _ => false, }); @@ -141,7 +144,7 @@ pub(crate) fn complex_raises( /// PT011 fn exception_needs_match(checker: &mut Checker, exception: &Expr) { if let Some(call_path) = checker - .semantic_model() + .semantic() .resolve_call_path(exception) .and_then(|call_path| { let is_broad_exception = checker diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/unittest_assert.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/unittest_assert.rs index 81b07d166a..106bf9ec51 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/unittest_assert.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/unittest_assert.rs @@ -3,7 +3,9 @@ use std::hash::BuildHasherDefault; use anyhow::{anyhow, bail, Result}; use ruff_text_size::TextRange; use rustc_hash::FxHashMap; -use rustpython_parser::ast::{self, Cmpop, Constant, Expr, ExprContext, Keyword, Stmt, Unaryop}; +use rustpython_parser::ast::{ + self, CmpOp, Constant, Expr, ExprContext, Identifier, Keyword, Stmt, UnaryOp, +}; /// An enum to represent the different types of assertions present in the /// `unittest` module. Note: any variants that can't be replaced with plain @@ -149,10 +151,10 @@ fn assert(expr: &Expr, msg: Option<&Expr>) -> Stmt { }) } -fn compare(left: &Expr, cmpop: Cmpop, right: &Expr) -> Expr { +fn compare(left: &Expr, cmp_op: CmpOp, right: &Expr) -> Expr { Expr::Compare(ast::ExprCompare { left: Box::new(left.clone()), - ops: vec![cmpop], + ops: vec![cmp_op], comparators: vec![right.clone()], range: TextRange::default(), }) @@ -206,9 +208,7 @@ impl UnittestAssert { keywords: &'a [Keyword], ) -> Result> { // If we have variable-length arguments, abort. - if args.iter().any(|arg| matches!(arg, Expr::Starred(_))) - || keywords.iter().any(|kw| kw.arg.is_none()) - { + if args.iter().any(Expr::is_starred_expr) || keywords.iter().any(|kw| kw.arg.is_none()) { bail!("Variable-length arguments are not supported"); } @@ -263,14 +263,14 @@ impl UnittestAssert { .ok_or_else(|| anyhow!("Missing argument `expr`"))?; let msg = args.get("msg").copied(); Ok(if matches!(self, UnittestAssert::False) { - let node = expr.clone(); - let node1 = Expr::UnaryOp(ast::ExprUnaryOp { - op: Unaryop::Not, - operand: Box::new(node), - range: TextRange::default(), - }); - let unary_expr = node1; - assert(&unary_expr, msg) + assert( + &Expr::UnaryOp(ast::ExprUnaryOp { + op: UnaryOp::Not, + operand: Box::new(expr.clone()), + range: TextRange::default(), + }), + msg, + ) } else { assert(expr, msg) }) @@ -292,18 +292,18 @@ impl UnittestAssert { .get("second") .ok_or_else(|| anyhow!("Missing argument `second`"))?; let msg = args.get("msg").copied(); - let cmpop = match self { - UnittestAssert::Equal | UnittestAssert::Equals => Cmpop::Eq, - UnittestAssert::NotEqual | UnittestAssert::NotEquals => Cmpop::NotEq, - UnittestAssert::Greater => Cmpop::Gt, - UnittestAssert::GreaterEqual => Cmpop::GtE, - UnittestAssert::Less => Cmpop::Lt, - UnittestAssert::LessEqual => Cmpop::LtE, - UnittestAssert::Is => Cmpop::Is, - UnittestAssert::IsNot => Cmpop::IsNot, + let cmp_op = match self { + UnittestAssert::Equal | UnittestAssert::Equals => CmpOp::Eq, + UnittestAssert::NotEqual | UnittestAssert::NotEquals => CmpOp::NotEq, + UnittestAssert::Greater => CmpOp::Gt, + UnittestAssert::GreaterEqual => CmpOp::GtE, + UnittestAssert::Less => CmpOp::Lt, + UnittestAssert::LessEqual => CmpOp::LtE, + UnittestAssert::Is => CmpOp::Is, + UnittestAssert::IsNot => CmpOp::IsNot, _ => unreachable!(), }; - let expr = compare(first, cmpop, second); + let expr = compare(first, cmp_op, second); Ok(assert(&expr, msg)) } UnittestAssert::In | UnittestAssert::NotIn => { @@ -314,12 +314,12 @@ impl UnittestAssert { .get("container") .ok_or_else(|| anyhow!("Missing argument `container`"))?; let msg = args.get("msg").copied(); - let cmpop = if matches!(self, UnittestAssert::In) { - Cmpop::In + let cmp_op = if matches!(self, UnittestAssert::In) { + CmpOp::In } else { - Cmpop::NotIn + CmpOp::NotIn }; - let expr = compare(member, cmpop, container); + let expr = compare(member, cmp_op, container); Ok(assert(&expr, msg)) } UnittestAssert::IsNone | UnittestAssert::IsNotNone => { @@ -327,17 +327,17 @@ impl UnittestAssert { .get("expr") .ok_or_else(|| anyhow!("Missing argument `expr`"))?; let msg = args.get("msg").copied(); - let cmpop = if matches!(self, UnittestAssert::IsNone) { - Cmpop::Is + let cmp_op = if matches!(self, UnittestAssert::IsNone) { + CmpOp::Is } else { - Cmpop::IsNot + CmpOp::IsNot }; let node = Expr::Constant(ast::ExprConstant { value: Constant::None, kind: None, range: TextRange::default(), }); - let expr = compare(expr, cmpop, &node); + let expr = compare(expr, cmp_op, &node); Ok(assert(&expr, msg)) } UnittestAssert::IsInstance | UnittestAssert::NotIsInstance => { @@ -364,7 +364,7 @@ impl UnittestAssert { Ok(assert(&isinstance, msg)) } else { let node = ast::ExprUnaryOp { - op: Unaryop::Not, + op: UnaryOp::Not, operand: Box::new(isinstance), range: TextRange::default(), }; @@ -390,7 +390,7 @@ impl UnittestAssert { }; let node1 = ast::ExprAttribute { value: Box::new(node.into()), - attr: "search".into(), + attr: Identifier::new("search".to_string(), TextRange::default()), ctx: ExprContext::Load, range: TextRange::default(), }; @@ -405,7 +405,7 @@ impl UnittestAssert { Ok(assert(&re_search, msg)) } else { let node = ast::ExprUnaryOp { - op: Unaryop::Not, + op: UnaryOp::Not, operand: Box::new(re_search), range: TextRange::default(), }; diff --git a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT001_default.snap b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT001_default.snap index 16ef00156f..ccfd30d007 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT001_default.snap +++ b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT001_default.snap @@ -10,7 +10,7 @@ PT001.py:9:1: PT001 [*] Use `@pytest.fixture()` over `@pytest.fixture` | = help: Add parentheses -ℹ Suggested fix +ℹ Fix 6 6 | # `import pytest` 7 7 | 8 8 | @@ -29,7 +29,7 @@ PT001.py:34:1: PT001 [*] Use `@pytest.fixture()` over `@pytest.fixture` | = help: Add parentheses -ℹ Suggested fix +ℹ Fix 31 31 | # `from pytest import fixture` 32 32 | 33 33 | @@ -48,7 +48,7 @@ PT001.py:59:1: PT001 [*] Use `@pytest.fixture()` over `@pytest.fixture` | = help: Add parentheses -ℹ Suggested fix +ℹ Fix 56 56 | # `from pytest import fixture as aliased` 57 57 | 58 58 | diff --git a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT001_no_parentheses.snap b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT001_no_parentheses.snap index ffbff683cf..408075a3cf 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT001_no_parentheses.snap +++ b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT001_no_parentheses.snap @@ -10,7 +10,7 @@ PT001.py:14:1: PT001 [*] Use `@pytest.fixture` over `@pytest.fixture()` | = help: Remove parentheses -ℹ Suggested fix +ℹ Fix 11 11 | return 42 12 12 | 13 13 | @@ -31,7 +31,7 @@ PT001.py:24:1: PT001 [*] Use `@pytest.fixture` over `@pytest.fixture()` | = help: Remove parentheses -ℹ Suggested fix +ℹ Fix 21 21 | return 42 22 22 | 23 23 | @@ -52,7 +52,7 @@ PT001.py:39:1: PT001 [*] Use `@pytest.fixture` over `@pytest.fixture()` | = help: Remove parentheses -ℹ Suggested fix +ℹ Fix 36 36 | return 42 37 37 | 38 38 | @@ -73,7 +73,7 @@ PT001.py:49:1: PT001 [*] Use `@pytest.fixture` over `@pytest.fixture()` | = help: Remove parentheses -ℹ Suggested fix +ℹ Fix 46 46 | return 42 47 47 | 48 48 | @@ -94,7 +94,7 @@ PT001.py:64:1: PT001 [*] Use `@pytest.fixture` over `@pytest.fixture()` | = help: Remove parentheses -ℹ Suggested fix +ℹ Fix 61 61 | return 42 62 62 | 63 63 | @@ -115,7 +115,7 @@ PT001.py:74:1: PT001 [*] Use `@pytest.fixture` over `@pytest.fixture()` | = help: Remove parentheses -ℹ Suggested fix +ℹ Fix 71 71 | return 42 72 72 | 73 73 | diff --git a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT006_csv.snap b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT006_csv.snap index 33f628f212..47d6acb99c 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT006_csv.snap +++ b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT006_csv.snap @@ -29,7 +29,7 @@ PT006.py:29:26: PT006 [*] Wrong name(s) type in `@pytest.mark.parametrize`, expe | = help: Use a `csv` for parameter names -ℹ Suggested fix +ℹ Fix 26 26 | ... 27 27 | 28 28 | @@ -67,7 +67,7 @@ PT006.py:39:26: PT006 [*] Wrong name(s) type in `@pytest.mark.parametrize`, expe | = help: Use a `csv` for parameter names -ℹ Suggested fix +ℹ Fix 36 36 | ... 37 37 | 38 38 | diff --git a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT006_default.snap b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT006_default.snap index 2aad073076..9ed5a819f4 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT006_default.snap +++ b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT006_default.snap @@ -67,7 +67,7 @@ PT006.py:29:26: PT006 [*] Wrong name(s) type in `@pytest.mark.parametrize`, expe | = help: Use a `csv` for parameter names -ℹ Suggested fix +ℹ Fix 26 26 | ... 27 27 | 28 28 | @@ -105,7 +105,7 @@ PT006.py:39:26: PT006 [*] Wrong name(s) type in `@pytest.mark.parametrize`, expe | = help: Use a `csv` for parameter names -ℹ Suggested fix +ℹ Fix 36 36 | ... 37 37 | 38 38 | diff --git a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT006_list.snap b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT006_list.snap index c052285e34..3cd37fa141 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT006_list.snap +++ b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT006_list.snap @@ -86,7 +86,7 @@ PT006.py:29:26: PT006 [*] Wrong name(s) type in `@pytest.mark.parametrize`, expe | = help: Use a `csv` for parameter names -ℹ Suggested fix +ℹ Fix 26 26 | ... 27 27 | 28 28 | @@ -105,7 +105,7 @@ PT006.py:39:26: PT006 [*] Wrong name(s) type in `@pytest.mark.parametrize`, expe | = help: Use a `csv` for parameter names -ℹ Suggested fix +ℹ Fix 36 36 | ... 37 37 | 38 38 | diff --git a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT016.snap b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT016.snap index 5223b12eed..945f989567 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT016.snap +++ b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT016.snap @@ -1,49 +1,70 @@ --- source: crates/ruff/src/rules/flake8_pytest_style/mod.rs --- -PT016.py:13:5: PT016 No message passed to `pytest.fail()` +PT016.py:19:5: PT016 No message passed to `pytest.fail()` | -12 | def test_xxx(): # Error -13 | pytest.fail() +17 | # Errors +18 | def f(): +19 | pytest.fail() | ^^^^^^^^^^^ PT016 -14 | pytest.fail("") -15 | pytest.fail(f"") +20 | pytest.fail("") +21 | pytest.fail(f"") | -PT016.py:14:5: PT016 No message passed to `pytest.fail()` +PT016.py:20:5: PT016 No message passed to `pytest.fail()` | -12 | def test_xxx(): # Error -13 | pytest.fail() -14 | pytest.fail("") +18 | def f(): +19 | pytest.fail() +20 | pytest.fail("") | ^^^^^^^^^^^ PT016 -15 | pytest.fail(f"") -16 | pytest.fail(msg="") +21 | pytest.fail(f"") +22 | pytest.fail(msg="") | -PT016.py:15:5: PT016 No message passed to `pytest.fail()` +PT016.py:21:5: PT016 No message passed to `pytest.fail()` | -13 | pytest.fail() -14 | pytest.fail("") -15 | pytest.fail(f"") +19 | pytest.fail() +20 | pytest.fail("") +21 | pytest.fail(f"") | ^^^^^^^^^^^ PT016 -16 | pytest.fail(msg="") -17 | pytest.fail(msg=f"") +22 | pytest.fail(msg="") +23 | pytest.fail(msg=f"") | -PT016.py:16:5: PT016 No message passed to `pytest.fail()` +PT016.py:22:5: PT016 No message passed to `pytest.fail()` | -14 | pytest.fail("") -15 | pytest.fail(f"") -16 | pytest.fail(msg="") +20 | pytest.fail("") +21 | pytest.fail(f"") +22 | pytest.fail(msg="") | ^^^^^^^^^^^ PT016 -17 | pytest.fail(msg=f"") +23 | pytest.fail(msg=f"") +24 | pytest.fail(reason="") | -PT016.py:17:5: PT016 No message passed to `pytest.fail()` +PT016.py:23:5: PT016 No message passed to `pytest.fail()` | -15 | pytest.fail(f"") -16 | pytest.fail(msg="") -17 | pytest.fail(msg=f"") +21 | pytest.fail(f"") +22 | pytest.fail(msg="") +23 | pytest.fail(msg=f"") + | ^^^^^^^^^^^ PT016 +24 | pytest.fail(reason="") +25 | pytest.fail(reason=f"") + | + +PT016.py:24:5: PT016 No message passed to `pytest.fail()` + | +22 | pytest.fail(msg="") +23 | pytest.fail(msg=f"") +24 | pytest.fail(reason="") + | ^^^^^^^^^^^ PT016 +25 | pytest.fail(reason=f"") + | + +PT016.py:25:5: PT016 No message passed to `pytest.fail()` + | +23 | pytest.fail(msg=f"") +24 | pytest.fail(reason="") +25 | pytest.fail(reason=f"") | ^^^^^^^^^^^ PT016 | diff --git a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT018.snap b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT018.snap index 8d9c69ab49..cbfde1160d 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT018.snap +++ b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT018.snap @@ -163,8 +163,8 @@ PT018.py:21:5: PT018 [*] Assertion should be broken down into multiple parts 22 | | message 23 | | """ | |_______^ PT018 -24 | -25 | # recursive case +24 | assert ( +25 | something | = help: Break down assertion into multiple parts @@ -177,131 +177,144 @@ PT018.py:21:5: PT018 [*] Assertion should be broken down into multiple parts 22 |+ assert something_else == """error 22 23 | message 23 24 | """ -24 25 | +24 25 | assert ( -PT018.py:26:5: PT018 [*] Assertion should be broken down into multiple parts +PT018.py:24:5: PT018 [*] Assertion should be broken down into multiple parts | -25 | # recursive case -26 | assert not (a or not (b or c)) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018 -27 | assert not (a or not (b and c)) - | - = help: Break down assertion into multiple parts - -ℹ Suggested fix -23 23 | """ -24 24 | -25 25 | # recursive case -26 |- assert not (a or not (b or c)) - 26 |+ assert not a - 27 |+ assert (b or c) -27 28 | assert not (a or not (b and c)) -28 29 | -29 30 | # detected, but no autofix for messages - -PT018.py:27:5: PT018 [*] Assertion should be broken down into multiple parts - | -25 | # recursive case -26 | assert not (a or not (b or c)) -27 | assert not (a or not (b and c)) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018 -28 | -29 | # detected, but no autofix for messages - | - = help: Break down assertion into multiple parts - -ℹ Suggested fix -24 24 | -25 25 | # recursive case -26 26 | assert not (a or not (b or c)) -27 |- assert not (a or not (b and c)) - 27 |+ assert not a - 28 |+ assert (b and c) -28 29 | -29 30 | # detected, but no autofix for messages -30 31 | assert something and something_else, "error message" - -PT018.py:30:5: PT018 Assertion should be broken down into multiple parts - | -29 | # detected, but no autofix for messages -30 | assert something and something_else, "error message" - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018 -31 | assert not (something or something_else and something_third), "with message" -32 | # detected, but no autofix for mixed conditions (e.g. `a or b and c`) - | - = help: Break down assertion into multiple parts - -PT018.py:31:5: PT018 Assertion should be broken down into multiple parts - | -29 | # detected, but no autofix for messages -30 | assert something and something_else, "error message" -31 | assert not (something or something_else and something_third), "with message" - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018 -32 | # detected, but no autofix for mixed conditions (e.g. `a or b and c`) -33 | assert not (something or something_else and something_third) - | - = help: Break down assertion into multiple parts - -PT018.py:33:5: PT018 Assertion should be broken down into multiple parts - | -31 | assert not (something or something_else and something_third), "with message" -32 | # detected, but no autofix for mixed conditions (e.g. `a or b and c`) -33 | assert not (something or something_else and something_third) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018 -34 | # detected, but no autofix for parenthesized conditions -35 | assert ( - | - = help: Break down assertion into multiple parts - -PT018.py:35:5: PT018 Assertion should be broken down into multiple parts - | -33 | assert not (something or something_else and something_third) -34 | # detected, but no autofix for parenthesized conditions -35 | assert ( +22 | message +23 | """ +24 | assert ( | _____^ -36 | | something -37 | | and something_else -38 | | == """error -39 | | message -40 | | """ -41 | | ) +25 | | something +26 | | and something_else +27 | | == """error +28 | | message +29 | | """ +30 | | ) | |_____^ PT018 +31 | +32 | # recursive case | = help: Break down assertion into multiple parts +ℹ Suggested fix +21 21 | assert something and something_else == """error +22 22 | message +23 23 | """ + 24 |+ assert something +24 25 | assert ( +25 |- something +26 |- and something_else + 26 |+ something_else +27 27 | == """error +28 28 | message +29 29 | """ + +PT018.py:33:5: PT018 [*] Assertion should be broken down into multiple parts + | +32 | # recursive case +33 | assert not (a or not (b or c)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018 +34 | assert not (a or not (b and c)) + | + = help: Break down assertion into multiple parts + +ℹ Suggested fix +30 30 | ) +31 31 | +32 32 | # recursive case +33 |- assert not (a or not (b or c)) + 33 |+ assert not a + 34 |+ assert (b or c) +34 35 | assert not (a or not (b and c)) +35 36 | +36 37 | # detected, but no autofix for messages + +PT018.py:34:5: PT018 [*] Assertion should be broken down into multiple parts + | +32 | # recursive case +33 | assert not (a or not (b or c)) +34 | assert not (a or not (b and c)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018 +35 | +36 | # detected, but no autofix for messages + | + = help: Break down assertion into multiple parts + +ℹ Suggested fix +31 31 | +32 32 | # recursive case +33 33 | assert not (a or not (b or c)) +34 |- assert not (a or not (b and c)) + 34 |+ assert not a + 35 |+ assert (b and c) +35 36 | +36 37 | # detected, but no autofix for messages +37 38 | assert something and something_else, "error message" + +PT018.py:37:5: PT018 Assertion should be broken down into multiple parts + | +36 | # detected, but no autofix for messages +37 | assert something and something_else, "error message" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018 +38 | assert not (something or something_else and something_third), "with message" +39 | # detected, but no autofix for mixed conditions (e.g. `a or b and c`) + | + = help: Break down assertion into multiple parts + +PT018.py:38:5: PT018 Assertion should be broken down into multiple parts + | +36 | # detected, but no autofix for messages +37 | assert something and something_else, "error message" +38 | assert not (something or something_else and something_third), "with message" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018 +39 | # detected, but no autofix for mixed conditions (e.g. `a or b and c`) +40 | assert not (something or something_else and something_third) + | + = help: Break down assertion into multiple parts + +PT018.py:40:5: PT018 Assertion should be broken down into multiple parts + | +38 | assert not (something or something_else and something_third), "with message" +39 | # detected, but no autofix for mixed conditions (e.g. `a or b and c`) +40 | assert not (something or something_else and something_third) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018 + | + = help: Break down assertion into multiple parts + +PT018.py:44:1: PT018 [*] Assertion should be broken down into multiple parts + | +43 | assert something # OK +44 | assert something and something_else # Error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018 +45 | assert something and something_else and something_third # Error + | + = help: Break down assertion into multiple parts + +ℹ Suggested fix +41 41 | +42 42 | +43 43 | assert something # OK +44 |-assert something and something_else # Error + 44 |+assert something + 45 |+assert something_else +45 46 | assert something and something_else and something_third # Error + PT018.py:45:1: PT018 [*] Assertion should be broken down into multiple parts | -44 | assert something # OK -45 | assert something and something_else # Error - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018 -46 | assert something and something_else and something_third # Error - | - = help: Break down assertion into multiple parts - -ℹ Suggested fix -42 42 | -43 43 | -44 44 | assert something # OK -45 |-assert something and something_else # Error - 45 |+assert something - 46 |+assert something_else -46 47 | assert something and something_else and something_third # Error - -PT018.py:46:1: PT018 [*] Assertion should be broken down into multiple parts - | -44 | assert something # OK -45 | assert something and something_else # Error -46 | assert something and something_else and something_third # Error +43 | assert something # OK +44 | assert something and something_else # Error +45 | assert something and something_else and something_third # Error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018 | = help: Break down assertion into multiple parts ℹ Suggested fix -43 43 | -44 44 | assert something # OK -45 45 | assert something and something_else # Error -46 |-assert something and something_else and something_third # Error - 46 |+assert something and something_else - 47 |+assert something_third +42 42 | +43 43 | assert something # OK +44 44 | assert something and something_else # Error +45 |-assert something and something_else and something_third # Error + 45 |+assert something and something_else + 46 |+assert something_third diff --git a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT022.snap b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT022.snap index a54bb6a1ff..96594ee97c 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT022.snap +++ b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT022.snap @@ -10,7 +10,7 @@ PT022.py:17:5: PT022 [*] No teardown in fixture `error`, use `return` instead of | = help: Replace `yield` with `return` -ℹ Suggested fix +ℹ Fix 14 14 | @pytest.fixture() 15 15 | def error(): 16 16 | resource = acquire_resource() diff --git a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT023_default.snap b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT023_default.snap index 7a0ba21063..03d769bf5a 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT023_default.snap +++ b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT023_default.snap @@ -10,7 +10,7 @@ PT023.py:12:1: PT023 [*] Use `@pytest.mark.foo()` over `@pytest.mark.foo` | = help: Add/remove parentheses -ℹ Suggested fix +ℹ Fix 9 9 | # Without parentheses 10 10 | 11 11 | @@ -29,7 +29,7 @@ PT023.py:17:1: PT023 [*] Use `@pytest.mark.foo()` over `@pytest.mark.foo` | = help: Add/remove parentheses -ℹ Suggested fix +ℹ Fix 14 14 | pass 15 15 | 16 16 | @@ -49,7 +49,7 @@ PT023.py:24:5: PT023 [*] Use `@pytest.mark.foo()` over `@pytest.mark.foo` | = help: Add/remove parentheses -ℹ Suggested fix +ℹ Fix 21 21 | 22 22 | 23 23 | class TestClass: @@ -69,7 +69,7 @@ PT023.py:30:5: PT023 [*] Use `@pytest.mark.foo()` over `@pytest.mark.foo` | = help: Add/remove parentheses -ℹ Suggested fix +ℹ Fix 27 27 | 28 28 | 29 29 | class TestClass: @@ -90,7 +90,7 @@ PT023.py:38:9: PT023 [*] Use `@pytest.mark.foo()` over `@pytest.mark.foo` | = help: Add/remove parentheses -ℹ Suggested fix +ℹ Fix 35 35 | 36 36 | class TestClass: 37 37 | class TestNestedClass: diff --git a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT023_no_parentheses.snap b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT023_no_parentheses.snap index c85f180c7b..8a049d4f70 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT023_no_parentheses.snap +++ b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT023_no_parentheses.snap @@ -10,7 +10,7 @@ PT023.py:46:1: PT023 [*] Use `@pytest.mark.foo` over `@pytest.mark.foo()` | = help: Add/remove parentheses -ℹ Suggested fix +ℹ Fix 43 43 | # With parentheses 44 44 | 45 45 | @@ -29,7 +29,7 @@ PT023.py:51:1: PT023 [*] Use `@pytest.mark.foo` over `@pytest.mark.foo()` | = help: Add/remove parentheses -ℹ Suggested fix +ℹ Fix 48 48 | pass 49 49 | 50 50 | @@ -49,7 +49,7 @@ PT023.py:58:5: PT023 [*] Use `@pytest.mark.foo` over `@pytest.mark.foo()` | = help: Add/remove parentheses -ℹ Suggested fix +ℹ Fix 55 55 | 56 56 | 57 57 | class TestClass: @@ -69,7 +69,7 @@ PT023.py:64:5: PT023 [*] Use `@pytest.mark.foo` over `@pytest.mark.foo()` | = help: Add/remove parentheses -ℹ Suggested fix +ℹ Fix 61 61 | 62 62 | 63 63 | class TestClass: @@ -90,7 +90,7 @@ PT023.py:72:9: PT023 [*] Use `@pytest.mark.foo` over `@pytest.mark.foo()` | = help: Add/remove parentheses -ℹ Suggested fix +ℹ Fix 69 69 | 70 70 | class TestClass: 71 71 | class TestNestedClass: diff --git a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT024.snap b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT024.snap index ec90561e26..14fd58d9d6 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT024.snap +++ b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT024.snap @@ -10,7 +10,7 @@ PT024.py:14:1: PT024 [*] `pytest.mark.asyncio` is unnecessary for fixtures | = help: Remove `pytest.mark.asyncio` -ℹ Suggested fix +ℹ Fix 11 11 | pass 12 12 | 13 13 | @@ -28,7 +28,7 @@ PT024.py:20:1: PT024 [*] `pytest.mark.asyncio` is unnecessary for fixtures | = help: Remove `pytest.mark.asyncio` -ℹ Suggested fix +ℹ Fix 17 17 | return 0 18 18 | 19 19 | @@ -47,7 +47,7 @@ PT024.py:27:1: PT024 [*] `pytest.mark.asyncio` is unnecessary for fixtures | = help: Remove `pytest.mark.asyncio` -ℹ Suggested fix +ℹ Fix 24 24 | 25 25 | 26 26 | @pytest.fixture() @@ -66,7 +66,7 @@ PT024.py:33:1: PT024 [*] `pytest.mark.asyncio` is unnecessary for fixtures | = help: Remove `pytest.mark.asyncio` -ℹ Suggested fix +ℹ Fix 30 30 | 31 31 | 32 32 | @pytest.fixture() diff --git a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT025.snap b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT025.snap index d3f4789403..181945326c 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT025.snap +++ b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT025.snap @@ -10,7 +10,7 @@ PT025.py:9:1: PT025 [*] `pytest.mark.usefixtures` has no effect on fixtures | = help: Remove `pytest.mark.usefixtures` -ℹ Suggested fix +ℹ Fix 6 6 | pass 7 7 | 8 8 | @@ -29,7 +29,7 @@ PT025.py:16:1: PT025 [*] `pytest.mark.usefixtures` has no effect on fixtures | = help: Remove `pytest.mark.usefixtures` -ℹ Suggested fix +ℹ Fix 13 13 | 14 14 | 15 15 | @pytest.fixture() diff --git a/crates/ruff/src/rules/flake8_quotes/rules/from_tokens.rs b/crates/ruff/src/rules/flake8_quotes/rules/from_tokens.rs index 7c6f990dd3..fa77834113 100644 --- a/crates/ruff/src/rules/flake8_quotes/rules/from_tokens.rs +++ b/crates/ruff/src/rules/flake8_quotes/rules/from_tokens.rs @@ -20,9 +20,6 @@ use super::super::settings::Quote; /// Consistency is good. Use either single or double quotes for inline /// strings, but be consistent. /// -/// ## Options -/// - `flake8-quotes.inline-quotes` -/// /// ## Example /// ```python /// foo = 'bar' @@ -32,6 +29,9 @@ use super::super::settings::Quote; /// ```python /// foo = "bar" /// ``` +/// +/// ## Options +/// - `flake8-quotes.inline-quotes` #[violation] pub struct BadQuotesInlineString { quote: Quote, @@ -42,16 +42,16 @@ impl AlwaysAutofixableViolation for BadQuotesInlineString { fn message(&self) -> String { let BadQuotesInlineString { quote } = self; match quote { - Quote::Single => format!("Double quotes found but single quotes preferred"), Quote::Double => format!("Single quotes found but double quotes preferred"), + Quote::Single => format!("Double quotes found but single quotes preferred"), } } fn autofix_title(&self) -> String { let BadQuotesInlineString { quote } = self; match quote { - Quote::Single => "Replace double quotes with single quotes".to_string(), Quote::Double => "Replace single quotes with double quotes".to_string(), + Quote::Single => "Replace double quotes with single quotes".to_string(), } } } @@ -65,9 +65,6 @@ impl AlwaysAutofixableViolation for BadQuotesInlineString { /// Consistency is good. Use either single or double quotes for multiline /// strings, but be consistent. /// -/// ## Options -/// - `flake8-quotes.multiline-quotes` -/// /// ## Example /// ```python /// foo = ''' @@ -81,6 +78,9 @@ impl AlwaysAutofixableViolation for BadQuotesInlineString { /// bar /// """ /// ``` +/// +/// ## Options +/// - `flake8-quotes.multiline-quotes` #[violation] pub struct BadQuotesMultilineString { quote: Quote, @@ -91,16 +91,16 @@ impl AlwaysAutofixableViolation for BadQuotesMultilineString { fn message(&self) -> String { let BadQuotesMultilineString { quote } = self; match quote { - Quote::Single => format!("Double quote multiline found but single quotes preferred"), Quote::Double => format!("Single quote multiline found but double quotes preferred"), + Quote::Single => format!("Double quote multiline found but single quotes preferred"), } } fn autofix_title(&self) -> String { let BadQuotesMultilineString { quote } = self; match quote { - Quote::Single => "Replace double multiline quotes with single quotes".to_string(), Quote::Double => "Replace single multiline quotes with double quotes".to_string(), + Quote::Single => "Replace double multiline quotes with single quotes".to_string(), } } } @@ -113,9 +113,6 @@ impl AlwaysAutofixableViolation for BadQuotesMultilineString { /// Consistency is good. Use either single or double quotes for docstring /// strings, but be consistent. /// -/// ## Options -/// - `flake8-quotes.docstring-quotes` -/// /// ## Example /// ```python /// ''' @@ -129,6 +126,9 @@ impl AlwaysAutofixableViolation for BadQuotesMultilineString { /// bar /// """ /// ``` +/// +/// ## Options +/// - `flake8-quotes.docstring-quotes` #[violation] pub struct BadQuotesDocstring { quote: Quote, @@ -139,16 +139,16 @@ impl AlwaysAutofixableViolation for BadQuotesDocstring { fn message(&self) -> String { let BadQuotesDocstring { quote } = self; match quote { - Quote::Single => format!("Double quote docstring found but single quotes preferred"), Quote::Double => format!("Single quote docstring found but double quotes preferred"), + Quote::Single => format!("Double quote docstring found but single quotes preferred"), } } fn autofix_title(&self) -> String { let BadQuotesDocstring { quote } = self; match quote { - Quote::Single => "Replace double quotes docstring with single quotes".to_string(), Quote::Double => "Replace single quotes docstring with double quotes".to_string(), + Quote::Single => "Replace double quotes docstring with single quotes".to_string(), } } } @@ -186,8 +186,8 @@ impl AlwaysAutofixableViolation for AvoidableEscapedQuote { const fn good_single(quote: Quote) -> char { match quote { - Quote::Single => '\'', Quote::Double => '"', + Quote::Single => '\'', } } @@ -200,22 +200,22 @@ const fn bad_single(quote: Quote) -> char { const fn good_multiline(quote: Quote) -> &'static str { match quote { - Quote::Single => "'''", Quote::Double => "\"\"\"", + Quote::Single => "'''", } } const fn good_multiline_ending(quote: Quote) -> &'static str { match quote { - Quote::Single => "'\"\"\"", Quote::Double => "\"'''", + Quote::Single => "'\"\"\"", } } const fn good_docstring(quote: Quote) -> &'static str { match quote { - Quote::Single => "'", Quote::Double => "\"", + Quote::Single => "'", } } @@ -284,8 +284,7 @@ fn docstring(locator: &Locator, range: TextRange, settings: &Settings) -> Option fixed_contents.push_str("e); fixed_contents.push_str(string_contents); fixed_contents.push_str("e); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( fixed_contents, range, ))); @@ -323,7 +322,7 @@ fn strings(locator: &Locator, sequence: &[TextRange], settings: &Settings) -> Ve string_contents.contains(good_single(quotes_settings.inline_quotes)) }); - for (range, trivia) in sequence.iter().zip(trivia.into_iter()) { + for (range, trivia) in sequence.iter().zip(trivia) { if trivia.is_multiline { // If our string is or contains a known good string, ignore it. if trivia @@ -358,8 +357,7 @@ fn strings(locator: &Locator, sequence: &[TextRange], settings: &Settings) -> Ve fixed_contents.push_str(quote); fixed_contents.push_str(string_contents); fixed_contents.push_str(quote); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( fixed_contents, *range, ))); @@ -425,8 +423,7 @@ fn strings(locator: &Locator, sequence: &[TextRange], settings: &Settings) -> Ve fixed_contents.push(quote); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( fixed_contents, *range, ))); @@ -452,8 +449,7 @@ fn strings(locator: &Locator, sequence: &[TextRange], settings: &Settings) -> Ve fixed_contents.push(quote); fixed_contents.push_str(string_contents); fixed_contents.push(quote); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( fixed_contents, *range, ))); @@ -468,12 +464,11 @@ fn strings(locator: &Locator, sequence: &[TextRange], settings: &Settings) -> Ve /// Generate `flake8-quote` diagnostics from a token stream. pub(crate) fn from_tokens( + diagnostics: &mut Vec, lxr: &[LexResult], locator: &Locator, settings: &Settings, -) -> Vec { - let mut diagnostics = vec![]; - +) { // Keep track of sequences of strings, which represent implicit string // concatenation, and should thus be handled as a single unit. let mut sequence = vec![]; @@ -492,7 +487,7 @@ pub(crate) fn from_tokens( diagnostics.push(diagnostic); } } else { - if matches!(tok, Tok::String { .. }) { + if tok.is_string() { // If this is a string, add it to the sequence. sequence.push(range); } else if !matches!(tok, Tok::Comment(..) | Tok::NonLogicalNewline) { @@ -510,6 +505,4 @@ pub(crate) fn from_tokens( diagnostics.extend(strings(locator, &sequence, settings)); sequence.clear(); } - - diagnostics } diff --git a/crates/ruff/src/rules/flake8_quotes/rules/mod.rs b/crates/ruff/src/rules/flake8_quotes/rules/mod.rs index 7f04e8908f..8ad6bad659 100644 --- a/crates/ruff/src/rules/flake8_quotes/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_quotes/rules/mod.rs @@ -1,6 +1,3 @@ -pub(crate) use from_tokens::{ - from_tokens, AvoidableEscapedQuote, BadQuotesDocstring, BadQuotesInlineString, - BadQuotesMultilineString, -}; +pub(crate) use from_tokens::*; mod from_tokens; diff --git a/crates/ruff/src/rules/flake8_quotes/settings.rs b/crates/ruff/src/rules/flake8_quotes/settings.rs index 121501065e..d0f377a2fb 100644 --- a/crates/ruff/src/rules/flake8_quotes/settings.rs +++ b/crates/ruff/src/rules/flake8_quotes/settings.rs @@ -8,10 +8,10 @@ use ruff_macros::{CacheKey, CombineOptions, ConfigurationOptions}; #[serde(deny_unknown_fields, rename_all = "kebab-case")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub enum Quote { - /// Use single quotes. - Single, /// Use double quotes. Double, + /// Use single quotes. + Single, } impl Default for Quote { diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles.py.snap index 56a74f2ee0..00664f2737 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles.py.snap @@ -14,7 +14,7 @@ docstring_doubles.py:5:1: Q001 [*] Double quote multiline found but single quote | = help: Replace double multiline quotes with single quotes -ℹ Suggested fix +ℹ Fix 2 2 | Double quotes multiline module docstring 3 3 | """ 4 4 | @@ -41,7 +41,7 @@ docstring_doubles.py:16:5: Q001 [*] Double quote multiline found but single quot | = help: Replace double multiline quotes with single quotes -ℹ Suggested fix +ℹ Fix 13 13 | Double quotes multiline class docstring 14 14 | """ 15 15 | @@ -66,7 +66,7 @@ docstring_doubles.py:21:21: Q001 [*] Double quote multiline found but single quo | = help: Replace double multiline quotes with single quotes -ℹ Suggested fix +ℹ Fix 18 18 | """ 19 19 | 20 20 | # The colon in the list indexing below is an edge case for the docstring scanner @@ -92,7 +92,7 @@ docstring_doubles.py:30:9: Q001 [*] Double quote multiline found but single quot | = help: Replace double multiline quotes with single quotes -ℹ Suggested fix +ℹ Fix 27 27 | 28 28 | some_expression = 'hello world' 29 29 | @@ -117,7 +117,7 @@ docstring_doubles.py:35:13: Q001 [*] Double quote multiline found but single quo | = help: Replace double multiline quotes with single quotes -ℹ Suggested fix +ℹ Fix 32 32 | """ 33 33 | 34 34 | if l: diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles_class.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles_class.py.snap index 39bbce6bb6..1cac8d3b14 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles_class.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles_class.py.snap @@ -12,7 +12,7 @@ docstring_doubles_class.py:3:5: Q001 [*] Double quote multiline found but single | = help: Replace double multiline quotes with single quotes -ℹ Suggested fix +ℹ Fix 1 1 | class SingleLineDocstrings(): 2 2 | """ Double quotes single line class docstring """ 3 |- """ Not a docstring """ @@ -32,7 +32,7 @@ docstring_doubles_class.py:5:23: Q001 [*] Double quote multiline found but singl | = help: Replace double multiline quotes with single quotes -ℹ Suggested fix +ℹ Fix 2 2 | """ Double quotes single line class docstring """ 3 3 | """ Not a docstring """ 4 4 | diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles_function.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles_function.py.snap index 8116a79371..561aba67da 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles_function.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles_function.py.snap @@ -11,7 +11,7 @@ docstring_doubles_function.py:3:5: Q001 [*] Double quote multiline found but sin | = help: Replace double multiline quotes with single quotes -ℹ Suggested fix +ℹ Fix 1 1 | def foo(): 2 2 | """function without params, single line docstring""" 3 |- """ not a docstring""" @@ -30,7 +30,7 @@ docstring_doubles_function.py:11:5: Q001 [*] Double quote multiline found but si | = help: Replace double multiline quotes with single quotes -ℹ Suggested fix +ℹ Fix 8 8 | """ 9 9 | function without params, multiline docstring 10 10 | """ @@ -51,7 +51,7 @@ docstring_doubles_function.py:15:39: Q001 [*] Double quote multiline found but s | = help: Replace double multiline quotes with single quotes -ℹ Suggested fix +ℹ Fix 12 12 | return 13 13 | 14 14 | @@ -74,7 +74,7 @@ docstring_doubles_function.py:17:5: Q001 [*] Double quote multiline found but si | = help: Replace double multiline quotes with single quotes -ℹ Suggested fix +ℹ Fix 14 14 | 15 15 | def fun_with_params_no_docstring(a, b=""" 16 16 | not a @@ -93,7 +93,7 @@ docstring_doubles_function.py:22:5: Q001 [*] Double quote multiline found but si | = help: Replace double multiline quotes with single quotes -ℹ Suggested fix +ℹ Fix 19 19 | 20 20 | 21 21 | def fun_with_params_no_docstring2(a, b=c[foo():], c=\ diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles_module_multiline.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles_module_multiline.py.snap index 83e43cece8..dc6f5236d4 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles_module_multiline.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles_module_multiline.py.snap @@ -14,7 +14,7 @@ docstring_doubles_module_multiline.py:4:1: Q001 [*] Double quote multiline found | = help: Replace double multiline quotes with single quotes -ℹ Suggested fix +ℹ Fix 1 1 | """ 2 2 | Double quotes multiline module docstring 3 3 | """ @@ -38,7 +38,7 @@ docstring_doubles_module_multiline.py:9:1: Q001 [*] Double quote multiline found | = help: Replace double multiline quotes with single quotes -ℹ Suggested fix +ℹ Fix 6 6 | """ 7 7 | def foo(): 8 8 | pass diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles_module_singleline.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles_module_singleline.py.snap index 2b1e4c9f7b..86ca4127b2 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles_module_singleline.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_doubles_module_singleline.py.snap @@ -11,7 +11,7 @@ docstring_doubles_module_singleline.py:2:1: Q001 [*] Double quote multiline foun | = help: Replace double multiline quotes with single quotes -ℹ Suggested fix +ℹ Fix 1 1 | """ Double quotes singleline module docstring """ 2 |-""" this is not a docstring """ 2 |+''' this is not a docstring ''' @@ -28,7 +28,7 @@ docstring_doubles_module_singleline.py:6:1: Q001 [*] Double quote multiline foun | = help: Replace double multiline quotes with single quotes -ℹ Suggested fix +ℹ Fix 3 3 | 4 4 | def foo(): 5 5 | pass diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles.py.snap index 5b500bfc98..2315b0b9f0 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles.py.snap @@ -12,7 +12,7 @@ docstring_singles.py:1:1: Q002 [*] Single quote docstring found but double quote | = help: Replace single quotes docstring with double quotes -ℹ Suggested fix +ℹ Fix 1 |-''' 1 |+""" 2 2 | Single quotes multiline module docstring @@ -36,7 +36,7 @@ docstring_singles.py:14:5: Q002 [*] Single quote docstring found but double quot | = help: Replace single quotes docstring with double quotes -ℹ Suggested fix +ℹ Fix 11 11 | class Cls(MakeKlass(''' 12 12 | class params \t not a docstring 13 13 | ''')): @@ -63,7 +63,7 @@ docstring_singles.py:26:9: Q002 [*] Single quote docstring found but double quot | = help: Replace single quotes docstring with double quotes -ℹ Suggested fix +ℹ Fix 23 23 | def f(self, bar=''' 24 24 | definitely not a docstring''', 25 25 | val=l[Cls():3]): diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_class.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_class.py.snap index 1ff2255082..2c822e35c8 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_class.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_class.py.snap @@ -10,7 +10,7 @@ docstring_singles_class.py:2:5: Q002 [*] Single quote docstring found but double | = help: Replace single quotes docstring with double quotes -ℹ Suggested fix +ℹ Fix 1 1 | class SingleLineDocstrings(): 2 |- ''' Double quotes single line class docstring ''' 2 |+ """ Double quotes single line class docstring """ @@ -27,7 +27,7 @@ docstring_singles_class.py:6:9: Q002 [*] Single quote docstring found but double | = help: Replace single quotes docstring with double quotes -ℹ Suggested fix +ℹ Fix 3 3 | ''' Not a docstring ''' 4 4 | 5 5 | def foo(self, bar='''not a docstring'''): @@ -46,7 +46,7 @@ docstring_singles_class.py:9:29: Q002 [*] Single quote docstring found but doubl | = help: Replace single quotes docstring with double quotes -ℹ Suggested fix +ℹ Fix 6 6 | ''' Double quotes single line method docstring''' 7 7 | pass 8 8 | diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_function.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_function.py.snap index b0065abd99..6d878507ce 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_function.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_function.py.snap @@ -11,7 +11,7 @@ docstring_singles_function.py:2:5: Q002 [*] Single quote docstring found but dou | = help: Replace single quotes docstring with double quotes -ℹ Suggested fix +ℹ Fix 1 1 | def foo(): 2 |- '''function without params, single line docstring''' 2 |+ """function without params, single line docstring""" @@ -32,7 +32,7 @@ docstring_singles_function.py:8:5: Q002 [*] Single quote docstring found but dou | = help: Replace single quotes docstring with double quotes -ℹ Suggested fix +ℹ Fix 5 5 | 6 6 | 7 7 | def foo2(): @@ -53,7 +53,7 @@ docstring_singles_function.py:27:5: Q002 [*] Single quote docstring found but do | = help: Replace single quotes docstring with double quotes -ℹ Suggested fix +ℹ Fix 24 24 | 25 25 | 26 26 | def function_with_single_docstring(a): diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_module_multiline.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_module_multiline.py.snap index a4858db55d..3ec2a92e01 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_module_multiline.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_module_multiline.py.snap @@ -12,7 +12,7 @@ docstring_singles_module_multiline.py:1:1: Q002 [*] Single quote docstring found | = help: Replace single quotes docstring with double quotes -ℹ Suggested fix +ℹ Fix 1 |-''' 1 |+""" 2 2 | Double quotes multiline module docstring diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_module_singleline.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_module_singleline.py.snap index b6f815e5e1..75b12d38c6 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_module_singleline.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_doubles_over_docstring_singles_module_singleline.py.snap @@ -9,7 +9,7 @@ docstring_singles_module_singleline.py:1:1: Q002 [*] Single quote docstring foun | = help: Replace single quotes docstring with double quotes -ℹ Suggested fix +ℹ Fix 1 |-''' Double quotes singleline module docstring ''' 1 |+""" Double quotes singleline module docstring """ 2 2 | ''' this is not a docstring ''' diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles.py.snap index 60352254df..3e4aff52d5 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles.py.snap @@ -12,7 +12,7 @@ docstring_doubles.py:1:1: Q002 [*] Double quote docstring found but single quote | = help: Replace double quotes docstring with single quotes -ℹ Suggested fix +ℹ Fix 1 |-""" 1 |+''' 2 2 | Double quotes multiline module docstring @@ -35,7 +35,7 @@ docstring_doubles.py:12:5: Q002 [*] Double quote docstring found but single quot | = help: Replace double quotes docstring with single quotes -ℹ Suggested fix +ℹ Fix 9 9 | l = [] 10 10 | 11 11 | class Cls: @@ -62,7 +62,7 @@ docstring_doubles.py:24:9: Q002 [*] Double quote docstring found but single quot | = help: Replace double quotes docstring with single quotes -ℹ Suggested fix +ℹ Fix 21 21 | def f(self, bar=""" 22 22 | definitely not a docstring""", 23 23 | val=l[Cls():3]): diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_class.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_class.py.snap index 743b6d6525..4f1be427c9 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_class.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_class.py.snap @@ -10,7 +10,7 @@ docstring_doubles_class.py:2:5: Q002 [*] Double quote docstring found but single | = help: Replace double quotes docstring with single quotes -ℹ Suggested fix +ℹ Fix 1 1 | class SingleLineDocstrings(): 2 |- """ Double quotes single line class docstring """ 2 |+ ''' Double quotes single line class docstring ''' @@ -27,7 +27,7 @@ docstring_doubles_class.py:6:9: Q002 [*] Double quote docstring found but single | = help: Replace double quotes docstring with single quotes -ℹ Suggested fix +ℹ Fix 3 3 | """ Not a docstring """ 4 4 | 5 5 | def foo(self, bar="""not a docstring"""): @@ -46,7 +46,7 @@ docstring_doubles_class.py:9:29: Q002 [*] Double quote docstring found but singl | = help: Replace double quotes docstring with single quotes -ℹ Suggested fix +ℹ Fix 6 6 | """ Double quotes single line method docstring""" 7 7 | pass 8 8 | diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_function.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_function.py.snap index 87d3c0c358..013f064e86 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_function.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_function.py.snap @@ -11,7 +11,7 @@ docstring_doubles_function.py:2:5: Q002 [*] Double quote docstring found but sin | = help: Replace double quotes docstring with single quotes -ℹ Suggested fix +ℹ Fix 1 1 | def foo(): 2 |- """function without params, single line docstring""" 2 |+ '''function without params, single line docstring''' @@ -32,7 +32,7 @@ docstring_doubles_function.py:8:5: Q002 [*] Double quote docstring found but sin | = help: Replace double quotes docstring with single quotes -ℹ Suggested fix +ℹ Fix 5 5 | 6 6 | 7 7 | def foo2(): @@ -53,7 +53,7 @@ docstring_doubles_function.py:27:5: Q002 [*] Double quote docstring found but si | = help: Replace double quotes docstring with single quotes -ℹ Suggested fix +ℹ Fix 24 24 | 25 25 | 26 26 | def function_with_single_docstring(a): diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_module_multiline.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_module_multiline.py.snap index 4a4a7d27dc..e2c5e024af 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_module_multiline.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_module_multiline.py.snap @@ -12,7 +12,7 @@ docstring_doubles_module_multiline.py:1:1: Q002 [*] Double quote docstring found | = help: Replace double quotes docstring with single quotes -ℹ Suggested fix +ℹ Fix 1 |-""" 1 |+''' 2 2 | Double quotes multiline module docstring diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_module_singleline.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_module_singleline.py.snap index ba51829241..c6332e463f 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_module_singleline.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_doubles_module_singleline.py.snap @@ -9,7 +9,7 @@ docstring_doubles_module_singleline.py:1:1: Q002 [*] Double quote docstring foun | = help: Replace double quotes docstring with single quotes -ℹ Suggested fix +ℹ Fix 1 |-""" Double quotes singleline module docstring """ 1 |+''' Double quotes singleline module docstring ''' 2 2 | """ this is not a docstring """ diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles.py.snap index 8724ea381a..90eb8de9cc 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles.py.snap @@ -14,7 +14,7 @@ docstring_singles.py:5:1: Q001 [*] Single quote multiline found but double quote | = help: Replace single multiline quotes with double quotes -ℹ Suggested fix +ℹ Fix 2 2 | Single quotes multiline module docstring 3 3 | ''' 4 4 | @@ -41,7 +41,7 @@ docstring_singles.py:11:21: Q001 [*] Single quote multiline found but double quo | = help: Replace single multiline quotes with double quotes -ℹ Suggested fix +ℹ Fix 8 8 | 9 9 | l = [] 10 10 | @@ -68,7 +68,7 @@ docstring_singles.py:18:5: Q001 [*] Single quote multiline found but double quot | = help: Replace single multiline quotes with double quotes -ℹ Suggested fix +ℹ Fix 15 15 | Single quotes multiline class docstring 16 16 | ''' 17 17 | @@ -93,7 +93,7 @@ docstring_singles.py:23:21: Q001 [*] Single quote multiline found but double quo | = help: Replace single multiline quotes with double quotes -ℹ Suggested fix +ℹ Fix 20 20 | ''' 21 21 | 22 22 | # The colon in the list indexing below is an edge case for the docstring scanner @@ -119,7 +119,7 @@ docstring_singles.py:32:9: Q001 [*] Single quote multiline found but double quot | = help: Replace single multiline quotes with double quotes -ℹ Suggested fix +ℹ Fix 29 29 | 30 30 | some_expression = 'hello world' 31 31 | @@ -144,7 +144,7 @@ docstring_singles.py:37:13: Q001 [*] Single quote multiline found but double quo | = help: Replace single multiline quotes with double quotes -ℹ Suggested fix +ℹ Fix 34 34 | ''' 35 35 | 36 36 | if l: diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles_class.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles_class.py.snap index e8fc6da0e5..323f502a22 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles_class.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles_class.py.snap @@ -12,7 +12,7 @@ docstring_singles_class.py:3:5: Q001 [*] Single quote multiline found but double | = help: Replace single multiline quotes with double quotes -ℹ Suggested fix +ℹ Fix 1 1 | class SingleLineDocstrings(): 2 2 | ''' Double quotes single line class docstring ''' 3 |- ''' Not a docstring ''' @@ -32,7 +32,7 @@ docstring_singles_class.py:5:23: Q001 [*] Single quote multiline found but doubl | = help: Replace single multiline quotes with double quotes -ℹ Suggested fix +ℹ Fix 2 2 | ''' Double quotes single line class docstring ''' 3 3 | ''' Not a docstring ''' 4 4 | diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles_function.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles_function.py.snap index 1c5f902f93..fe7371af8f 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles_function.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles_function.py.snap @@ -11,7 +11,7 @@ docstring_singles_function.py:3:5: Q001 [*] Single quote multiline found but dou | = help: Replace single multiline quotes with double quotes -ℹ Suggested fix +ℹ Fix 1 1 | def foo(): 2 2 | '''function without params, single line docstring''' 3 |- ''' not a docstring''' @@ -30,7 +30,7 @@ docstring_singles_function.py:11:5: Q001 [*] Single quote multiline found but do | = help: Replace single multiline quotes with double quotes -ℹ Suggested fix +ℹ Fix 8 8 | ''' 9 9 | function without params, multiline docstring 10 10 | ''' @@ -51,7 +51,7 @@ docstring_singles_function.py:15:39: Q001 [*] Single quote multiline found but d | = help: Replace single multiline quotes with double quotes -ℹ Suggested fix +ℹ Fix 12 12 | return 13 13 | 14 14 | @@ -74,7 +74,7 @@ docstring_singles_function.py:17:5: Q001 [*] Single quote multiline found but do | = help: Replace single multiline quotes with double quotes -ℹ Suggested fix +ℹ Fix 14 14 | 15 15 | def fun_with_params_no_docstring(a, b=''' 16 16 | not a @@ -93,7 +93,7 @@ docstring_singles_function.py:22:5: Q001 [*] Single quote multiline found but do | = help: Replace single multiline quotes with double quotes -ℹ Suggested fix +ℹ Fix 19 19 | 20 20 | 21 21 | def fun_with_params_no_docstring2(a, b=c[foo():], c=\ diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles_module_multiline.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles_module_multiline.py.snap index 6286084ba4..ca3d7d259d 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles_module_multiline.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles_module_multiline.py.snap @@ -14,7 +14,7 @@ docstring_singles_module_multiline.py:4:1: Q001 [*] Single quote multiline found | = help: Replace single multiline quotes with double quotes -ℹ Suggested fix +ℹ Fix 1 1 | ''' 2 2 | Double quotes multiline module docstring 3 3 | ''' @@ -38,7 +38,7 @@ docstring_singles_module_multiline.py:9:1: Q001 [*] Single quote multiline found | = help: Replace single multiline quotes with double quotes -ℹ Suggested fix +ℹ Fix 6 6 | ''' 7 7 | def foo(): 8 8 | pass diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles_module_singleline.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles_module_singleline.py.snap index 4b822211be..3d9926a1e6 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles_module_singleline.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_docstring_singles_over_docstring_singles_module_singleline.py.snap @@ -11,7 +11,7 @@ docstring_singles_module_singleline.py:2:1: Q001 [*] Single quote multiline foun | = help: Replace single multiline quotes with double quotes -ℹ Suggested fix +ℹ Fix 1 1 | ''' Double quotes singleline module docstring ''' 2 |-''' this is not a docstring ''' 2 |+""" this is not a docstring """ @@ -28,7 +28,7 @@ docstring_singles_module_singleline.py:6:1: Q001 [*] Single quote multiline foun | = help: Replace single multiline quotes with double quotes -ℹ Suggested fix +ℹ Fix 3 3 | 4 4 | def foo(): 5 5 | pass diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_doubles_over_singles.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_doubles_over_singles.py.snap index 7513bc9db0..83354230b3 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_doubles_over_singles.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_doubles_over_singles.py.snap @@ -10,7 +10,7 @@ singles.py:1:25: Q000 [*] Single quotes found but double quotes preferred | = help: Replace single quotes with double quotes -ℹ Suggested fix +ℹ Fix 1 |-this_should_be_linted = 'single quote string' 1 |+this_should_be_linted = "single quote string" 2 2 | this_should_be_linted = u'double quote string' @@ -27,7 +27,7 @@ singles.py:2:25: Q000 [*] Single quotes found but double quotes preferred | = help: Replace single quotes with double quotes -ℹ Suggested fix +ℹ Fix 1 1 | this_should_be_linted = 'single quote string' 2 |-this_should_be_linted = u'double quote string' 2 |+this_should_be_linted = u"double quote string" @@ -44,7 +44,7 @@ singles.py:3:25: Q000 [*] Single quotes found but double quotes preferred | = help: Replace single quotes with double quotes -ℹ Suggested fix +ℹ Fix 1 1 | this_should_be_linted = 'single quote string' 2 2 | this_should_be_linted = u'double quote string' 3 |-this_should_be_linted = f'double quote string' diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_doubles_over_singles_escaped.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_doubles_over_singles_escaped.py.snap index c78cedfe0a..6dd7d7e8f6 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_doubles_over_singles_escaped.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_doubles_over_singles_escaped.py.snap @@ -10,7 +10,7 @@ singles_escaped.py:1:26: Q003 [*] Change outer quotes to avoid escaping inner qu | = help: Change outer quotes to avoid escaping inner quotes -ℹ Suggested fix +ℹ Fix 1 |-this_should_raise_Q003 = "This is a \"string\"" 1 |+this_should_raise_Q003 = 'This is a "string"' 2 2 | this_is_fine = "'This' is a \"string\"" @@ -27,7 +27,7 @@ singles_escaped.py:9:5: Q003 [*] Change outer quotes to avoid escaping inner quo | = help: Change outer quotes to avoid escaping inner quotes -ℹ Suggested fix +ℹ Fix 6 6 | this_is_fine = R"This is a \"string\"" 7 7 | this_should_raise = ( 8 8 | "This is a" diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_doubles_over_singles_implicit.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_doubles_over_singles_implicit.py.snap index 23f7be19e5..24ebd5c6f3 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_doubles_over_singles_implicit.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_doubles_over_singles_implicit.py.snap @@ -11,7 +11,7 @@ singles_implicit.py:2:5: Q000 [*] Single quotes found but double quotes preferre | = help: Replace single quotes with double quotes -ℹ Suggested fix +ℹ Fix 1 1 | x = ( 2 |- 'This' 2 |+ "This" @@ -30,7 +30,7 @@ singles_implicit.py:3:5: Q000 [*] Single quotes found but double quotes preferre | = help: Replace single quotes with double quotes -ℹ Suggested fix +ℹ Fix 1 1 | x = ( 2 2 | 'This' 3 |- 'is' @@ -49,7 +49,7 @@ singles_implicit.py:4:5: Q000 [*] Single quotes found but double quotes preferre | = help: Replace single quotes with double quotes -ℹ Suggested fix +ℹ Fix 1 1 | x = ( 2 2 | 'This' 3 3 | 'is' @@ -69,7 +69,7 @@ singles_implicit.py:8:5: Q000 [*] Single quotes found but double quotes preferre | = help: Replace single quotes with double quotes -ℹ Suggested fix +ℹ Fix 5 5 | ) 6 6 | 7 7 | x = ( @@ -90,7 +90,7 @@ singles_implicit.py:9:5: Q000 [*] Single quotes found but double quotes preferre | = help: Replace single quotes with double quotes -ℹ Suggested fix +ℹ Fix 6 6 | 7 7 | x = ( 8 8 | 'This' \ @@ -110,7 +110,7 @@ singles_implicit.py:10:5: Q000 [*] Single quotes found but double quotes preferr | = help: Replace single quotes with double quotes -ℹ Suggested fix +ℹ Fix 7 7 | x = ( 8 8 | 'This' \ 9 9 | 'is' \ @@ -129,7 +129,7 @@ singles_implicit.py:27:1: Q000 [*] Single quotes found but double quotes preferr | = help: Replace single quotes with double quotes -ℹ Suggested fix +ℹ Fix 24 24 | 25 25 | if True: 26 26 | 'This can use "single" quotes' diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_doubles_over_singles_multiline_string.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_doubles_over_singles_multiline_string.py.snap index 8477c544b3..20cd5ce322 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_doubles_over_singles_multiline_string.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_doubles_over_singles_multiline_string.py.snap @@ -13,7 +13,7 @@ singles_multiline_string.py:1:5: Q001 [*] Single quote multiline found but doubl | = help: Replace single multiline quotes with double quotes -ℹ Suggested fix +ℹ Fix 1 |-s = ''' This 'should' 1 |+s = """ This 'should' 2 2 | be diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_singles_over_doubles.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_singles_over_doubles.py.snap index b7206b147e..0550913c80 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_singles_over_doubles.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_singles_over_doubles.py.snap @@ -10,7 +10,7 @@ doubles.py:1:25: Q000 [*] Double quotes found but single quotes preferred | = help: Replace double quotes with single quotes -ℹ Suggested fix +ℹ Fix 1 |-this_should_be_linted = "double quote string" 1 |+this_should_be_linted = 'double quote string' 2 2 | this_should_be_linted = u"double quote string" @@ -27,7 +27,7 @@ doubles.py:2:25: Q000 [*] Double quotes found but single quotes preferred | = help: Replace double quotes with single quotes -ℹ Suggested fix +ℹ Fix 1 1 | this_should_be_linted = "double quote string" 2 |-this_should_be_linted = u"double quote string" 2 |+this_should_be_linted = u'double quote string' @@ -44,7 +44,7 @@ doubles.py:3:25: Q000 [*] Double quotes found but single quotes preferred | = help: Replace double quotes with single quotes -ℹ Suggested fix +ℹ Fix 1 1 | this_should_be_linted = "double quote string" 2 2 | this_should_be_linted = u"double quote string" 3 |-this_should_be_linted = f"double quote string" diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_singles_over_doubles_escaped.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_singles_over_doubles_escaped.py.snap index 743d3219d3..6a31d7a11f 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_singles_over_doubles_escaped.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_singles_over_doubles_escaped.py.snap @@ -10,7 +10,7 @@ doubles_escaped.py:1:26: Q003 [*] Change outer quotes to avoid escaping inner qu | = help: Change outer quotes to avoid escaping inner quotes -ℹ Suggested fix +ℹ Fix 1 |-this_should_raise_Q003 = 'This is a \'string\'' 1 |+this_should_raise_Q003 = "This is a 'string'" 2 2 | this_should_raise_Q003 = 'This is \\ a \\\'string\'' @@ -27,7 +27,7 @@ doubles_escaped.py:2:26: Q003 [*] Change outer quotes to avoid escaping inner qu | = help: Change outer quotes to avoid escaping inner quotes -ℹ Suggested fix +ℹ Fix 1 1 | this_should_raise_Q003 = 'This is a \'string\'' 2 |-this_should_raise_Q003 = 'This is \\ a \\\'string\'' 2 |+this_should_raise_Q003 = "This is \\ a \\'string'" @@ -45,7 +45,7 @@ doubles_escaped.py:10:5: Q003 [*] Change outer quotes to avoid escaping inner qu | = help: Change outer quotes to avoid escaping inner quotes -ℹ Suggested fix +ℹ Fix 7 7 | this_is_fine = R'This is a \'string\'' 8 8 | this_should_raise = ( 9 9 | 'This is a' diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_singles_over_doubles_implicit.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_singles_over_doubles_implicit.py.snap index 6b1bb3c26a..dcd5cd4f75 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_singles_over_doubles_implicit.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_singles_over_doubles_implicit.py.snap @@ -11,7 +11,7 @@ doubles_implicit.py:2:5: Q000 [*] Double quotes found but single quotes preferre | = help: Replace double quotes with single quotes -ℹ Suggested fix +ℹ Fix 1 1 | x = ( 2 |- "This" 2 |+ 'This' @@ -30,7 +30,7 @@ doubles_implicit.py:3:5: Q000 [*] Double quotes found but single quotes preferre | = help: Replace double quotes with single quotes -ℹ Suggested fix +ℹ Fix 1 1 | x = ( 2 2 | "This" 3 |- "is" @@ -49,7 +49,7 @@ doubles_implicit.py:4:5: Q000 [*] Double quotes found but single quotes preferre | = help: Replace double quotes with single quotes -ℹ Suggested fix +ℹ Fix 1 1 | x = ( 2 2 | "This" 3 3 | "is" @@ -69,7 +69,7 @@ doubles_implicit.py:8:5: Q000 [*] Double quotes found but single quotes preferre | = help: Replace double quotes with single quotes -ℹ Suggested fix +ℹ Fix 5 5 | ) 6 6 | 7 7 | x = ( @@ -90,7 +90,7 @@ doubles_implicit.py:9:5: Q000 [*] Double quotes found but single quotes preferre | = help: Replace double quotes with single quotes -ℹ Suggested fix +ℹ Fix 6 6 | 7 7 | x = ( 8 8 | "This" \ @@ -110,7 +110,7 @@ doubles_implicit.py:10:5: Q000 [*] Double quotes found but single quotes preferr | = help: Replace double quotes with single quotes -ℹ Suggested fix +ℹ Fix 7 7 | x = ( 8 8 | "This" \ 9 9 | "is" \ @@ -129,7 +129,7 @@ doubles_implicit.py:27:1: Q000 [*] Double quotes found but single quotes preferr | = help: Replace double quotes with single quotes -ℹ Suggested fix +ℹ Fix 24 24 | 25 25 | if True: 26 26 | "This can use 'double' quotes" diff --git a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_singles_over_doubles_multiline_string.py.snap b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_singles_over_doubles_multiline_string.py.snap index 67502e94f2..07ee108c25 100644 --- a/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_singles_over_doubles_multiline_string.py.snap +++ b/crates/ruff/src/rules/flake8_quotes/snapshots/ruff__rules__flake8_quotes__tests__require_singles_over_doubles_multiline_string.py.snap @@ -13,7 +13,7 @@ doubles_multiline_string.py:1:5: Q001 [*] Double quote multiline found but singl | = help: Replace double multiline quotes with single quotes -ℹ Suggested fix +ℹ Fix 1 |-s = """ This "should" 1 |+s = ''' This "should" 2 2 | be diff --git a/crates/ruff/src/rules/flake8_raise/rules/mod.rs b/crates/ruff/src/rules/flake8_raise/rules/mod.rs index 12efaed6ba..2df0cf3dd3 100644 --- a/crates/ruff/src/rules/flake8_raise/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_raise/rules/mod.rs @@ -1,5 +1,3 @@ -pub(crate) use unnecessary_paren_on_raise_exception::{ - unnecessary_paren_on_raise_exception, UnnecessaryParenOnRaiseException, -}; +pub(crate) use unnecessary_paren_on_raise_exception::*; mod unnecessary_paren_on_raise_exception; diff --git a/crates/ruff/src/rules/flake8_raise/rules/unnecessary_paren_on_raise_exception.rs b/crates/ruff/src/rules/flake8_raise/rules/unnecessary_paren_on_raise_exception.rs index a37c60da20..b9fbec4697 100644 --- a/crates/ruff/src/rules/flake8_raise/rules/unnecessary_paren_on_raise_exception.rs +++ b/crates/ruff/src/rules/flake8_raise/rules/unnecessary_paren_on_raise_exception.rs @@ -2,11 +2,34 @@ use rustpython_parser::ast::{self, Expr, Ranged}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; + use ruff_python_ast::helpers::match_parens; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Checks for unnecessary parentheses on raised exceptions. +/// +/// ## Why is this bad? +/// If an exception is raised without any arguments, parentheses are not +/// required, as the `raise` statement accepts either an exception instance +/// or an exception class (which is then implicitly instantiated). +/// +/// Removing the parentheses makes the code more concise. +/// +/// ## Example +/// ```python +/// raise TypeError() +/// ``` +/// +/// Use instead: +/// ```python +/// raise TypeError +/// ``` +/// +/// ## References +/// - [Python documentation: The `raise` statement](https://docs.python.org/3/reference/simple_stmts.html#the-raise-statement) #[violation] pub struct UnnecessaryParenOnRaiseException; @@ -31,12 +54,22 @@ pub(crate) fn unnecessary_paren_on_raise_exception(checker: &mut Checker, expr: }) = expr { if args.is_empty() && keywords.is_empty() { + // `raise func()` still requires parentheses; only `raise Class()` does not. + if checker + .semantic() + .lookup_attribute(func) + .map_or(false, |id| { + checker.semantic().binding(id).kind.is_function_definition() + }) + { + return; + } + let range = match_parens(func.end(), checker.locator) .expect("Expected call to include parentheses"); let mut diagnostic = Diagnostic::new(UnnecessaryParenOnRaiseException, range); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::deletion(func.end(), range.end()))); + diagnostic.set_fix(Fix::automatic(Edit::deletion(func.end(), range.end()))); } checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/flake8_raise/snapshots/ruff__rules__flake8_raise__tests__unnecessary-paren-on-raise-exception_RSE102.py.snap b/crates/ruff/src/rules/flake8_raise/snapshots/ruff__rules__flake8_raise__tests__unnecessary-paren-on-raise-exception_RSE102.py.snap index 1982ba25a2..4f905dd654 100644 --- a/crates/ruff/src/rules/flake8_raise/snapshots/ruff__rules__flake8_raise__tests__unnecessary-paren-on-raise-exception_RSE102.py.snap +++ b/crates/ruff/src/rules/flake8_raise/snapshots/ruff__rules__flake8_raise__tests__unnecessary-paren-on-raise-exception_RSE102.py.snap @@ -12,7 +12,7 @@ RSE102.py:5:21: RSE102 [*] Unnecessary parentheses on raised exception | = help: Remove unnecessary parentheses -ℹ Suggested fix +ℹ Fix 2 2 | y = 6 + "7" 3 3 | except TypeError: 4 4 | # RSE102 @@ -32,7 +32,7 @@ RSE102.py:13:16: RSE102 [*] Unnecessary parentheses on raised exception | = help: Remove unnecessary parentheses -ℹ Suggested fix +ℹ Fix 10 10 | raise 11 11 | 12 12 | # RSE102 @@ -52,7 +52,7 @@ RSE102.py:16:17: RSE102 [*] Unnecessary parentheses on raised exception | = help: Remove unnecessary parentheses -ℹ Suggested fix +ℹ Fix 13 13 | raise TypeError() 14 14 | 15 15 | # RSE102 @@ -73,7 +73,7 @@ RSE102.py:20:5: RSE102 [*] Unnecessary parentheses on raised exception | = help: Remove unnecessary parentheses -ℹ Suggested fix +ℹ Fix 16 16 | raise TypeError () 17 17 | 18 18 | # RSE102 @@ -97,7 +97,7 @@ RSE102.py:23:16: RSE102 [*] Unnecessary parentheses on raised exception | = help: Remove unnecessary parentheses -ℹ Suggested fix +ℹ Fix 20 20 | () 21 21 | 22 22 | # RSE102 @@ -118,11 +118,11 @@ RSE102.py:28:16: RSE102 [*] Unnecessary parentheses on raised exception 30 | | ) | |_^ RSE102 31 | -32 | raise AssertionError +32 | # OK | = help: Remove unnecessary parentheses -ℹ Suggested fix +ℹ Fix 25 25 | ) 26 26 | 27 27 | # RSE102 @@ -131,7 +131,7 @@ RSE102.py:28:16: RSE102 [*] Unnecessary parentheses on raised exception 30 |-) 28 |+raise TypeError 31 29 | -32 30 | raise AssertionError -33 31 | +32 30 | # OK +33 31 | raise AssertionError diff --git a/crates/ruff/src/rules/flake8_return/rules/function.rs b/crates/ruff/src/rules/flake8_return/rules/function.rs index 8f5e3aaf88..01d27636d3 100644 --- a/crates/ruff/src/rules/flake8_return/rules/function.rs +++ b/crates/ruff/src/rules/flake8_return/rules/function.rs @@ -1,16 +1,16 @@ use std::ops::Add; use ruff_text_size::{TextRange, TextSize}; -use rustpython_parser::ast::{self, Constant, Expr, Ranged, Stmt}; +use rustpython_parser::ast::{self, Expr, Ranged, Stmt}; use ruff_diagnostics::{AlwaysAutofixableViolation, Violation}; use ruff_diagnostics::{Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::elif_else_range; use ruff_python_ast::helpers::is_const_none; +use ruff_python_ast::helpers::{elif_else_range, is_const_false, is_const_true}; use ruff_python_ast::visitor::Visitor; use ruff_python_ast::whitespace::indentation; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; use crate::autofix::edits; use crate::checkers::ast::Checker; @@ -339,13 +339,7 @@ fn unnecessary_return_none(checker: &mut Checker, stack: &Stack) { let Some(expr) = stmt.value.as_deref() else { continue; }; - if !matches!( - expr, - Expr::Constant(ast::ExprConstant { - value: Constant::None, - .. - }) - ) { + if !is_const_none(expr) { continue; } let mut diagnostic = Diagnostic::new(UnnecessaryReturnNone, stmt.range); @@ -376,34 +370,17 @@ fn implicit_return_value(checker: &mut Checker, stack: &Stack) { } } -const NORETURN_FUNCS: &[&[&str]] = &[ - // builtins - &["", "exit"], - &["", "quit"], - // stdlib - &["builtins", "exit"], - &["builtins", "quit"], - &["os", "_exit"], - &["os", "abort"], - &["posix", "_exit"], - &["posix", "abort"], - &["sys", "exit"], - &["_thread", "exit"], - &["_winapi", "ExitProcess"], - // third-party modules - &["pytest", "exit"], - &["pytest", "fail"], - &["pytest", "skip"], - &["pytest", "xfail"], -]; - /// Return `true` if the `func` is a known function that never returns. -fn is_noreturn_func(model: &SemanticModel, func: &Expr) -> bool { - model.resolve_call_path(func).map_or(false, |call_path| { - NORETURN_FUNCS - .iter() - .any(|target| call_path.as_slice() == *target) - || model.match_typing_call_path(&call_path, "assert_never") +fn is_noreturn_func(func: &Expr, semantic: &SemanticModel) -> bool { + semantic.resolve_call_path(func).map_or(false, |call_path| { + matches!( + call_path.as_slice(), + ["" | "builtins" | "sys" | "_thread" | "pytest", "exit"] + | ["" | "builtins", "quit"] + | ["os" | "posix", "_exit" | "abort"] + | ["_winapi", "ExitProcess"] + | ["pytest", "fail" | "skip" | "xfail"] + ) || semantic.match_typing_call_path(&call_path, "assert_never") }) } @@ -433,22 +410,8 @@ fn implicit_return(checker: &mut Checker, stmt: &Stmt) { checker.diagnostics.push(diagnostic); } } - Stmt::Assert(ast::StmtAssert { test, .. }) - if matches!( - test.as_ref(), - Expr::Constant(ast::ExprConstant { - value: Constant::Bool(false), - .. - }) - ) => {} - Stmt::While(ast::StmtWhile { test, .. }) - if matches!( - test.as_ref(), - Expr::Constant(ast::ExprConstant { - value: Constant::Bool(true), - .. - }) - ) => {} + Stmt::Assert(ast::StmtAssert { test, .. }) if is_const_false(test) => {} + Stmt::While(ast::StmtWhile { test, .. }) if is_const_true(test) => {} Stmt::For(ast::StmtFor { orelse, .. }) | Stmt::AsyncFor(ast::StmtAsyncFor { orelse, .. }) | Stmt::While(ast::StmtWhile { orelse, .. }) => { @@ -489,7 +452,7 @@ fn implicit_return(checker: &mut Checker, stmt: &Stmt) { if matches!( value.as_ref(), Expr::Call(ast::ExprCall { func, .. }) - if is_noreturn_func(checker.semantic_model(), func) + if is_noreturn_func(func, checker.semantic()) ) => {} _ => { let mut diagnostic = Diagnostic::new(ImplicitReturn, stmt.range()); @@ -518,7 +481,10 @@ fn unnecessary_assign(checker: &mut Checker, stack: &Stack) { continue; }; - let Expr::Name(ast::ExprName { id: returned_id, .. }) = value.as_ref() else { + let Expr::Name(ast::ExprName { + id: returned_id, .. + }) = value.as_ref() + else { continue; }; @@ -531,7 +497,10 @@ fn unnecessary_assign(checker: &mut Checker, stack: &Stack) { continue; }; - let Expr::Name(ast::ExprName { id: assigned_id, .. }) = target else { + let Expr::Name(ast::ExprName { + id: assigned_id, .. + }) = target + else { continue; }; @@ -554,13 +523,8 @@ fn unnecessary_assign(checker: &mut Checker, stack: &Stack) { // Delete the `return` statement. There's no need to treat this as an isolated // edit, since we're editing the preceding statement, so no conflicting edit would // be allowed to remove that preceding statement. - let delete_return = edits::delete_stmt( - stmt, - None, - checker.locator, - checker.indexer, - checker.stylist, - ); + let delete_return = + edits::delete_stmt(stmt, None, checker.locator, checker.indexer); // Replace the `x = 1` statement with `return 1`. let content = checker.locator.slice(assign.range()); diff --git a/crates/ruff/src/rules/flake8_return/rules/mod.rs b/crates/ruff/src/rules/flake8_return/rules/mod.rs index 7e6a3fbcf7..7435e8a228 100644 --- a/crates/ruff/src/rules/flake8_return/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_return/rules/mod.rs @@ -1,6 +1,3 @@ -pub(crate) use function::{ - function, ImplicitReturn, ImplicitReturnValue, SuperfluousElseBreak, SuperfluousElseContinue, - SuperfluousElseRaise, SuperfluousElseReturn, UnnecessaryAssign, UnnecessaryReturnNone, -}; +pub(crate) use function::*; mod function; diff --git a/crates/ruff/src/rules/flake8_return/visitor.rs b/crates/ruff/src/rules/flake8_return/visitor.rs index d07c12b184..32b7daa93d 100644 --- a/crates/ruff/src/rules/flake8_return/visitor.rs +++ b/crates/ruff/src/rules/flake8_return/visitor.rs @@ -5,27 +5,27 @@ use ruff_python_ast::visitor; use ruff_python_ast::visitor::Visitor; #[derive(Default)] -pub(crate) struct Stack<'a> { +pub(super) struct Stack<'a> { /// The `return` statements in the current function. - pub(crate) returns: Vec<&'a ast::StmtReturn>, + pub(super) returns: Vec<&'a ast::StmtReturn>, /// The `else` statements in the current function. - pub(crate) elses: Vec<&'a ast::StmtIf>, + pub(super) elses: Vec<&'a ast::StmtIf>, /// The `elif` statements in the current function. - pub(crate) elifs: Vec<&'a ast::StmtIf>, + pub(super) elifs: Vec<&'a ast::StmtIf>, /// The non-local variables in the current function. - pub(crate) non_locals: FxHashSet<&'a str>, + pub(super) non_locals: FxHashSet<&'a str>, /// Whether the current function is a generator. - pub(crate) is_generator: bool, + pub(super) is_generator: bool, /// The `assignment`-to-`return` statement pairs in the current function. /// TODO(charlie): Remove the extra [`Stmt`] here, which is necessary to support statement /// removal for the `return` statement. - pub(crate) assignment_return: Vec<(&'a ast::StmtAssign, &'a ast::StmtReturn, &'a Stmt)>, + pub(super) assignment_return: Vec<(&'a ast::StmtAssign, &'a ast::StmtReturn, &'a Stmt)>, } #[derive(Default)] -pub(crate) struct ReturnVisitor<'a> { +pub(super) struct ReturnVisitor<'a> { /// The current stack of nodes. - pub(crate) stack: Stack<'a>, + pub(super) stack: Stack<'a>, /// The preceding sibling of the current node. sibling: Option<&'a Stmt>, /// The parent nodes of the current node. diff --git a/crates/ruff/src/rules/flake8_self/rules/mod.rs b/crates/ruff/src/rules/flake8_self/rules/mod.rs index edd1f1a2f7..f2dfbef12a 100644 --- a/crates/ruff/src/rules/flake8_self/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_self/rules/mod.rs @@ -1,3 +1,3 @@ -pub(crate) use private_member_access::{private_member_access, PrivateMemberAccess}; +pub(crate) use private_member_access::*; mod private_member_access; diff --git a/crates/ruff/src/rules/flake8_self/rules/private_member_access.rs b/crates/ruff/src/rules/flake8_self/rules/private_member_access.rs index 40ebfdf1ab..c20d3f77dd 100644 --- a/crates/ruff/src/rules/flake8_self/rules/private_member_access.rs +++ b/crates/ruff/src/rules/flake8_self/rules/private_member_access.rs @@ -3,7 +3,7 @@ use rustpython_parser::ast::{self, Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::call_path::collect_call_path; -use ruff_python_semantic::scope::ScopeKind; +use ruff_python_semantic::ScopeKind; use crate::checkers::ast::Checker; @@ -20,9 +20,6 @@ use crate::checkers::ast::Checker; /// versions, that it will have the same type, or that it will have the same /// behavior. Instead, use the class's public interface. /// -/// ## Options -/// - `flake8-self.ignore-names` -/// /// ## Example /// ```python /// class Class: @@ -45,6 +42,9 @@ use crate::checkers::ast::Checker; /// print(var.public_member) /// ``` /// +/// ## Options +/// - `flake8-self.ignore-names` +/// /// ## References /// - [_What is the meaning of single or double underscores before an object name?_](https://stackoverflow.com/questions/1301346/what-is-the-meaning-of-single-and-double-underscore-before-an-object-name) #[violation] @@ -77,7 +77,7 @@ pub(crate) fn private_member_access(checker: &mut Checker, expr: &Expr) { // Ignore accesses on instances within special methods (e.g., `__eq__`). if let ScopeKind::Function(ast::StmtFunctionDef { name, .. }) = - checker.semantic_model().scope().kind + checker.semantic().scope().kind { if matches!( name.as_str(), @@ -136,22 +136,19 @@ pub(crate) fn private_member_access(checker: &mut Checker, expr: &Expr) { if let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() { // Ignore `super()` calls. if let Some(call_path) = collect_call_path(func) { - if call_path.as_slice() == ["super"] { + if matches!(call_path.as_slice(), ["super"]) { return; } } } else if let Some(call_path) = collect_call_path(value) { // Ignore `self` and `cls` accesses. - if call_path.as_slice() == ["self"] - || call_path.as_slice() == ["cls"] - || call_path.as_slice() == ["mcs"] - { + if matches!(call_path.as_slice(), ["self" | "cls" | "mcs"]) { return; } // Ignore accesses on class members from _within_ the class. if checker - .semantic_model() + .semantic() .scopes .iter() .rev() @@ -162,7 +159,7 @@ pub(crate) fn private_member_access(checker: &mut Checker, expr: &Expr) { .map_or(false, |name| { if call_path.as_slice() == [name.as_str()] { checker - .semantic_model() + .semantic() .find_binding(name) .map_or(false, |binding| { // TODO(charlie): Could the name ever be bound to a diff --git a/crates/ruff/src/rules/flake8_self/settings.rs b/crates/ruff/src/rules/flake8_self/settings.rs index 728b64c97c..004815516d 100644 --- a/crates/ruff/src/rules/flake8_self/settings.rs +++ b/crates/ruff/src/rules/flake8_self/settings.rs @@ -4,9 +4,18 @@ use serde::{Deserialize, Serialize}; use ruff_macros::{CacheKey, CombineOptions, ConfigurationOptions}; -// By default, ignore the `namedtuple` methods and attributes, which are underscore-prefixed to -// prevent conflicts with field names. -const IGNORE_NAMES: [&str; 5] = ["_make", "_asdict", "_replace", "_fields", "_field_defaults"]; +// By default, ignore the `namedtuple` methods and attributes, as well as the +// _sunder_ names in Enum, which are underscore-prefixed to prevent conflicts +// with field names. +const IGNORE_NAMES: [&str; 7] = [ + "_make", + "_asdict", + "_replace", + "_fields", + "_field_defaults", + "_name_", + "_value_", +]; #[derive( Debug, PartialEq, Eq, Serialize, Deserialize, Default, ConfigurationOptions, CombineOptions, @@ -19,7 +28,7 @@ const IGNORE_NAMES: [&str; 5] = ["_make", "_asdict", "_replace", "_fields", "_fi #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct Options { #[option( - default = r#"["_make", "_asdict", "_replace", "_fields", "_field_defaults"]"#, + default = r#"["_make", "_asdict", "_replace", "_fields", "_field_defaults", "_name_", "_value_"]"#, value_type = "list[str]", example = r#" ignore-names = ["_new"] diff --git a/crates/ruff/src/rules/flake8_simplify/rules/ast_bool_op.rs b/crates/ruff/src/rules/flake8_simplify/rules/ast_bool_op.rs index 3ed0b7492b..df882a4b0e 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/ast_bool_op.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/ast_bool_op.rs @@ -5,7 +5,7 @@ use itertools::Either::{Left, Right}; use itertools::Itertools; use ruff_text_size::TextRange; use rustc_hash::FxHashMap; -use rustpython_parser::ast::{self, Boolop, Cmpop, Expr, ExprContext, Ranged, Unaryop}; +use rustpython_parser::ast::{self, BoolOp, CmpOp, Expr, ExprContext, Ranged, UnaryOp}; use ruff_diagnostics::{AlwaysAutofixableViolation, AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -40,7 +40,7 @@ use crate::registry::AsRule; /// ``` /// /// ## References -/// - [Python: "isinstance"](https://docs.python.org/3/library/functions.html#isinstance) +/// - [Python documentation: `isinstance`](https://docs.python.org/3/library/functions.html#isinstance) #[violation] pub struct DuplicateIsinstanceCall { name: Option, @@ -299,7 +299,12 @@ fn is_same_expr<'a>(a: &'a Expr, b: &'a Expr) -> Option<&'a str> { /// SIM101 pub(crate) fn duplicate_isinstance_call(checker: &mut Checker, expr: &Expr) { - let Expr::BoolOp(ast::ExprBoolOp { op: Boolop::Or, values, range: _ } )= expr else { + let Expr::BoolOp(ast::ExprBoolOp { + op: BoolOp::Or, + values, + range: _, + }) = expr + else { return; }; @@ -308,7 +313,13 @@ pub(crate) fn duplicate_isinstance_call(checker: &mut Checker, expr: &Expr) { let mut duplicates: FxHashMap> = FxHashMap::default(); for (index, call) in values.iter().enumerate() { // Verify that this is an `isinstance` call. - let Expr::Call(ast::ExprCall { func, args, keywords, range: _ }) = &call else { + let Expr::Call(ast::ExprCall { + func, + args, + keywords, + range: _, + }) = &call + else { continue; }; if args.len() != 2 { @@ -323,7 +334,7 @@ pub(crate) fn duplicate_isinstance_call(checker: &mut Checker, expr: &Expr) { if func_name != "isinstance" { continue; } - if !checker.semantic_model().is_builtin("isinstance") { + if !checker.semantic().is_builtin("isinstance") { continue; } @@ -356,7 +367,7 @@ pub(crate) fn duplicate_isinstance_call(checker: &mut Checker, expr: &Expr) { expr.range(), ); if checker.patch(diagnostic.kind.rule()) { - if !contains_effect(target, |id| checker.semantic_model().is_builtin(id)) { + if !contains_effect(target, |id| checker.semantic().is_builtin(id)) { // Grab the types used in each duplicate `isinstance` call (e.g., `int` and `str` // in `isinstance(obj, int) or isinstance(obj, str)`). let types: Vec<&Expr> = indices @@ -402,7 +413,7 @@ pub(crate) fn duplicate_isinstance_call(checker: &mut Checker, expr: &Expr) { // Generate the combined `BoolOp`. let node = ast::ExprBoolOp { - op: Boolop::Or, + op: BoolOp::Or, values: iter::once(call) .chain( values @@ -418,8 +429,7 @@ pub(crate) fn duplicate_isinstance_call(checker: &mut Checker, expr: &Expr) { // Populate the `Fix`. Replace the _entire_ `BoolOp`. Note that if we have // multiple duplicates, the fixes will conflict. - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( checker.generator().expr(&bool_op), expr.range(), ))); @@ -431,13 +441,19 @@ pub(crate) fn duplicate_isinstance_call(checker: &mut Checker, expr: &Expr) { } fn match_eq_target(expr: &Expr) -> Option<(&str, &Expr)> { - let Expr::Compare(ast::ExprCompare { left, ops, comparators, range: _ } )= expr else { + let Expr::Compare(ast::ExprCompare { + left, + ops, + comparators, + range: _, + }) = expr + else { return None; }; if ops.len() != 1 || comparators.len() != 1 { return None; } - if !matches!(&ops[0], Cmpop::Eq) { + if !matches!(&ops[0], CmpOp::Eq) { return None; } let Expr::Name(ast::ExprName { id, .. }) = left.as_ref() else { @@ -452,7 +468,12 @@ fn match_eq_target(expr: &Expr) -> Option<(&str, &Expr)> { /// SIM109 pub(crate) fn compare_with_tuple(checker: &mut Checker, expr: &Expr) { - let Expr::BoolOp(ast::ExprBoolOp { op: Boolop::Or, values, range: _ }) = expr else { + let Expr::BoolOp(ast::ExprBoolOp { + op: BoolOp::Or, + values, + range: _, + }) = expr + else { return; }; @@ -478,7 +499,7 @@ pub(crate) fn compare_with_tuple(checker: &mut Checker, expr: &Expr) { // Avoid rewriting (e.g.) `a == "foo" or a == f()`. if comparators .iter() - .any(|expr| contains_effect(expr, |id| checker.semantic_model().is_builtin(id))) + .any(|expr| contains_effect(expr, |id| checker.semantic().is_builtin(id))) { continue; } @@ -501,7 +522,7 @@ pub(crate) fn compare_with_tuple(checker: &mut Checker, expr: &Expr) { }; let node2 = ast::ExprCompare { left: Box::new(node1.into()), - ops: vec![Cmpop::In], + ops: vec![CmpOp::In], comparators: vec![node.into()], range: TextRange::default(), }; @@ -524,14 +545,13 @@ pub(crate) fn compare_with_tuple(checker: &mut Checker, expr: &Expr) { } else { // Wrap in a `x in (a, b) or ...` boolean operation. let node = ast::ExprBoolOp { - op: Boolop::Or, + op: BoolOp::Or, values: iter::once(in_expr).chain(unmatched).collect(), range: TextRange::default(), }; node.into() }; - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( checker.generator().expr(&in_expr), expr.range(), ))); @@ -542,7 +562,12 @@ pub(crate) fn compare_with_tuple(checker: &mut Checker, expr: &Expr) { /// SIM220 pub(crate) fn expr_and_not_expr(checker: &mut Checker, expr: &Expr) { - let Expr::BoolOp(ast::ExprBoolOp { op: Boolop::And, values, range: _, }) = expr else { + let Expr::BoolOp(ast::ExprBoolOp { + op: BoolOp::And, + values, + range: _, + }) = expr + else { return; }; if values.len() < 2 { @@ -554,7 +579,7 @@ pub(crate) fn expr_and_not_expr(checker: &mut Checker, expr: &Expr) { let mut non_negated_expr = vec![]; for expr in values { if let Expr::UnaryOp(ast::ExprUnaryOp { - op: Unaryop::Not, + op: UnaryOp::Not, operand, range: _, }) = expr @@ -569,7 +594,7 @@ pub(crate) fn expr_and_not_expr(checker: &mut Checker, expr: &Expr) { return; } - if contains_effect(expr, |id| checker.semantic_model().is_builtin(id)) { + if contains_effect(expr, |id| checker.semantic().is_builtin(id)) { return; } @@ -583,8 +608,7 @@ pub(crate) fn expr_and_not_expr(checker: &mut Checker, expr: &Expr) { expr.range(), ); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( "False".to_string(), expr.range(), ))); @@ -597,7 +621,12 @@ pub(crate) fn expr_and_not_expr(checker: &mut Checker, expr: &Expr) { /// SIM221 pub(crate) fn expr_or_not_expr(checker: &mut Checker, expr: &Expr) { - let Expr::BoolOp(ast::ExprBoolOp { op: Boolop::Or, values, range: _, }) = expr else { + let Expr::BoolOp(ast::ExprBoolOp { + op: BoolOp::Or, + values, + range: _, + }) = expr + else { return; }; if values.len() < 2 { @@ -609,7 +638,7 @@ pub(crate) fn expr_or_not_expr(checker: &mut Checker, expr: &Expr) { let mut non_negated_expr = vec![]; for expr in values { if let Expr::UnaryOp(ast::ExprUnaryOp { - op: Unaryop::Not, + op: UnaryOp::Not, operand, range: _, }) = expr @@ -624,7 +653,7 @@ pub(crate) fn expr_or_not_expr(checker: &mut Checker, expr: &Expr) { return; } - if contains_effect(expr, |id| checker.semantic_model().is_builtin(id)) { + if contains_effect(expr, |id| checker.semantic().is_builtin(id)) { return; } @@ -638,8 +667,7 @@ pub(crate) fn expr_or_not_expr(checker: &mut Checker, expr: &Expr) { expr.range(), ); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( "True".to_string(), expr.range(), ))); @@ -673,18 +701,23 @@ pub(crate) fn get_short_circuit_edit( fn is_short_circuit( expr: &Expr, - expected_op: Boolop, + expected_op: BoolOp, checker: &Checker, ) -> Option<(Edit, ContentAround)> { - let Expr::BoolOp(ast::ExprBoolOp { op, values, range: _, }) = expr else { + let Expr::BoolOp(ast::ExprBoolOp { + op, + values, + range: _, + }) = expr + else { return None; }; if *op != expected_op { return None; } let short_circuit_truthiness = match op { - Boolop::And => Truthiness::Falsey, - Boolop::Or => Truthiness::Truthy, + BoolOp::And => Truthiness::Falsey, + BoolOp::Or => Truthiness::Truthy, }; let mut location = expr.start(); @@ -693,15 +726,14 @@ fn is_short_circuit( for (index, (value, next_value)) in values.iter().tuple_windows().enumerate() { // Keep track of the location of the furthest-right, truthy or falsey expression. - let value_truthiness = - Truthiness::from_expr(value, |id| checker.semantic_model().is_builtin(id)); + let value_truthiness = Truthiness::from_expr(value, |id| checker.semantic().is_builtin(id)); let next_value_truthiness = - Truthiness::from_expr(next_value, |id| checker.semantic_model().is_builtin(id)); + Truthiness::from_expr(next_value, |id| checker.semantic().is_builtin(id)); // Keep track of the location of the furthest-right, non-effectful expression. if value_truthiness.is_unknown() - && (!checker.semantic_model().in_boolean_test() - || contains_effect(value, |id| checker.semantic_model().is_builtin(id))) + && (!checker.semantic().in_boolean_test() + || contains_effect(value, |id| checker.semantic().is_builtin(id))) { location = next_value.start(); continue; @@ -721,7 +753,7 @@ fn is_short_circuit( value, TextRange::new(location, expr.end()), short_circuit_truthiness, - checker.semantic_model().in_boolean_test(), + checker.semantic().in_boolean_test(), checker, )); break; @@ -739,7 +771,7 @@ fn is_short_circuit( next_value, TextRange::new(location, expr.end()), short_circuit_truthiness, - checker.semantic_model().in_boolean_test(), + checker.semantic().in_boolean_test(), checker, )); break; @@ -754,7 +786,7 @@ fn is_short_circuit( /// SIM222 pub(crate) fn expr_or_true(checker: &mut Checker, expr: &Expr) { - if let Some((edit, remove)) = is_short_circuit(expr, Boolop::Or, checker) { + if let Some((edit, remove)) = is_short_circuit(expr, BoolOp::Or, checker) { let mut diagnostic = Diagnostic::new( ExprOrTrue { expr: edit.content().unwrap_or_default().to_string(), @@ -763,8 +795,7 @@ pub(crate) fn expr_or_true(checker: &mut Checker, expr: &Expr) { edit.range(), ); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(edit)); + diagnostic.set_fix(Fix::suggested(edit)); } checker.diagnostics.push(diagnostic); } @@ -772,7 +803,7 @@ pub(crate) fn expr_or_true(checker: &mut Checker, expr: &Expr) { /// SIM223 pub(crate) fn expr_and_false(checker: &mut Checker, expr: &Expr) { - if let Some((edit, remove)) = is_short_circuit(expr, Boolop::And, checker) { + if let Some((edit, remove)) = is_short_circuit(expr, BoolOp::And, checker) { let mut diagnostic = Diagnostic::new( ExprAndFalse { expr: edit.content().unwrap_or_default().to_string(), @@ -781,8 +812,7 @@ pub(crate) fn expr_and_false(checker: &mut Checker, expr: &Expr) { edit.range(), ); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(edit)); + diagnostic.set_fix(Fix::suggested(edit)); } checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/flake8_simplify/rules/ast_expr.rs b/crates/ruff/src/rules/flake8_simplify/rules/ast_expr.rs index f552b6f378..bf9409f4bd 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/ast_expr.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/ast_expr.rs @@ -3,6 +3,7 @@ use rustpython_parser::ast::{self, Constant, Expr, Ranged}; use ruff_diagnostics::{AlwaysAutofixableViolation, AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers::is_const_none; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -73,7 +74,7 @@ impl Violation for UncapitalizedEnvironmentVariables { /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/stdtypes.html#dict.get) +/// - [Python documentation: `dict.get`](https://docs.python.org/3/library/stdtypes.html#dict.get) #[violation] pub struct DictGetWithNoneDefault { expected: String, @@ -108,15 +109,21 @@ pub(crate) fn use_capital_environment_variables(checker: &mut Checker, expr: &Ex let Some(arg) = args.get(0) else { return; }; - let Expr::Constant(ast::ExprConstant { value: Constant::Str(env_var), .. }) = arg else { + let Expr::Constant(ast::ExprConstant { + value: Constant::Str(env_var), + .. + }) = arg + else { return; }; if !checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["os", "environ", "get"] - || call_path.as_slice() == ["os", "getenv"] + matches!( + call_path.as_slice(), + ["os", "environ", "get"] | ["os", "getenv"] + ) }) { return; @@ -140,7 +147,12 @@ fn check_os_environ_subscript(checker: &mut Checker, expr: &Expr) { let Expr::Subscript(ast::ExprSubscript { value, slice, .. }) = expr else { return; }; - let Expr::Attribute(ast::ExprAttribute { value: attr_value, attr, .. }) = value.as_ref() else { + let Expr::Attribute(ast::ExprAttribute { + value: attr_value, + attr, + .. + }) = value.as_ref() + else { return; }; let Expr::Name(ast::ExprName { id, .. }) = attr_value.as_ref() else { @@ -149,7 +161,12 @@ fn check_os_environ_subscript(checker: &mut Checker, expr: &Expr) { if id != "os" || attr != "environ" { return; } - let Expr::Constant(ast::ExprConstant { value: Constant::Str(env_var), kind, range: _ }) = slice.as_ref() else { + let Expr::Constant(ast::ExprConstant { + value: Constant::Str(env_var), + kind, + range: _, + }) = slice.as_ref() + else { return; }; let capital_env_var = env_var.to_ascii_uppercase(); @@ -171,8 +188,7 @@ fn check_os_environ_subscript(checker: &mut Checker, expr: &Expr) { range: TextRange::default(), }; let new_env_var = node.into(); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( checker.generator().expr(&new_env_var), slice.range(), ))); @@ -182,13 +198,19 @@ fn check_os_environ_subscript(checker: &mut Checker, expr: &Expr) { /// SIM910 pub(crate) fn dict_get_with_none_default(checker: &mut Checker, expr: &Expr) { - let Expr::Call(ast::ExprCall { func, args, keywords, range: _ }) = expr else { + let Expr::Call(ast::ExprCall { + func, + args, + keywords, + range: _, + }) = expr + else { return; }; if !keywords.is_empty() { return; } - let Expr::Attribute(ast::ExprAttribute { value, attr, .. } )= func.as_ref() else { + let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = func.as_ref() else { return; }; if !value.is_dict_expr() { @@ -206,15 +228,9 @@ pub(crate) fn dict_get_with_none_default(checker: &mut Checker, expr: &Expr) { let Some(default) = args.get(1) else { return; }; - if !matches!( - default, - Expr::Constant(ast::ExprConstant { - value: Constant::None, - .. - }) - ) { + if !is_const_none(default) { return; - }; + } let expected = format!( "{}({})", @@ -232,8 +248,7 @@ pub(crate) fn dict_get_with_none_default(checker: &mut Checker, expr: &Expr) { ); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( expected, expr.range(), ))); diff --git a/crates/ruff/src/rules/flake8_simplify/rules/ast_if.rs b/crates/ruff/src/rules/flake8_simplify/rules/ast_if.rs index 431838a350..0accafcc0f 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/ast_if.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/ast_if.rs @@ -1,7 +1,7 @@ use log::error; use ruff_text_size::TextRange; use rustc_hash::FxHashSet; -use rustpython_parser::ast::{self, Cmpop, Constant, Expr, ExprContext, Ranged, Stmt}; +use rustpython_parser::ast::{self, CmpOp, Constant, Expr, ExprContext, Identifier, Ranged, Stmt}; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -9,7 +9,7 @@ use ruff_python_ast::comparable::{ComparableConstant, ComparableExpr, Comparable use ruff_python_ast::helpers::{ any_over_expr, contains_effect, first_colon_range, has_comments, has_comments_in, }; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; use ruff_python_whitespace::UniversalNewlines; use crate::checkers::ast::Checker; @@ -300,7 +300,15 @@ fn is_main_check(expr: &Expr) -> bool { /// ... /// ``` fn find_last_nested_if(body: &[Stmt]) -> Option<(&Expr, &Stmt)> { - let [Stmt::If(ast::StmtIf { test, body: inner_body, orelse, .. })] = body else { return None }; + let [Stmt::If(ast::StmtIf { + test, + body: inner_body, + orelse, + .. + })] = body + else { + return None; + }; if !orelse.is_empty() { return None; } @@ -385,8 +393,7 @@ pub(crate) fn nested_if_statements( <= checker.settings.line_length }) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(edit)); + diagnostic.set_fix(Fix::suggested(edit)); } } Err(err) => error!("Failed to fix nested if: {err}"), @@ -430,10 +437,19 @@ fn is_one_line_return_bool(stmts: &[Stmt]) -> Option { /// SIM103 pub(crate) fn needless_bool(checker: &mut Checker, stmt: &Stmt) { - let Stmt::If(ast::StmtIf { test, body, orelse, range: _ }) = stmt else { + let Stmt::If(ast::StmtIf { + test, + body, + orelse, + range: _, + }) = stmt + else { return; }; - let (Some(if_return), Some(else_return)) = (is_one_line_return_bool(body), is_one_line_return_bool(orelse)) else { + let (Some(if_return), Some(else_return)) = ( + is_one_line_return_bool(body), + is_one_line_return_bool(orelse), + ) else { return; }; @@ -449,7 +465,7 @@ pub(crate) fn needless_bool(checker: &mut Checker, stmt: &Stmt) { if matches!(if_return, Bool::True) && matches!(else_return, Bool::False) && !has_comments(stmt, checker.locator) - && (test.is_compare_expr() || checker.semantic_model().is_builtin("bool")) + && (test.is_compare_expr() || checker.semantic().is_builtin("bool")) { if test.is_compare_expr() { // If the condition is a comparison, we can replace it with the condition. @@ -457,8 +473,7 @@ pub(crate) fn needless_bool(checker: &mut Checker, stmt: &Stmt) { value: Some(test.clone()), range: TextRange::default(), }; - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( checker.generator().stmt(&node.into()), stmt.range(), ))); @@ -480,8 +495,7 @@ pub(crate) fn needless_bool(checker: &mut Checker, stmt: &Stmt) { value: Some(Box::new(node1.into())), range: TextRange::default(), }; - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( checker.generator().stmt(&node2.into()), stmt.range(), ))); @@ -508,9 +522,9 @@ fn ternary(target_var: &Expr, body_value: &Expr, test: &Expr, orelse_value: &Exp } /// Return `true` if the `Expr` contains a reference to `${module}.${target}`. -fn contains_call_path(model: &SemanticModel, expr: &Expr, target: &[&str]) -> bool { +fn contains_call_path(expr: &Expr, target: &[&str], semantic: &SemanticModel) -> bool { any_over_expr(expr, &|expr| { - model + semantic .resolve_call_path(expr) .map_or(false, |call_path| call_path.as_slice() == target) }) @@ -518,25 +532,41 @@ fn contains_call_path(model: &SemanticModel, expr: &Expr, target: &[&str]) -> bo /// SIM108 pub(crate) fn use_ternary_operator(checker: &mut Checker, stmt: &Stmt, parent: Option<&Stmt>) { - let Stmt::If(ast::StmtIf { test, body, orelse, range: _ } )= stmt else { + let Stmt::If(ast::StmtIf { + test, + body, + orelse, + range: _, + }) = stmt + else { return; }; if body.len() != 1 || orelse.len() != 1 { return; } - let Stmt::Assign(ast::StmtAssign { targets: body_targets, value: body_value, .. } )= &body[0] else { + let Stmt::Assign(ast::StmtAssign { + targets: body_targets, + value: body_value, + .. + }) = &body[0] + else { return; }; - let Stmt::Assign(ast::StmtAssign { targets: orelse_targets, value: orelse_value, .. } )= &orelse[0] else { + let Stmt::Assign(ast::StmtAssign { + targets: orelse_targets, + value: orelse_value, + .. + }) = &orelse[0] + else { return; }; if body_targets.len() != 1 || orelse_targets.len() != 1 { return; } - let Expr::Name(ast::ExprName { id: body_id, .. } )= &body_targets[0] else { + let Expr::Name(ast::ExprName { id: body_id, .. }) = &body_targets[0] else { return; }; - let Expr::Name(ast::ExprName { id: orelse_id, .. } )= &orelse_targets[0] else { + let Expr::Name(ast::ExprName { id: orelse_id, .. }) = &orelse_targets[0] else { return; }; if body_id != orelse_id { @@ -544,13 +574,13 @@ pub(crate) fn use_ternary_operator(checker: &mut Checker, stmt: &Stmt, parent: O } // Avoid suggesting ternary for `if sys.version_info >= ...`-style checks. - if contains_call_path(checker.semantic_model(), test, &["sys", "version_info"]) { + if contains_call_path(test, &["sys", "version_info"], checker.semantic()) { return; } // Avoid suggesting ternary for `if sys.platform.startswith("...")`-style // checks. - if contains_call_path(checker.semantic_model(), test, &["sys", "platform"]) { + if contains_call_path(test, &["sys", "platform"], checker.semantic()) { return; } @@ -621,8 +651,7 @@ pub(crate) fn use_ternary_operator(checker: &mut Checker, stmt: &Stmt, parent: O ); if checker.patch(diagnostic.kind.rule()) { if !has_comments(stmt, checker.locator) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( contents, stmt.range(), ))); @@ -642,7 +671,13 @@ fn get_if_body_pairs<'a>( if orelse.len() != 1 { break; } - let Stmt::If(ast::StmtIf { test, body, orelse: orelse_orelse, range: _ }) = &orelse[0] else { + let Stmt::If(ast::StmtIf { + test, + body, + orelse: orelse_orelse, + range: _, + }) = &orelse[0] + else { break; }; pairs.push((test, body)); @@ -653,7 +688,13 @@ fn get_if_body_pairs<'a>( /// SIM114 pub(crate) fn if_with_same_arms(checker: &mut Checker, stmt: &Stmt, parent: Option<&Stmt>) { - let Stmt::If(ast::StmtIf { test, body, orelse, range: _ }) = stmt else { + let Stmt::If(ast::StmtIf { + test, + body, + orelse, + range: _, + }) = stmt + else { return; }; @@ -722,7 +763,8 @@ pub(crate) fn manual_dict_lookup( ops, comparators, range: _, - })= &test else { + }) = &test + else { return; }; let Expr::Name(ast::ExprName { id: target, .. }) = left.as_ref() else { @@ -734,20 +776,23 @@ pub(crate) fn manual_dict_lookup( if orelse.len() != 1 { return; } - if !(ops.len() == 1 && ops[0] == Cmpop::Eq) { + if !(ops.len() == 1 && ops[0] == CmpOp::Eq) { return; } if comparators.len() != 1 { return; } - let Expr::Constant(ast::ExprConstant { value: constant, .. }) = &comparators[0] else { + let Expr::Constant(ast::ExprConstant { + value: constant, .. + }) = &comparators[0] + else { return; }; let Stmt::Return(ast::StmtReturn { value, range: _ }) = &body[0] else { return; }; if value.as_ref().map_or(false, |value| { - contains_effect(value, |id| checker.semantic_model().is_builtin(id)) + contains_effect(value, |id| checker.semantic().is_builtin(id)) }) { return; } @@ -787,7 +832,13 @@ pub(crate) fn manual_dict_lookup( let mut child: Option<&Stmt> = orelse.get(0); while let Some(current) = child.take() { - let Stmt::If(ast::StmtIf { test, body, orelse, range: _ }) = ¤t else { + let Stmt::If(ast::StmtIf { + test, + body, + orelse, + range: _, + }) = ¤t + else { return; }; if body.len() != 1 { @@ -800,27 +851,31 @@ pub(crate) fn manual_dict_lookup( left, ops, comparators, - range: _ - } )= test.as_ref() else { + range: _, + }) = test.as_ref() + else { return; }; let Expr::Name(ast::ExprName { id, .. }) = left.as_ref() else { return; }; - if !(id == target && ops.len() == 1 && ops[0] == Cmpop::Eq) { + if !(id == target && ops.len() == 1 && ops[0] == CmpOp::Eq) { return; } if comparators.len() != 1 { return; } - let Expr::Constant(ast::ExprConstant { value: constant, .. } )= &comparators[0] else { + let Expr::Constant(ast::ExprConstant { + value: constant, .. + }) = &comparators[0] + else { return; }; - let Stmt::Return(ast::StmtReturn { value, range: _ } )= &body[0] else { + let Stmt::Return(ast::StmtReturn { value, range: _ }) = &body[0] else { return; }; if value.as_ref().map_or(false, |value| { - contains_effect(value, |id| checker.semantic_model().is_builtin(id)) + contains_effect(value, |id| checker.semantic().is_builtin(id)) }) { return; }; @@ -863,33 +918,54 @@ pub(crate) fn use_dict_get_with_default( if body.len() != 1 || orelse.len() != 1 { return; } - let Stmt::Assign(ast::StmtAssign { targets: body_var, value: body_value, ..}) = &body[0] else { + let Stmt::Assign(ast::StmtAssign { + targets: body_var, + value: body_value, + .. + }) = &body[0] + else { return; }; if body_var.len() != 1 { return; }; - let Stmt::Assign(ast::StmtAssign { targets: orelse_var, value: orelse_value, .. }) = &orelse[0] else { + let Stmt::Assign(ast::StmtAssign { + targets: orelse_var, + value: orelse_value, + .. + }) = &orelse[0] + else { return; }; if orelse_var.len() != 1 { return; }; - let Expr::Compare(ast::ExprCompare { left: test_key, ops , comparators: test_dict, range: _ }) = &test else { + let Expr::Compare(ast::ExprCompare { + left: test_key, + ops, + comparators: test_dict, + range: _, + }) = &test + else { return; }; if test_dict.len() != 1 { return; } let (expected_var, expected_value, default_var, default_value) = match ops[..] { - [Cmpop::In] => (&body_var[0], body_value, &orelse_var[0], orelse_value), - [Cmpop::NotIn] => (&orelse_var[0], orelse_value, &body_var[0], body_value), + [CmpOp::In] => (&body_var[0], body_value, &orelse_var[0], orelse_value), + [CmpOp::NotIn] => (&orelse_var[0], orelse_value, &body_var[0], body_value), _ => { return; } }; let test_dict = &test_dict[0]; - let Expr::Subscript(ast::ExprSubscript { value: expected_subscript, slice: expected_slice, .. } ) = expected_value.as_ref() else { + let Expr::Subscript(ast::ExprSubscript { + value: expected_subscript, + slice: expected_slice, + .. + }) = expected_value.as_ref() + else { return; }; @@ -903,7 +979,7 @@ pub(crate) fn use_dict_get_with_default( } // Check that the default value is not "complex". - if contains_effect(default_value, |id| checker.semantic_model().is_builtin(id)) { + if contains_effect(default_value, |id| checker.semantic().is_builtin(id)) { return; } @@ -941,7 +1017,7 @@ pub(crate) fn use_dict_get_with_default( let node1 = *test_key.clone(); let node2 = ast::ExprAttribute { value: expected_subscript.clone(), - attr: "get".into(), + attr: Identifier::new("get".to_string(), TextRange::default()), ctx: ExprContext::Load, range: TextRange::default(), }; @@ -978,8 +1054,7 @@ pub(crate) fn use_dict_get_with_default( ); if checker.patch(diagnostic.kind.rule()) { if !has_comments(stmt, checker.locator) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( contents, stmt.range(), ))); diff --git a/crates/ruff/src/rules/flake8_simplify/rules/ast_ifexp.rs b/crates/ruff/src/rules/flake8_simplify/rules/ast_ifexp.rs index d10a58f8ac..d9f15d068e 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/ast_ifexp.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/ast_ifexp.rs @@ -1,5 +1,5 @@ use ruff_text_size::TextRange; -use rustpython_parser::ast::{self, Constant, Expr, ExprContext, Ranged, Unaryop}; +use rustpython_parser::ast::{self, Constant, Expr, ExprContext, Ranged, UnaryOp}; use ruff_diagnostics::{AlwaysAutofixableViolation, AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -141,13 +141,13 @@ pub(crate) fn explicit_true_false_in_ifexpr( body: &Expr, orelse: &Expr, ) { - let Expr::Constant(ast::ExprConstant { value, .. } )= &body else { + let Expr::Constant(ast::ExprConstant { value, .. }) = &body else { return; }; if !matches!(value, Constant::Bool(true)) { return; } - let Expr::Constant(ast::ExprConstant { value, .. } )= &orelse else { + let Expr::Constant(ast::ExprConstant { value, .. }) = &orelse else { return; }; if !matches!(value, Constant::Bool(false)) { @@ -162,12 +162,11 @@ pub(crate) fn explicit_true_false_in_ifexpr( ); if checker.patch(diagnostic.kind.rule()) { if matches!(test, Expr::Compare(_)) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( checker.generator().expr(&test.clone()), expr.range(), ))); - } else if checker.semantic_model().is_builtin("bool") { + } else if checker.semantic().is_builtin("bool") { let node = ast::ExprName { id: "bool".into(), ctx: ExprContext::Load, @@ -179,8 +178,7 @@ pub(crate) fn explicit_true_false_in_ifexpr( keywords: vec![], range: TextRange::default(), }; - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( checker.generator().expr(&node1.into()), expr.range(), ))); @@ -219,12 +217,11 @@ pub(crate) fn explicit_false_true_in_ifexpr( if checker.patch(diagnostic.kind.rule()) { let node = test.clone(); let node1 = ast::ExprUnaryOp { - op: Unaryop::Not, + op: UnaryOp::Not, operand: Box::new(node), range: TextRange::default(), }; - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( checker.generator().expr(&node1.into()), expr.range(), ))); @@ -240,7 +237,12 @@ pub(crate) fn twisted_arms_in_ifexpr( body: &Expr, orelse: &Expr, ) { - let Expr::UnaryOp(ast::ExprUnaryOp { op, operand: test_operand, range: _ } )= &test else { + let Expr::UnaryOp(ast::ExprUnaryOp { + op, + operand: test_operand, + range: _, + }) = &test + else { return; }; if !op.is_not() { @@ -248,10 +250,10 @@ pub(crate) fn twisted_arms_in_ifexpr( } // Check if the test operand and else branch use the same variable. - let Expr::Name(ast::ExprName { id: test_id, .. } )= test_operand.as_ref() else { + let Expr::Name(ast::ExprName { id: test_id, .. }) = test_operand.as_ref() else { return; }; - let Expr::Name(ast::ExprName {id: orelse_id, ..}) = orelse else { + let Expr::Name(ast::ExprName { id: orelse_id, .. }) = orelse else { return; }; if !test_id.eq(orelse_id) { @@ -275,8 +277,7 @@ pub(crate) fn twisted_arms_in_ifexpr( orelse: Box::new(node), range: TextRange::default(), }; - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( checker.generator().expr(&node3.into()), expr.range(), ))); diff --git a/crates/ruff/src/rules/flake8_simplify/rules/ast_unary_op.rs b/crates/ruff/src/rules/flake8_simplify/rules/ast_unary_op.rs index 35e4c620a7..2febbca3a6 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/ast_unary_op.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/ast_unary_op.rs @@ -1,9 +1,9 @@ use ruff_text_size::TextRange; -use rustpython_parser::ast::{self, Cmpop, Expr, ExprContext, Ranged, Stmt, Unaryop}; +use rustpython_parser::ast::{self, CmpOp, Expr, ExprContext, Ranged, Stmt, UnaryOp}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::scope::ScopeKind; +use ruff_python_semantic::ScopeKind; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -119,10 +119,21 @@ impl AlwaysAutofixableViolation for DoubleNegation { } } -const DUNDER_METHODS: &[&str] = &["__eq__", "__ne__", "__lt__", "__le__", "__gt__", "__ge__"]; +fn is_dunder_method(name: &str) -> bool { + matches!( + name, + "__eq__" | "__ne__" | "__lt__" | "__le__" | "__gt__" | "__ge__" + ) +} fn is_exception_check(stmt: &Stmt) -> bool { - let Stmt::If(ast::StmtIf {test: _, body, orelse: _, range: _ })= stmt else { + let Stmt::If(ast::StmtIf { + test: _, + body, + orelse: _, + range: _, + }) = stmt + else { return false; }; if body.len() != 1 { @@ -138,28 +149,34 @@ fn is_exception_check(stmt: &Stmt) -> bool { pub(crate) fn negation_with_equal_op( checker: &mut Checker, expr: &Expr, - op: Unaryop, + op: UnaryOp, operand: &Expr, ) { - if !matches!(op, Unaryop::Not) { + if !matches!(op, UnaryOp::Not) { return; } - let Expr::Compare(ast::ExprCompare { left, ops, comparators, range: _}) = operand else { + let Expr::Compare(ast::ExprCompare { + left, + ops, + comparators, + range: _, + }) = operand + else { return; }; - if !matches!(&ops[..], [Cmpop::Eq]) { + if !matches!(&ops[..], [CmpOp::Eq]) { return; } - if is_exception_check(checker.semantic_model().stmt()) { + if is_exception_check(checker.semantic().stmt()) { return; } // Avoid flagging issues in dunder implementations. if let ScopeKind::Function(ast::StmtFunctionDef { name, .. }) | ScopeKind::AsyncFunction(ast::StmtAsyncFunctionDef { name, .. }) = - &checker.semantic_model().scope().kind + &checker.semantic().scope().kind { - if DUNDER_METHODS.contains(&name.as_str()) { + if is_dunder_method(name) { return; } } @@ -174,12 +191,11 @@ pub(crate) fn negation_with_equal_op( if checker.patch(diagnostic.kind.rule()) { let node = ast::ExprCompare { left: left.clone(), - ops: vec![Cmpop::NotEq], + ops: vec![CmpOp::NotEq], comparators: comparators.clone(), range: TextRange::default(), }; - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( checker.generator().expr(&node.into()), expr.range(), ))); @@ -191,28 +207,34 @@ pub(crate) fn negation_with_equal_op( pub(crate) fn negation_with_not_equal_op( checker: &mut Checker, expr: &Expr, - op: Unaryop, + op: UnaryOp, operand: &Expr, ) { - if !matches!(op, Unaryop::Not) { + if !matches!(op, UnaryOp::Not) { return; } - let Expr::Compare(ast::ExprCompare { left, ops, comparators, range: _}) = operand else { + let Expr::Compare(ast::ExprCompare { + left, + ops, + comparators, + range: _, + }) = operand + else { return; }; - if !matches!(&ops[..], [Cmpop::NotEq]) { + if !matches!(&ops[..], [CmpOp::NotEq]) { return; } - if is_exception_check(checker.semantic_model().stmt()) { + if is_exception_check(checker.semantic().stmt()) { return; } // Avoid flagging issues in dunder implementations. if let ScopeKind::Function(ast::StmtFunctionDef { name, .. }) | ScopeKind::AsyncFunction(ast::StmtAsyncFunctionDef { name, .. }) = - &checker.semantic_model().scope().kind + &checker.semantic().scope().kind { - if DUNDER_METHODS.contains(&name.as_str()) { + if is_dunder_method(name) { return; } } @@ -227,12 +249,11 @@ pub(crate) fn negation_with_not_equal_op( if checker.patch(diagnostic.kind.rule()) { let node = ast::ExprCompare { left: left.clone(), - ops: vec![Cmpop::Eq], + ops: vec![CmpOp::Eq], comparators: comparators.clone(), range: TextRange::default(), }; - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( checker.generator().expr(&node.into()), expr.range(), ))); @@ -241,14 +262,19 @@ pub(crate) fn negation_with_not_equal_op( } /// SIM208 -pub(crate) fn double_negation(checker: &mut Checker, expr: &Expr, op: Unaryop, operand: &Expr) { - if !matches!(op, Unaryop::Not) { +pub(crate) fn double_negation(checker: &mut Checker, expr: &Expr, op: UnaryOp, operand: &Expr) { + if !matches!(op, UnaryOp::Not) { return; } - let Expr::UnaryOp(ast::ExprUnaryOp { op: operand_op, operand, range: _ }) = operand else { + let Expr::UnaryOp(ast::ExprUnaryOp { + op: operand_op, + operand, + range: _, + }) = operand + else { return; }; - if !matches!(operand_op, Unaryop::Not) { + if !matches!(operand_op, UnaryOp::Not) { return; } @@ -259,13 +285,12 @@ pub(crate) fn double_negation(checker: &mut Checker, expr: &Expr, op: Unaryop, o expr.range(), ); if checker.patch(diagnostic.kind.rule()) { - if checker.semantic_model().in_boolean_test() { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + if checker.semantic().in_boolean_test() { + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( checker.generator().expr(operand), expr.range(), ))); - } else if checker.semantic_model().is_builtin("bool") { + } else if checker.semantic().is_builtin("bool") { let node = ast::ExprName { id: "bool".into(), ctx: ExprContext::Load, @@ -277,8 +302,7 @@ pub(crate) fn double_negation(checker: &mut Checker, expr: &Expr, op: Unaryop, o keywords: vec![], range: TextRange::default(), }; - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( checker.generator().expr(&node1.into()), expr.range(), ))); diff --git a/crates/ruff/src/rules/flake8_simplify/rules/ast_with.rs b/crates/ruff/src/rules/flake8_simplify/rules/ast_with.rs index cf88a3ba94..8029e2a97b 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/ast_with.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/ast_with.rs @@ -1,6 +1,6 @@ use log::error; use ruff_text_size::TextRange; -use rustpython_parser::ast::{self, Ranged, Stmt, Withitem}; +use rustpython_parser::ast::{self, Ranged, Stmt, WithItem}; use ruff_diagnostics::{AutofixKind, Violation}; use ruff_diagnostics::{Diagnostic, Fix}; @@ -40,7 +40,7 @@ use super::fix_with; /// ``` /// /// ## References -/// - [Python: "The with statement"](https://docs.python.org/3/reference/compound_stmts.html#the-with-statement) +/// - [Python documentation: The `with` statement](https://docs.python.org/3/reference/compound_stmts.html#the-with-statement) #[violation] pub struct MultipleWithStatements; @@ -60,9 +60,14 @@ impl Violation for MultipleWithStatements { } } -fn find_last_with(body: &[Stmt]) -> Option<(&[Withitem], &[Stmt])> { - let [Stmt::With(ast::StmtWith { items, body, .. })] = body else { return None }; - find_last_with(body).or(Some((items, body))) +/// Returns a boolean indicating whether it's an async with statement, the items +/// and body. +fn next_with(body: &[Stmt]) -> Option<(bool, &[WithItem], &[Stmt])> { + match body { + [Stmt::With(ast::StmtWith { items, body, .. })] => Some((false, items, body)), + [Stmt::AsyncWith(ast::StmtAsyncWith { items, body, .. })] => Some((true, items, body)), + _ => None, + } } /// SIM117 @@ -72,12 +77,38 @@ pub(crate) fn multiple_with_statements( with_body: &[Stmt], with_parent: Option<&Stmt>, ) { + // Make sure we fix from top to bottom for nested with statements, e.g. for + // ```python + // with A(): + // with B(): + // with C(): + // print("hello") + // ``` + // suggests + // ```python + // with A(), B(): + // with C(): + // print("hello") + // ``` + // but not the following + // ```python + // with A(): + // with B(), C(): + // print("hello") + // ``` if let Some(Stmt::With(ast::StmtWith { body, .. })) = with_parent { if body.len() == 1 { return; } } - if let Some((items, body)) = find_last_with(with_body) { + + if let Some((is_async, items, body)) = next_with(with_body) { + if is_async != with_stmt.is_async_with_stmt() { + // One of the statements is an async with, while the other is not, + // we can't merge those statements. + return; + } + let last_item = items.last().expect("Expected items to be non-empty"); let colon = first_colon_range( TextRange::new( @@ -117,8 +148,7 @@ pub(crate) fn multiple_with_statements( <= checker.settings.line_length }) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(edit)); + diagnostic.set_fix(Fix::suggested(edit)); } } Err(err) => error!("Failed to fix nested with: {err}"), diff --git a/crates/ruff/src/rules/flake8_simplify/rules/fix_if.rs b/crates/ruff/src/rules/flake8_simplify/rules/fix_if.rs index 0988aaf3b3..3199c6c509 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/fix_if.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/fix_if.rs @@ -90,7 +90,8 @@ pub(crate) fn fix_nested_if_statements( body: Suite::IndentedBlock(ref mut outer_body), orelse: None, .. - } = outer_if else { + } = outer_if + else { bail!("Expected outer if to have indented body and no else") }; diff --git a/crates/ruff/src/rules/flake8_simplify/rules/fix_with.rs b/crates/ruff/src/rules/flake8_simplify/rules/fix_with.rs index b3636cabbc..649496bb8b 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/fix_with.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/fix_with.rs @@ -54,13 +54,12 @@ pub(crate) fn fix_multiple_with_statements( let With { body: Suite::IndentedBlock(ref mut outer_body), .. - } = outer_with else { + } = outer_with + else { bail!("Expected outer with to have indented body") }; - let [Statement::Compound(CompoundStatement::With(inner_with))] = - &mut *outer_body.body - else { + let [Statement::Compound(CompoundStatement::With(inner_with))] = &mut *outer_body.body else { bail!("Expected one inner with statement"); }; diff --git a/crates/ruff/src/rules/flake8_simplify/rules/key_in_dict.rs b/crates/ruff/src/rules/flake8_simplify/rules/key_in_dict.rs index c8c8f4021a..f4738c06bc 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/key_in_dict.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/key_in_dict.rs @@ -1,7 +1,7 @@ use anyhow::Result; use log::error; use ruff_text_size::TextRange; -use rustpython_parser::ast::{self, Cmpop, Expr, Ranged}; +use rustpython_parser::ast::{self, CmpOp, Expr, Ranged}; use ruff_diagnostics::Edit; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix}; @@ -71,8 +71,9 @@ fn key_in_dict(checker: &mut Checker, left: &Expr, right: &Expr, range: TextRang func, args, keywords, - range: _ - }) = &right else { + range: _, + }) = &right + else { return; }; if !(args.is_empty() && keywords.is_empty()) { @@ -128,10 +129,10 @@ pub(crate) fn key_in_dict_compare( checker: &mut Checker, expr: &Expr, left: &Expr, - ops: &[Cmpop], + ops: &[CmpOp], comparators: &[Expr], ) { - if !matches!(ops[..], [Cmpop::In]) { + if !matches!(ops[..], [CmpOp::In]) { return; } diff --git a/crates/ruff/src/rules/flake8_simplify/rules/mod.rs b/crates/ruff/src/rules/flake8_simplify/rules/mod.rs index 80a9ad752d..0e4b3b4937 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/mod.rs @@ -1,36 +1,15 @@ -pub(crate) use ast_bool_op::{ - compare_with_tuple, duplicate_isinstance_call, expr_and_false, expr_and_not_expr, - expr_or_not_expr, expr_or_true, CompareWithTuple, DuplicateIsinstanceCall, ExprAndFalse, - ExprAndNotExpr, ExprOrNotExpr, ExprOrTrue, -}; -pub(crate) use ast_expr::{ - dict_get_with_none_default, use_capital_environment_variables, DictGetWithNoneDefault, - UncapitalizedEnvironmentVariables, -}; -pub(crate) use ast_if::{ - if_with_same_arms, manual_dict_lookup, needless_bool, nested_if_statements, - use_dict_get_with_default, use_ternary_operator, CollapsibleIf, IfElseBlockInsteadOfDictGet, - IfElseBlockInsteadOfDictLookup, IfElseBlockInsteadOfIfExp, IfWithSameArms, NeedlessBool, -}; -pub(crate) use ast_ifexp::{ - explicit_false_true_in_ifexpr, explicit_true_false_in_ifexpr, twisted_arms_in_ifexpr, - IfExprWithFalseTrue, IfExprWithTrueFalse, IfExprWithTwistedArms, -}; -pub(crate) use ast_unary_op::{ - double_negation, negation_with_equal_op, negation_with_not_equal_op, DoubleNegation, - NegateEqualOp, NegateNotEqualOp, -}; -pub(crate) use ast_with::{multiple_with_statements, MultipleWithStatements}; -pub(crate) use key_in_dict::{key_in_dict_compare, key_in_dict_for, InDictKeys}; -pub(crate) use open_file_with_context_handler::{ - open_file_with_context_handler, OpenFileWithContextHandler, -}; -pub(crate) use reimplemented_builtin::{convert_for_loop_to_any_all, ReimplementedBuiltin}; -pub(crate) use return_in_try_except_finally::{ - return_in_try_except_finally, ReturnInTryExceptFinally, -}; -pub(crate) use suppressible_exception::{suppressible_exception, SuppressibleException}; -pub(crate) use yoda_conditions::{yoda_conditions, YodaConditions}; +pub(crate) use ast_bool_op::*; +pub(crate) use ast_expr::*; +pub(crate) use ast_if::*; +pub(crate) use ast_ifexp::*; +pub(crate) use ast_unary_op::*; +pub(crate) use ast_with::*; +pub(crate) use key_in_dict::*; +pub(crate) use open_file_with_context_handler::*; +pub(crate) use reimplemented_builtin::*; +pub(crate) use return_in_try_except_finally::*; +pub(crate) use suppressible_exception::*; +pub(crate) use yoda_conditions::*; mod ast_bool_op; mod ast_expr; diff --git a/crates/ruff/src/rules/flake8_simplify/rules/open_file_with_context_handler.rs b/crates/ruff/src/rules/flake8_simplify/rules/open_file_with_context_handler.rs index 8d10ddb424..41da294234 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/open_file_with_context_handler.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/open_file_with_context_handler.rs @@ -2,12 +2,12 @@ use rustpython_parser::ast::{self, Expr, Ranged, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; use crate::checkers::ast::Checker; /// ## What it does -/// Checks for usages of the builtin `open()` function without an associated context +/// Checks for uses of the builtin `open()` function without an associated context /// manager. /// /// ## Why is this bad? @@ -29,7 +29,7 @@ use crate::checkers::ast::Checker; /// ``` /// /// # References -/// - [Python documentation](https://docs.python.org/3/library/functions.html#open) +/// - [Python documentation: `open`](https://docs.python.org/3/library/functions.html#open) #[violation] pub struct OpenFileWithContextHandler; @@ -42,28 +42,28 @@ impl Violation for OpenFileWithContextHandler { /// Return `true` if the current expression is nested in an `await /// exit_stack.enter_async_context` call. -fn match_async_exit_stack(model: &SemanticModel) -> bool { - let Some(expr) = model.expr_grandparent() else { +fn match_async_exit_stack(semantic: &SemanticModel) -> bool { + let Some(expr) = semantic.expr_grandparent() else { return false; }; let Expr::Await(ast::ExprAwait { value, range: _ }) = expr else { return false; }; - let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() else { - return false; - }; + let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() else { + return false; + }; let Expr::Attribute(ast::ExprAttribute { attr, .. }) = func.as_ref() else { return false; }; if attr != "enter_async_context" { return false; } - for parent in model.parents() { + for parent in semantic.parents() { if let Stmt::With(ast::StmtWith { items, .. }) = parent { for item in items { if let Expr::Call(ast::ExprCall { func, .. }) = &item.context_expr { - if model.resolve_call_path(func).map_or(false, |call_path| { - call_path.as_slice() == ["contextlib", "AsyncExitStack"] + if semantic.resolve_call_path(func).map_or(false, |call_path| { + matches!(call_path.as_slice(), ["contextlib", "AsyncExitStack"]) }) { return true; } @@ -76,25 +76,25 @@ fn match_async_exit_stack(model: &SemanticModel) -> bool { /// Return `true` if the current expression is nested in an /// `exit_stack.enter_context` call. -fn match_exit_stack(model: &SemanticModel) -> bool { - let Some(expr) = model.expr_parent() else { +fn match_exit_stack(semantic: &SemanticModel) -> bool { + let Some(expr) = semantic.expr_parent() else { + return false; + }; + let Expr::Call(ast::ExprCall { func, .. }) = expr else { return false; }; - let Expr::Call(ast::ExprCall { func, .. }) = expr else { - return false; - }; let Expr::Attribute(ast::ExprAttribute { attr, .. }) = func.as_ref() else { return false; }; if attr != "enter_context" { return false; } - for parent in model.parents() { + for parent in semantic.parents() { if let Stmt::With(ast::StmtWith { items, .. }) = parent { for item in items { if let Expr::Call(ast::ExprCall { func, .. }) = &item.context_expr { - if model.resolve_call_path(func).map_or(false, |call_path| { - call_path.as_slice() == ["contextlib", "ExitStack"] + if semantic.resolve_call_path(func).map_or(false, |call_path| { + matches!(call_path.as_slice(), ["contextlib", "ExitStack"]) }) { return true; } @@ -107,30 +107,34 @@ fn match_exit_stack(model: &SemanticModel) -> bool { /// SIM115 pub(crate) fn open_file_with_context_handler(checker: &mut Checker, func: &Expr) { - if checker - .semantic_model() - .resolve_call_path(func) - .map_or(false, |call_path| call_path.as_slice() == ["", "open"]) - { - if checker.semantic_model().is_builtin("open") { - // Ex) `with open("foo.txt") as f: ...` - if matches!(checker.semantic_model().stmt(), Stmt::With(_)) { - return; - } + let Expr::Name(ast::ExprName { id, .. }) = func else { + return; + }; - // Ex) `with contextlib.ExitStack() as exit_stack: ...` - if match_exit_stack(checker.semantic_model()) { - return; - } - - // Ex) `with contextlib.AsyncExitStack() as exit_stack: ...` - if match_async_exit_stack(checker.semantic_model()) { - return; - } - - checker - .diagnostics - .push(Diagnostic::new(OpenFileWithContextHandler, func.range())); - } + if id.as_str() != "open" { + return; } + + // Ex) `with open("foo.txt") as f: ...` + if checker.semantic().stmt().is_with_stmt() { + return; + } + + if !checker.semantic().is_builtin("open") { + return; + } + + // Ex) `with contextlib.ExitStack() as exit_stack: ...` + if match_exit_stack(checker.semantic()) { + return; + } + + // Ex) `with contextlib.AsyncExitStack() as exit_stack: ...` + if match_async_exit_stack(checker.semantic()) { + return; + } + + checker + .diagnostics + .push(Diagnostic::new(OpenFileWithContextHandler, func.range())); } diff --git a/crates/ruff/src/rules/flake8_simplify/rules/reimplemented_builtin.rs b/crates/ruff/src/rules/flake8_simplify/rules/reimplemented_builtin.rs index 53e3b59718..f0e757b111 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/reimplemented_builtin.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/reimplemented_builtin.rs @@ -1,10 +1,11 @@ use ruff_text_size::{TextRange, TextSize}; use rustpython_parser::ast::{ - self, Cmpop, Comprehension, Constant, Expr, ExprContext, Ranged, Stmt, Unaryop, + self, CmpOp, Comprehension, Constant, Expr, ExprContext, Ranged, Stmt, UnaryOp, }; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers::any_over_expr; use ruff_python_ast::source_code::Generator; use crate::checkers::ast::Checker; @@ -55,6 +56,154 @@ impl Violation for ReimplementedBuiltin { } } +/// SIM110, SIM111 +pub(crate) fn convert_for_loop_to_any_all( + checker: &mut Checker, + stmt: &Stmt, + sibling: Option<&Stmt>, +) { + // There are two cases to consider: + // - `for` loop with an `else: return True` or `else: return False`. + // - `for` loop followed by `return True` or `return False` + if let Some(loop_info) = return_values_for_else(stmt) + .or_else(|| sibling.and_then(|sibling| return_values_for_siblings(stmt, sibling))) + { + // Check if loop_info.target, loop_info.iter, or loop_info.test contains `await`. + if contains_await(loop_info.target) + || contains_await(loop_info.iter) + || contains_await(loop_info.test) + { + return; + } + if loop_info.return_value && !loop_info.next_return_value { + if checker.enabled(Rule::ReimplementedBuiltin) { + let contents = return_stmt( + "any", + loop_info.test, + loop_info.target, + loop_info.iter, + checker.generator(), + ); + + // Don't flag if the resulting expression would exceed the maximum line length. + let line_start = checker.locator.line_start(stmt.start()); + if LineWidth::new(checker.settings.tab_size) + .add_str(&checker.locator.contents()[TextRange::new(line_start, stmt.start())]) + .add_str(&contents) + > checker.settings.line_length + { + return; + } + + let mut diagnostic = Diagnostic::new( + ReimplementedBuiltin { + repl: contents.clone(), + }, + TextRange::new(stmt.start(), loop_info.terminal), + ); + if checker.patch(diagnostic.kind.rule()) && checker.semantic().is_builtin("any") { + diagnostic.set_fix(Fix::suggested(Edit::replacement( + contents, + stmt.start(), + loop_info.terminal, + ))); + } + checker.diagnostics.push(diagnostic); + } + } + + if !loop_info.return_value && loop_info.next_return_value { + if checker.enabled(Rule::ReimplementedBuiltin) { + // Invert the condition. + let test = { + if let Expr::UnaryOp(ast::ExprUnaryOp { + op: UnaryOp::Not, + operand, + range: _, + }) = &loop_info.test + { + *operand.clone() + } else if let Expr::Compare(ast::ExprCompare { + left, + ops, + comparators, + range: _, + }) = &loop_info.test + { + if let ([op], [comparator]) = (ops.as_slice(), comparators.as_slice()) { + let op = match op { + CmpOp::Eq => CmpOp::NotEq, + CmpOp::NotEq => CmpOp::Eq, + CmpOp::Lt => CmpOp::GtE, + CmpOp::LtE => CmpOp::Gt, + CmpOp::Gt => CmpOp::LtE, + CmpOp::GtE => CmpOp::Lt, + CmpOp::Is => CmpOp::IsNot, + CmpOp::IsNot => CmpOp::Is, + CmpOp::In => CmpOp::NotIn, + CmpOp::NotIn => CmpOp::In, + }; + let node = ast::ExprCompare { + left: left.clone(), + ops: vec![op], + comparators: vec![comparator.clone()], + range: TextRange::default(), + }; + node.into() + } else { + let node = ast::ExprUnaryOp { + op: UnaryOp::Not, + operand: Box::new(loop_info.test.clone()), + range: TextRange::default(), + }; + node.into() + } + } else { + let node = ast::ExprUnaryOp { + op: UnaryOp::Not, + operand: Box::new(loop_info.test.clone()), + range: TextRange::default(), + }; + node.into() + } + }; + let contents = return_stmt( + "all", + &test, + loop_info.target, + loop_info.iter, + checker.generator(), + ); + + // Don't flag if the resulting expression would exceed the maximum line length. + let line_start = checker.locator.line_start(stmt.start()); + if LineWidth::new(checker.settings.tab_size) + .add_str(&checker.locator.contents()[TextRange::new(line_start, stmt.start())]) + .add_str(&contents) + > checker.settings.line_length + { + return; + } + + let mut diagnostic = Diagnostic::new( + ReimplementedBuiltin { + repl: contents.clone(), + }, + TextRange::new(stmt.start(), loop_info.terminal), + ); + if checker.patch(diagnostic.kind.rule()) && checker.semantic().is_builtin("all") { + diagnostic.set_fix(Fix::suggested(Edit::replacement( + contents, + stmt.start(), + loop_info.terminal, + ))); + } + checker.diagnostics.push(diagnostic); + } + } + } +} + struct Loop<'a> { return_value: bool, next_return_value: bool, @@ -72,7 +221,8 @@ fn return_values_for_else(stmt: &Stmt) -> Option { iter, orelse, .. - }) = stmt else { + }) = stmt + else { return None; }; @@ -87,8 +237,10 @@ fn return_values_for_else(stmt: &Stmt) -> Option { let Stmt::If(ast::StmtIf { body: nested_body, test: nested_test, - orelse: nested_orelse, range: _, - }) = &body[0] else { + orelse: nested_orelse, + range: _, + }) = &body[0] + else { return None; }; if nested_body.len() != 1 { @@ -103,18 +255,30 @@ fn return_values_for_else(stmt: &Stmt) -> Option { let Some(value) = value else { return None; }; - let Expr::Constant(ast::ExprConstant { value: Constant::Bool(value), .. }) = value.as_ref() else { + let Expr::Constant(ast::ExprConstant { + value: Constant::Bool(value), + .. + }) = value.as_ref() + else { return None; }; // The `else` block has to contain a single `return True` or `return False`. - let Stmt::Return(ast::StmtReturn { value: next_value, range: _ }) = &orelse[0] else { + let Stmt::Return(ast::StmtReturn { + value: next_value, + range: _, + }) = &orelse[0] + else { return None; }; let Some(next_value) = next_value else { return None; }; - let Expr::Constant(ast::ExprConstant { value: Constant::Bool(next_value), .. }) = next_value.as_ref() else { + let Expr::Constant(ast::ExprConstant { + value: Constant::Bool(next_value), + .. + }) = next_value.as_ref() + else { return None; }; @@ -137,7 +301,8 @@ fn return_values_for_siblings<'a>(stmt: &'a Stmt, sibling: &'a Stmt) -> Option(stmt: &'a Stmt, sibling: &'a Stmt) -> Option(stmt: &'a Stmt, sibling: &'a Stmt) -> Option, -) { - // There are two cases to consider: - // - `for` loop with an `else: return True` or `else: return False`. - // - `for` loop followed by `return True` or `return False` - if let Some(loop_info) = return_values_for_else(stmt) - .or_else(|| sibling.and_then(|sibling| return_values_for_siblings(stmt, sibling))) - { - if loop_info.return_value && !loop_info.next_return_value { - if checker.enabled(Rule::ReimplementedBuiltin) { - let contents = return_stmt( - "any", - loop_info.test, - loop_info.target, - loop_info.iter, - checker.generator(), - ); - - // Don't flag if the resulting expression would exceed the maximum line length. - let line_start = checker.locator.line_start(stmt.start()); - if LineWidth::new(checker.settings.tab_size) - .add_str(&checker.locator.contents()[TextRange::new(line_start, stmt.start())]) - .add_str(&contents) - > checker.settings.line_length - { - return; - } - - let mut diagnostic = Diagnostic::new( - ReimplementedBuiltin { - repl: contents.clone(), - }, - TextRange::new(stmt.start(), loop_info.terminal), - ); - if checker.patch(diagnostic.kind.rule()) - && checker.semantic_model().is_builtin("any") - { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::replacement( - contents, - stmt.start(), - loop_info.terminal, - ))); - } - checker.diagnostics.push(diagnostic); - } - } - - if !loop_info.return_value && loop_info.next_return_value { - if checker.enabled(Rule::ReimplementedBuiltin) { - // Invert the condition. - let test = { - if let Expr::UnaryOp(ast::ExprUnaryOp { - op: Unaryop::Not, - operand, - range: _, - }) = &loop_info.test - { - *operand.clone() - } else if let Expr::Compare(ast::ExprCompare { - left, - ops, - comparators, - range: _, - }) = &loop_info.test - { - if let ([op], [comparator]) = (ops.as_slice(), comparators.as_slice()) { - let op = match op { - Cmpop::Eq => Cmpop::NotEq, - Cmpop::NotEq => Cmpop::Eq, - Cmpop::Lt => Cmpop::GtE, - Cmpop::LtE => Cmpop::Gt, - Cmpop::Gt => Cmpop::LtE, - Cmpop::GtE => Cmpop::Lt, - Cmpop::Is => Cmpop::IsNot, - Cmpop::IsNot => Cmpop::Is, - Cmpop::In => Cmpop::NotIn, - Cmpop::NotIn => Cmpop::In, - }; - let node = ast::ExprCompare { - left: left.clone(), - ops: vec![op], - comparators: vec![comparator.clone()], - range: TextRange::default(), - }; - node.into() - } else { - let node = ast::ExprUnaryOp { - op: Unaryop::Not, - operand: Box::new(loop_info.test.clone()), - range: TextRange::default(), - }; - node.into() - } - } else { - let node = ast::ExprUnaryOp { - op: Unaryop::Not, - operand: Box::new(loop_info.test.clone()), - range: TextRange::default(), - }; - node.into() - } - }; - let contents = return_stmt( - "all", - &test, - loop_info.target, - loop_info.iter, - checker.generator(), - ); - - // Don't flag if the resulting expression would exceed the maximum line length. - let line_start = checker.locator.line_start(stmt.start()); - if LineWidth::new(checker.settings.tab_size) - .add_str(&checker.locator.contents()[TextRange::new(line_start, stmt.start())]) - .add_str(&contents) - > checker.settings.line_length - { - return; - } - - let mut diagnostic = Diagnostic::new( - ReimplementedBuiltin { - repl: contents.clone(), - }, - TextRange::new(stmt.start(), loop_info.terminal), - ); - if checker.patch(diagnostic.kind.rule()) - && checker.semantic_model().is_builtin("all") - { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::replacement( - contents, - stmt.start(), - loop_info.terminal, - ))); - } - checker.diagnostics.push(diagnostic); - } - } - } +/// Return `true` if the [`Expr`] contains an `await` expression. +fn contains_await(expr: &Expr) -> bool { + any_over_expr(expr, &Expr::is_await_expr) } diff --git a/crates/ruff/src/rules/flake8_simplify/rules/return_in_try_except_finally.rs b/crates/ruff/src/rules/flake8_simplify/rules/return_in_try_except_finally.rs index f966d4ab8d..ce6872778f 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/return_in_try_except_finally.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/return_in_try_except_finally.rs @@ -1,4 +1,4 @@ -use rustpython_parser::ast::{self, Excepthandler, Ranged, Stmt}; +use rustpython_parser::ast::{self, ExceptHandler, Ranged, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -38,7 +38,7 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/tutorial/errors.html#defining-clean-up-actions) +/// - [Python documentation: Defining Clean-up Actions](https://docs.python.org/3/tutorial/errors.html#defining-clean-up-actions) #[violation] pub struct ReturnInTryExceptFinally; @@ -57,12 +57,12 @@ fn find_return(stmts: &[Stmt]) -> Option<&Stmt> { pub(crate) fn return_in_try_except_finally( checker: &mut Checker, body: &[Stmt], - handlers: &[Excepthandler], + handlers: &[ExceptHandler], finalbody: &[Stmt], ) { let try_has_return = find_return(body).is_some(); let except_has_return = handlers.iter().any(|handler| { - let Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { body, .. }) = handler; + let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { body, .. }) = handler; find_return(body).is_some() }); diff --git a/crates/ruff/src/rules/flake8_simplify/rules/suppressible_exception.rs b/crates/ruff/src/rules/flake8_simplify/rules/suppressible_exception.rs index 869a1298c7..a025d17601 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/suppressible_exception.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/suppressible_exception.rs @@ -1,5 +1,5 @@ use ruff_text_size::{TextLen, TextRange}; -use rustpython_parser::ast::{self, Constant, Excepthandler, Expr, Ranged, Stmt}; +use rustpython_parser::ast::{self, Constant, ExceptHandler, Expr, Ranged, Stmt}; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -68,7 +68,7 @@ pub(crate) fn suppressible_exception( checker: &mut Checker, stmt: &Stmt, try_body: &[Stmt], - handlers: &[Excepthandler], + handlers: &[ExceptHandler], orelse: &[Stmt], finalbody: &[Stmt], ) { @@ -90,7 +90,7 @@ pub(crate) fn suppressible_exception( return; } let handler = &handlers[0]; - let Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { body, .. }) = handler; + let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { body, .. }) = handler; if body.len() == 1 { let node = &body[0]; if node.is_pass_stmt() @@ -122,7 +122,7 @@ pub(crate) fn suppressible_exception( let (import_edit, binding) = checker.importer.get_or_import_symbol( &ImportRequest::import("contextlib", "suppress"), stmt.start(), - checker.semantic_model(), + checker.semantic(), )?; let replace_try = Edit::range_replacement( format!("with {binding}({exception})"), @@ -130,8 +130,7 @@ pub(crate) fn suppressible_exception( ); let handler_line_begin = checker.locator.line_start(handler.start()); let remove_handler = Edit::deletion(handler_line_begin, handler.end()); - #[allow(deprecated)] - Ok(Fix::unspecified_edits( + Ok(Fix::suggested_edits( import_edit, [replace_try, remove_handler], )) diff --git a/crates/ruff/src/rules/flake8_simplify/rules/yoda_conditions.rs b/crates/ruff/src/rules/flake8_simplify/rules/yoda_conditions.rs index cc488b5276..cadf0af99d 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/yoda_conditions.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/yoda_conditions.rs @@ -1,6 +1,6 @@ use anyhow::Result; use libcst_native::CompOp; -use rustpython_parser::ast::{self, Cmpop, Expr, Ranged, Unaryop}; +use rustpython_parser::ast::{self, CmpOp, Expr, Ranged, UnaryOp}; use crate::autofix::codemods::CodegenStylist; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; @@ -71,12 +71,12 @@ impl Violation for YodaConditions { /// Return `true` if an [`Expr`] is a constant or a constant-like name. fn is_constant_like(expr: &Expr) -> bool { match expr { - Expr::Attribute(ast::ExprAttribute { attr, .. }) => str::is_upper(attr), + Expr::Attribute(ast::ExprAttribute { attr, .. }) => str::is_cased_uppercase(attr), Expr::Constant(_) => true, Expr::Tuple(ast::ExprTuple { elts, .. }) => elts.iter().all(is_constant_like), - Expr::Name(ast::ExprName { id, .. }) => str::is_upper(id), + Expr::Name(ast::ExprName { id, .. }) => str::is_cased_uppercase(id), Expr::UnaryOp(ast::ExprUnaryOp { - op: Unaryop::UAdd | Unaryop::USub | Unaryop::Invert, + op: UnaryOp::UAdd | UnaryOp::USub | UnaryOp::Invert, operand, range: _, }) => operand.is_constant_expr(), @@ -156,7 +156,7 @@ pub(crate) fn yoda_conditions( checker: &mut Checker, expr: &Expr, left: &Expr, - ops: &[Cmpop], + ops: &[CmpOp], comparators: &[Expr], ) { let ([op], [right]) = (ops, comparators) else { @@ -165,7 +165,7 @@ pub(crate) fn yoda_conditions( if !matches!( op, - Cmpop::Eq | Cmpop::NotEq | Cmpop::Lt | Cmpop::LtE | Cmpop::Gt | Cmpop::GtE, + CmpOp::Eq | CmpOp::NotEq | CmpOp::Lt | CmpOp::LtE | CmpOp::Gt | CmpOp::GtE, ) { return; } @@ -182,8 +182,7 @@ pub(crate) fn yoda_conditions( expr.range(), ); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( suggestion, expr.range(), ))); diff --git a/crates/ruff/src/rules/flake8_simplify/snapshots/ruff__rules__flake8_simplify__tests__SIM110_SIM110.py.snap b/crates/ruff/src/rules/flake8_simplify/snapshots/ruff__rules__flake8_simplify__tests__SIM110_SIM110.py.snap index 7c56013102..1a815035bd 100644 --- a/crates/ruff/src/rules/flake8_simplify/snapshots/ruff__rules__flake8_simplify__tests__SIM110_SIM110.py.snap +++ b/crates/ruff/src/rules/flake8_simplify/snapshots/ruff__rules__flake8_simplify__tests__SIM110_SIM110.py.snap @@ -294,4 +294,27 @@ SIM110.py:162:5: SIM110 [*] Use `return any(x.isdigit() for x in "012ß9💣2ℝ 167 164 | 168 165 | def f(): +SIM110.py:184:5: SIM110 [*] Use `return any(check(x) for x in iterable)` instead of `for` loop + | +182 | async def f(): +183 | # SIM110 +184 | for x in iterable: + | _____^ +185 | | if check(x): +186 | | return True +187 | | return False + | |________________^ SIM110 + | + = help: Replace with `return any(check(x) for x in iterable)` + +ℹ Suggested fix +181 181 | +182 182 | async def f(): +183 183 | # SIM110 +184 |- for x in iterable: +185 |- if check(x): +186 |- return True +187 |- return False + 184 |+ return any(check(x) for x in iterable) + diff --git a/crates/ruff/src/rules/flake8_simplify/snapshots/ruff__rules__flake8_simplify__tests__SIM117_SIM117.py.snap b/crates/ruff/src/rules/flake8_simplify/snapshots/ruff__rules__flake8_simplify__tests__SIM117_SIM117.py.snap index c3cf611f84..ceb934c7e9 100644 --- a/crates/ruff/src/rules/flake8_simplify/snapshots/ruff__rules__flake8_simplify__tests__SIM117_SIM117.py.snap +++ b/crates/ruff/src/rules/flake8_simplify/snapshots/ruff__rules__flake8_simplify__tests__SIM117_SIM117.py.snap @@ -27,8 +27,8 @@ SIM117.py:7:1: SIM117 [*] Use a single `with` statement with multiple contexts i 6 | # SIM117 7 | / with A(): 8 | | with B(): - 9 | | with C(): - | |_________________^ SIM117 + | |_____________^ SIM117 + 9 | with C(): 10 | print("hello") | = help: Combine `with` statements @@ -85,6 +85,29 @@ SIM117.py:19:1: SIM117 [*] Use a single `with` statement with multiple contexts 24 23 | # OK 25 24 | with A() as a: +SIM117.py:47:1: SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements + | +46 | # SIM117 +47 | / async with A() as a: +48 | | async with B() as b: + | |________________________^ SIM117 +49 | print("hello") + | + = help: Combine `with` statements + +ℹ Suggested fix +44 44 | print("hello") +45 45 | +46 46 | # SIM117 +47 |-async with A() as a: +48 |- async with B() as b: +49 |- print("hello") + 47 |+async with A() as a, B() as b: + 48 |+ print("hello") +50 49 | +51 50 | while True: +52 51 | # SIM117 + SIM117.py:53:5: SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements | 51 | while True: @@ -249,4 +272,16 @@ SIM117.py:100:1: SIM117 Use a single `with` statement with multiple contexts ins | = help: Combine `with` statements +SIM117.py:106:5: SIM117 Use a single `with` statement with multiple contexts instead of nested `with` statements + | +104 | # From issue #3025. +105 | async def main(): +106 | async with A() as a: # SIM117. + | _____^ +107 | | async with B() as b: + | |____________________________^ SIM117 +108 | print("async-inside!") + | + = help: Combine `with` statements + diff --git a/crates/ruff/src/rules/flake8_simplify/snapshots/ruff__rules__flake8_simplify__tests__SIM201_SIM201.py.snap b/crates/ruff/src/rules/flake8_simplify/snapshots/ruff__rules__flake8_simplify__tests__SIM201_SIM201.py.snap index f7a55e6697..cff98673a5 100644 --- a/crates/ruff/src/rules/flake8_simplify/snapshots/ruff__rules__flake8_simplify__tests__SIM201_SIM201.py.snap +++ b/crates/ruff/src/rules/flake8_simplify/snapshots/ruff__rules__flake8_simplify__tests__SIM201_SIM201.py.snap @@ -10,7 +10,7 @@ SIM201.py:2:4: SIM201 [*] Use `a != b` instead of `not a == b` | = help: Replace with `!=` operator -ℹ Suggested fix +ℹ Fix 1 1 | # SIM201 2 |-if not a == b: 2 |+if a != b: @@ -27,7 +27,7 @@ SIM201.py:6:4: SIM201 [*] Use `a != b + c` instead of `not a == b + c` | = help: Replace with `!=` operator -ℹ Suggested fix +ℹ Fix 3 3 | pass 4 4 | 5 5 | # SIM201 @@ -46,7 +46,7 @@ SIM201.py:10:4: SIM201 [*] Use `a + b != c` instead of `not a + b == c` | = help: Replace with `!=` operator -ℹ Suggested fix +ℹ Fix 7 7 | pass 8 8 | 9 9 | # SIM201 diff --git a/crates/ruff/src/rules/flake8_simplify/snapshots/ruff__rules__flake8_simplify__tests__SIM300_SIM300.py.snap b/crates/ruff/src/rules/flake8_simplify/snapshots/ruff__rules__flake8_simplify__tests__SIM300_SIM300.py.snap index 1b199b678d..38307e22c2 100644 --- a/crates/ruff/src/rules/flake8_simplify/snapshots/ruff__rules__flake8_simplify__tests__SIM300_SIM300.py.snap +++ b/crates/ruff/src/rules/flake8_simplify/snapshots/ruff__rules__flake8_simplify__tests__SIM300_SIM300.py.snap @@ -11,7 +11,7 @@ SIM300.py:2:1: SIM300 [*] Yoda conditions are discouraged, use `compare == "yoda | = help: Replace Yoda condition with `compare == "yoda"` -ℹ Suggested fix +ℹ Fix 1 1 | # Errors 2 |-"yoda" == compare # SIM300 2 |+compare == "yoda" # SIM300 @@ -30,7 +30,7 @@ SIM300.py:3:1: SIM300 [*] Yoda conditions are discouraged, use `compare == "yoda | = help: Replace Yoda condition with `compare == "yoda"` -ℹ Suggested fix +ℹ Fix 1 1 | # Errors 2 2 | "yoda" == compare # SIM300 3 |-"yoda" == compare # SIM300 @@ -50,7 +50,7 @@ SIM300.py:4:1: SIM300 [*] Yoda conditions are discouraged, use `age == 42` inste | = help: Replace Yoda condition with `age == 42` -ℹ Suggested fix +ℹ Fix 1 1 | # Errors 2 2 | "yoda" == compare # SIM300 3 3 | "yoda" == compare # SIM300 @@ -71,7 +71,7 @@ SIM300.py:5:1: SIM300 [*] Yoda conditions are discouraged, use `compare == ("a", | = help: Replace Yoda condition with `compare == ("a", "b")` -ℹ Suggested fix +ℹ Fix 2 2 | "yoda" == compare # SIM300 3 3 | "yoda" == compare # SIM300 4 4 | 42 == age # SIM300 @@ -92,7 +92,7 @@ SIM300.py:6:1: SIM300 [*] Yoda conditions are discouraged, use `compare >= "yoda | = help: Replace Yoda condition with `compare >= "yoda"` -ℹ Suggested fix +ℹ Fix 3 3 | "yoda" == compare # SIM300 4 4 | 42 == age # SIM300 5 5 | ("a", "b") == compare # SIM300 @@ -113,7 +113,7 @@ SIM300.py:7:1: SIM300 [*] Yoda conditions are discouraged, use `compare > "yoda" | = help: Replace Yoda condition with `compare > "yoda"` -ℹ Suggested fix +ℹ Fix 4 4 | 42 == age # SIM300 5 5 | ("a", "b") == compare # SIM300 6 6 | "yoda" <= compare # SIM300 @@ -134,7 +134,7 @@ SIM300.py:8:1: SIM300 [*] Yoda conditions are discouraged, use `age < 42` instea | = help: Replace Yoda condition with `age < 42` -ℹ Suggested fix +ℹ Fix 5 5 | ("a", "b") == compare # SIM300 6 6 | "yoda" <= compare # SIM300 7 7 | "yoda" < compare # SIM300 @@ -155,7 +155,7 @@ SIM300.py:9:1: SIM300 [*] Yoda conditions are discouraged, use `age < -42` inste | = help: Replace Yoda condition with `age < -42` -ℹ Suggested fix +ℹ Fix 6 6 | "yoda" <= compare # SIM300 7 7 | "yoda" < compare # SIM300 8 8 | 42 > age # SIM300 @@ -176,7 +176,7 @@ SIM300.py:10:1: SIM300 [*] Yoda conditions are discouraged, use `age < +42` inst | = help: Replace Yoda condition with `age < +42` -ℹ Suggested fix +ℹ Fix 7 7 | "yoda" < compare # SIM300 8 8 | 42 > age # SIM300 9 9 | -42 > age # SIM300 @@ -197,7 +197,7 @@ SIM300.py:11:1: SIM300 [*] Yoda conditions are discouraged, use `age == YODA` in | = help: Replace Yoda condition with `age == YODA` -ℹ Suggested fix +ℹ Fix 8 8 | 42 > age # SIM300 9 9 | -42 > age # SIM300 10 10 | +42 > age # SIM300 @@ -218,7 +218,7 @@ SIM300.py:12:1: SIM300 [*] Yoda conditions are discouraged, use `age < YODA` ins | = help: Replace Yoda condition with `age < YODA` -ℹ Suggested fix +ℹ Fix 9 9 | -42 > age # SIM300 10 10 | +42 > age # SIM300 11 11 | YODA == age # SIM300 @@ -239,7 +239,7 @@ SIM300.py:13:1: SIM300 [*] Yoda conditions are discouraged, use `age <= YODA` in | = help: Replace Yoda condition with `age <= YODA` -ℹ Suggested fix +ℹ Fix 10 10 | +42 > age # SIM300 11 11 | YODA == age # SIM300 12 12 | YODA > age # SIM300 @@ -260,7 +260,7 @@ SIM300.py:14:1: SIM300 [*] Yoda conditions are discouraged, use `age == JediOrde | = help: Replace Yoda condition with `age == JediOrder.YODA` -ℹ Suggested fix +ℹ Fix 11 11 | YODA == age # SIM300 12 12 | YODA > age # SIM300 13 13 | YODA >= age # SIM300 @@ -280,7 +280,7 @@ SIM300.py:15:1: SIM300 [*] Yoda conditions are discouraged, use `(number - 100) | = help: Replace Yoda condition with `(number - 100) > 0` -ℹ Suggested fix +ℹ Fix 12 12 | YODA > age # SIM300 13 13 | YODA >= age # SIM300 14 14 | JediOrder.YODA == age # SIM300 @@ -301,7 +301,7 @@ SIM300.py:16:1: SIM300 [*] Yoda conditions are discouraged, use `(60 * 60) < Som | = help: Replace Yoda condition with `(60 * 60) < SomeClass().settings.SOME_CONSTANT_VALUE` -ℹ Suggested fix +ℹ Fix 13 13 | YODA >= age # SIM300 14 14 | JediOrder.YODA == age # SIM300 15 15 | 0 < (number - 100) # SIM300 diff --git a/crates/ruff/src/rules/flake8_simplify/snapshots/ruff__rules__flake8_simplify__tests__SIM910_SIM910.py.snap b/crates/ruff/src/rules/flake8_simplify/snapshots/ruff__rules__flake8_simplify__tests__SIM910_SIM910.py.snap index 66f8de5edb..ec96791486 100644 --- a/crates/ruff/src/rules/flake8_simplify/snapshots/ruff__rules__flake8_simplify__tests__SIM910_SIM910.py.snap +++ b/crates/ruff/src/rules/flake8_simplify/snapshots/ruff__rules__flake8_simplify__tests__SIM910_SIM910.py.snap @@ -11,7 +11,7 @@ SIM910.py:2:1: SIM910 [*] Use `{}.get(key)` instead of `{}.get(key, None)` | = help: Replace `{}.get(key, None)` with `{}.get(key)` -ℹ Suggested fix +ℹ Fix 1 1 | # SIM910 2 |-{}.get(key, None) 2 |+{}.get(key) @@ -29,7 +29,7 @@ SIM910.py:5:1: SIM910 [*] Use `{}.get("key")` instead of `{}.get("key", None)` | = help: Replace `{}.get("key", None)` with `{}.get("key")` -ℹ Suggested fix +ℹ Fix 2 2 | {}.get(key, None) 3 3 | 4 4 | # SIM910 @@ -48,7 +48,7 @@ SIM910.py:20:9: SIM910 [*] Use `{}.get(key)` instead of `{}.get(key, None)` | = help: Replace `{}.get(key, None)` with `{}.get(key)` -ℹ Suggested fix +ℹ Fix 17 17 | {}.get("key", False) 18 18 | 19 19 | # SIM910 @@ -68,7 +68,7 @@ SIM910.py:24:5: SIM910 [*] Use `{}.get(key)` instead of `{}.get(key, None)` | = help: Replace `{}.get(key, None)` with `{}.get(key)` -ℹ Suggested fix +ℹ Fix 21 21 | pass 22 22 | 23 23 | # SIM910 @@ -86,7 +86,7 @@ SIM910.py:27:1: SIM910 [*] Use `({}).get(key)` instead of `({}).get(key, None)` | = help: Replace `({}).get(key, None)` with `({}).get(key)` -ℹ Suggested fix +ℹ Fix 24 24 | a = {}.get(key, None) 25 25 | 26 26 | # SIM910 diff --git a/crates/ruff/src/rules/flake8_slots/rules/mod.rs b/crates/ruff/src/rules/flake8_slots/rules/mod.rs index d7a91ce438..abbed6b9e4 100644 --- a/crates/ruff/src/rules/flake8_slots/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_slots/rules/mod.rs @@ -1,8 +1,6 @@ -pub(crate) use no_slots_in_namedtuple_subclass::{ - no_slots_in_namedtuple_subclass, NoSlotsInNamedtupleSubclass, -}; -pub(crate) use no_slots_in_str_subclass::{no_slots_in_str_subclass, NoSlotsInStrSubclass}; -pub(crate) use no_slots_in_tuple_subclass::{no_slots_in_tuple_subclass, NoSlotsInTupleSubclass}; +pub(crate) use no_slots_in_namedtuple_subclass::*; +pub(crate) use no_slots_in_str_subclass::*; +pub(crate) use no_slots_in_tuple_subclass::*; mod helpers; mod no_slots_in_namedtuple_subclass; diff --git a/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs b/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs index 2928f8b9af..de20c1e926 100644 --- a/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs +++ b/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs @@ -3,8 +3,8 @@ use rustpython_parser::ast::{Expr, StmtClassDef}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::identifier_range; -use ruff_python_ast::prelude::Stmt; +use ruff_python_ast::identifier::Identifier; +use rustpython_parser::ast::Stmt; use crate::checkers::ast::Checker; use crate::rules::flake8_slots::rules::helpers::has_slots; @@ -46,7 +46,7 @@ use crate::rules::flake8_slots::rules::helpers::has_slots; /// ``` /// /// ## References -/// - [Python documentation: `__slots__`](https://docs.python.org/3.7/reference/datamodel.html#slots) +/// - [Python documentation: `__slots__`](https://docs.python.org/3/reference/datamodel.html#slots) #[violation] pub struct NoSlotsInNamedtupleSubclass; @@ -68,7 +68,7 @@ pub(crate) fn no_slots_in_namedtuple_subclass( return false; }; checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { matches!(call_path.as_slice(), ["collections", "namedtuple"]) @@ -77,7 +77,7 @@ pub(crate) fn no_slots_in_namedtuple_subclass( if !has_slots(&class.body) { checker.diagnostics.push(Diagnostic::new( NoSlotsInNamedtupleSubclass, - identifier_range(stmt, checker.locator), + stmt.identifier(), )); } } diff --git a/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_str_subclass.rs b/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_str_subclass.rs index 76580bb931..2df5535188 100644 --- a/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_str_subclass.rs +++ b/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_str_subclass.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::{Stmt, StmtClassDef}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::identifier_range; +use ruff_python_ast::identifier::Identifier; use crate::checkers::ast::Checker; use crate::rules::flake8_slots::rules::helpers::has_slots; @@ -37,7 +37,7 @@ use crate::rules::flake8_slots::rules::helpers::has_slots; /// ``` /// /// ## References -/// - [Python documentation: `__slots__`](https://docs.python.org/3.7/reference/datamodel.html#slots) +/// - [Python documentation: `__slots__`](https://docs.python.org/3/reference/datamodel.html#slots) #[violation] pub struct NoSlotsInStrSubclass; @@ -52,17 +52,16 @@ impl Violation for NoSlotsInStrSubclass { pub(crate) fn no_slots_in_str_subclass(checker: &mut Checker, stmt: &Stmt, class: &StmtClassDef) { if class.bases.iter().any(|base| { checker - .semantic_model() + .semantic() .resolve_call_path(base) .map_or(false, |call_path| { matches!(call_path.as_slice(), ["" | "builtins", "str"]) }) }) { if !has_slots(&class.body) { - checker.diagnostics.push(Diagnostic::new( - NoSlotsInStrSubclass, - identifier_range(stmt, checker.locator), - )); + checker + .diagnostics + .push(Diagnostic::new(NoSlotsInStrSubclass, stmt.identifier())); } } } diff --git a/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_tuple_subclass.rs b/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_tuple_subclass.rs index 3dea2b0057..98ce10bb56 100644 --- a/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_tuple_subclass.rs +++ b/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_tuple_subclass.rs @@ -2,7 +2,8 @@ use rustpython_parser::ast::{Stmt, StmtClassDef}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::{identifier_range, map_subscript}; +use ruff_python_ast::helpers::map_subscript; +use ruff_python_ast::identifier::Identifier; use crate::checkers::ast::Checker; use crate::rules::flake8_slots::rules::helpers::has_slots; @@ -37,7 +38,7 @@ use crate::rules::flake8_slots::rules::helpers::has_slots; /// ``` /// /// ## References -/// - [Python documentation: `__slots__`](https://docs.python.org/3.7/reference/datamodel.html#slots) +/// - [Python documentation: `__slots__`](https://docs.python.org/3/reference/datamodel.html#slots) #[violation] pub struct NoSlotsInTupleSubclass; @@ -52,20 +53,19 @@ impl Violation for NoSlotsInTupleSubclass { pub(crate) fn no_slots_in_tuple_subclass(checker: &mut Checker, stmt: &Stmt, class: &StmtClassDef) { if class.bases.iter().any(|base| { checker - .semantic_model() + .semantic() .resolve_call_path(map_subscript(base)) .map_or(false, |call_path| { matches!(call_path.as_slice(), ["" | "builtins", "tuple"]) || checker - .semantic_model() + .semantic() .match_typing_call_path(&call_path, "Tuple") }) }) { if !has_slots(&class.body) { - checker.diagnostics.push(Diagnostic::new( - NoSlotsInTupleSubclass, - identifier_range(stmt, checker.locator), - )); + checker + .diagnostics + .push(Diagnostic::new(NoSlotsInTupleSubclass, stmt.identifier())); } } } diff --git a/crates/ruff/src/rules/flake8_tidy_imports/rules/banned_api.rs b/crates/ruff/src/rules/flake8_tidy_imports/rules/banned_api.rs index 668f764354..6730ce8700 100644 --- a/crates/ruff/src/rules/flake8_tidy_imports/rules/banned_api.rs +++ b/crates/ruff/src/rules/flake8_tidy_imports/rules/banned_api.rs @@ -86,7 +86,7 @@ pub(crate) fn banned_attribute_access(checker: &mut Checker, expr: &Expr) { let banned_api = &checker.settings.flake8_tidy_imports.banned_api; if let Some((banned_path, ban)) = checker - .semantic_model() + .semantic() .resolve_call_path(expr) .and_then(|call_path| { banned_api diff --git a/crates/ruff/src/rules/flake8_tidy_imports/rules/mod.rs b/crates/ruff/src/rules/flake8_tidy_imports/rules/mod.rs index 8efbdfd6ae..660116d718 100644 --- a/crates/ruff/src/rules/flake8_tidy_imports/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_tidy_imports/rules/mod.rs @@ -1,7 +1,5 @@ -pub(crate) use banned_api::{ - banned_attribute_access, name_is_banned, name_or_parent_is_banned, BannedApi, -}; -pub(crate) use relative_imports::{banned_relative_import, RelativeImports}; +pub(crate) use banned_api::*; +pub(crate) use relative_imports::*; mod banned_api; mod relative_imports; diff --git a/crates/ruff/src/rules/flake8_tidy_imports/rules/relative_imports.rs b/crates/ruff/src/rules/flake8_tidy_imports/rules/relative_imports.rs index 9bf9bc41a8..5012d69950 100644 --- a/crates/ruff/src/rules/flake8_tidy_imports/rules/relative_imports.rs +++ b/crates/ruff/src/rules/flake8_tidy_imports/rules/relative_imports.rs @@ -1,5 +1,5 @@ use ruff_text_size::TextRange; -use rustpython_parser::ast::{self, Int, Ranged, Stmt}; +use rustpython_parser::ast::{self, Identifier, Int, Ranged, Stmt}; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -31,9 +31,6 @@ use crate::rules::flake8_tidy_imports::settings::Strictness; /// > from .sibling import example /// > ``` /// -/// ## Options -/// - `flake8-tidy-imports.ban-relative-imports` -/// /// ## Example /// ```python /// from .. import foo @@ -44,6 +41,9 @@ use crate::rules::flake8_tidy_imports::settings::Strictness; /// from mypkg import foo /// ``` /// +/// ## Options +/// - `flake8-tidy-imports.ban-relative-imports` +/// /// [PEP 8]: https://peps.python.org/pep-0008/#imports #[violation] pub struct RelativeImports { @@ -94,14 +94,16 @@ fn fix_banned_relative_import( panic!("Expected Stmt::ImportFrom"); }; let node = ast::StmtImportFrom { - module: Some(module_path.to_string().into()), + module: Some(Identifier::new( + module_path.to_string(), + TextRange::default(), + )), names: names.clone(), level: Some(Int::new(0)), range: TextRange::default(), }; let content = generator.stmt(&node.into()); - #[allow(deprecated)] - Some(Fix::unspecified(Edit::range_replacement( + Some(Fix::suggested(Edit::range_replacement( content, stmt.range(), ))) diff --git a/crates/ruff/src/rules/flake8_todos/rules/mod.rs b/crates/ruff/src/rules/flake8_todos/rules/mod.rs index dd10c6bc3b..9e5980aeb9 100644 --- a/crates/ruff/src/rules/flake8_todos/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_todos/rules/mod.rs @@ -1,6 +1,3 @@ -pub(crate) use todos::{ - todos, InvalidTodoCapitalization, InvalidTodoTag, MissingSpaceAfterTodoColon, - MissingTodoAuthor, MissingTodoColon, MissingTodoDescription, MissingTodoLink, -}; +pub(crate) use todos::*; mod todos; diff --git a/crates/ruff/src/rules/flake8_todos/rules/todos.rs b/crates/ruff/src/rules/flake8_todos/rules/todos.rs index b032c111fd..7bd52d9ba1 100644 --- a/crates/ruff/src/rules/flake8_todos/rules/todos.rs +++ b/crates/ruff/src/rules/flake8_todos/rules/todos.rs @@ -67,7 +67,7 @@ pub struct MissingTodoAuthor; impl Violation for MissingTodoAuthor { #[derive_message_formats] fn message(&self) -> String { - format!("Missing author in TODO; try: `# TODO(): ...`") + format!("Missing author in TODO; try: `# TODO(): ...` or `# TODO @: ...`") } } @@ -227,21 +227,20 @@ impl Violation for MissingSpaceAfterTodoColon { static ISSUE_LINK_REGEX_SET: Lazy = Lazy::new(|| { RegexSet::new([ - r#"^#\s*(http|https)://.*"#, // issue link - r#"^#\s*\d+$"#, // issue code - like "003" - r#"^#\s*[A-Z]{1,6}\-?\d+$"#, // issue code - like "TD003" or "TD-003" + r"^#\s*(http|https)://.*", // issue link + r"^#\s*\d+$", // issue code - like "003" + r"^#\s*[A-Z]{1,6}\-?\d+$", // issue code - like "TD003" ]) .unwrap() }); pub(crate) fn todos( + diagnostics: &mut Vec, todo_comments: &[TodoComment], - indexer: &Indexer, locator: &Locator, + indexer: &Indexer, settings: &Settings, -) -> Vec { - let mut diagnostics: Vec = vec![]; - +) { for todo_comment in todo_comments { let TodoComment { directive, @@ -256,8 +255,8 @@ pub(crate) fn todos( continue; } - directive_errors(directive, &mut diagnostics, settings); - static_errors(&mut diagnostics, content, range, directive); + directive_errors(diagnostics, directive, settings); + static_errors(diagnostics, content, range, directive); let mut has_issue_link = false; let mut curr_range = range; @@ -297,14 +296,12 @@ pub(crate) fn todos( diagnostics.push(Diagnostic::new(MissingTodoLink, directive.range)); } } - - diagnostics } /// Check that the directive itself is valid. This function modifies `diagnostics` in-place. fn directive_errors( - directive: &TodoDirective, diagnostics: &mut Vec, + directive: &TodoDirective, settings: &Settings, ) { if directive.content == "TODO" { @@ -339,8 +336,7 @@ fn directive_errors( } } -/// Checks for "static" errors in the comment: missing colon, missing author, etc. This function -/// modifies `diagnostics` in-place. +/// Checks for "static" errors in the comment: missing colon, missing author, etc. fn static_errors( diagnostics: &mut Vec, comment: &str, @@ -358,6 +354,15 @@ fn static_errors( } else { trimmed.text_len() } + } else if trimmed.starts_with('@') { + if let Some(end_index) = trimmed.find(|c: char| c.is_whitespace() || c == ':') { + TextSize::try_from(end_index).unwrap() + } else { + // TD002 + diagnostics.push(Diagnostic::new(MissingTodoAuthor, directive.range)); + + TextSize::new(0) + } } else { // TD002 diagnostics.push(Diagnostic::new(MissingTodoAuthor, directive.range)); diff --git a/crates/ruff/src/rules/flake8_todos/snapshots/ruff__rules__flake8_todos__tests__missing-todo-author_TD002.py.snap b/crates/ruff/src/rules/flake8_todos/snapshots/ruff__rules__flake8_todos__tests__missing-todo-author_TD002.py.snap index 0647b705d4..f14a00cd5f 100644 --- a/crates/ruff/src/rules/flake8_todos/snapshots/ruff__rules__flake8_todos__tests__missing-todo-author_TD002.py.snap +++ b/crates/ruff/src/rules/flake8_todos/snapshots/ruff__rules__flake8_todos__tests__missing-todo-author_TD002.py.snap @@ -1,41 +1,41 @@ --- source: crates/ruff/src/rules/flake8_todos/mod.rs --- -TD002.py:5:3: TD002 Missing author in TODO; try: `# TODO(): ...` - | -3 | # TODO(evanrittenhouse): this also has an author -4 | # T002 - errors -5 | # TODO: this has no author - | ^^^^ TD002 -6 | # FIXME: neither does this -7 | # TODO : and neither does this - | +TD002.py:11:3: TD002 Missing author in TODO; try: `# TODO(): ...` or `# TODO @: ...` + | + 9 | # TODO @mayrholu and more: this has an author +10 | # T002 - errors +11 | # TODO: this has no author + | ^^^^ TD002 +12 | # FIXME: neither does this +13 | # TODO : and neither does this + | -TD002.py:6:3: TD002 Missing author in TODO; try: `# TODO(): ...` - | -4 | # T002 - errors -5 | # TODO: this has no author -6 | # FIXME: neither does this - | ^^^^^ TD002 -7 | # TODO : and neither does this -8 | # foo # TODO: this doesn't either - | +TD002.py:12:3: TD002 Missing author in TODO; try: `# TODO(): ...` or `# TODO @: ...` + | +10 | # T002 - errors +11 | # TODO: this has no author +12 | # FIXME: neither does this + | ^^^^^ TD002 +13 | # TODO : and neither does this +14 | # foo # TODO: this doesn't either + | -TD002.py:7:3: TD002 Missing author in TODO; try: `# TODO(): ...` - | -5 | # TODO: this has no author -6 | # FIXME: neither does this -7 | # TODO : and neither does this - | ^^^^ TD002 -8 | # foo # TODO: this doesn't either - | +TD002.py:13:3: TD002 Missing author in TODO; try: `# TODO(): ...` or `# TODO @: ...` + | +11 | # TODO: this has no author +12 | # FIXME: neither does this +13 | # TODO : and neither does this + | ^^^^ TD002 +14 | # foo # TODO: this doesn't either + | -TD002.py:8:9: TD002 Missing author in TODO; try: `# TODO(): ...` - | -6 | # FIXME: neither does this -7 | # TODO : and neither does this -8 | # foo # TODO: this doesn't either - | ^^^^ TD002 - | +TD002.py:14:9: TD002 Missing author in TODO; try: `# TODO(): ...` or `# TODO @: ...` + | +12 | # FIXME: neither does this +13 | # TODO : and neither does this +14 | # foo # TODO: this doesn't either + | ^^^^ TD002 + | diff --git a/crates/ruff/src/rules/flake8_type_checking/helpers.rs b/crates/ruff/src/rules/flake8_type_checking/helpers.rs index b8d399aa01..144e5eee12 100644 --- a/crates/ruff/src/rules/flake8_type_checking/helpers.rs +++ b/crates/ruff/src/rules/flake8_type_checking/helpers.rs @@ -2,51 +2,44 @@ use rustpython_parser::ast; use ruff_python_ast::call_path::from_qualified_name; use ruff_python_ast::helpers::map_callable; -use ruff_python_semantic::binding::{Binding, BindingKind}; -use ruff_python_semantic::model::SemanticModel; -use ruff_python_semantic::scope::ScopeKind; +use ruff_python_semantic::{Binding, BindingKind, ScopeKind, SemanticModel}; -pub(crate) fn is_valid_runtime_import(semantic_model: &SemanticModel, binding: &Binding) -> bool { +pub(crate) fn is_valid_runtime_import(binding: &Binding, semantic: &SemanticModel) -> bool { if matches!( binding.kind, - BindingKind::Importation(..) - | BindingKind::FromImportation(..) - | BindingKind::SubmoduleImportation(..) + BindingKind::Import(..) | BindingKind::FromImport(..) | BindingKind::SubmoduleImport(..) ) { binding.context.is_runtime() - && binding.references().any(|reference_id| { - semantic_model - .reference(reference_id) - .context() - .is_runtime() - }) + && binding + .references() + .any(|reference_id| semantic.reference(reference_id).context().is_runtime()) } else { false } } pub(crate) fn runtime_evaluated( - semantic_model: &SemanticModel, base_classes: &[String], decorators: &[String], + semantic: &SemanticModel, ) -> bool { if !base_classes.is_empty() { - if runtime_evaluated_base_class(semantic_model, base_classes) { + if runtime_evaluated_base_class(base_classes, semantic) { return true; } } if !decorators.is_empty() { - if runtime_evaluated_decorators(semantic_model, decorators) { + if runtime_evaluated_decorators(decorators, semantic) { return true; } } false } -fn runtime_evaluated_base_class(semantic_model: &SemanticModel, base_classes: &[String]) -> bool { - if let ScopeKind::Class(ast::StmtClassDef { bases, .. }) = &semantic_model.scope().kind { - for base in bases.iter() { - if let Some(call_path) = semantic_model.resolve_call_path(base) { +fn runtime_evaluated_base_class(base_classes: &[String], semantic: &SemanticModel) -> bool { + if let ScopeKind::Class(ast::StmtClassDef { bases, .. }) = &semantic.scope().kind { + for base in bases { + if let Some(call_path) = semantic.resolve_call_path(base) { if base_classes .iter() .any(|base_class| from_qualified_name(base_class) == call_path) @@ -59,12 +52,10 @@ fn runtime_evaluated_base_class(semantic_model: &SemanticModel, base_classes: &[ false } -fn runtime_evaluated_decorators(semantic_model: &SemanticModel, decorators: &[String]) -> bool { - if let ScopeKind::Class(ast::StmtClassDef { decorator_list, .. }) = &semantic_model.scope().kind - { - for decorator in decorator_list.iter() { - if let Some(call_path) = - semantic_model.resolve_call_path(map_callable(&decorator.expression)) +fn runtime_evaluated_decorators(decorators: &[String], semantic: &SemanticModel) -> bool { + if let ScopeKind::Class(ast::StmtClassDef { decorator_list, .. }) = &semantic.scope().kind { + for decorator in decorator_list { + if let Some(call_path) = semantic.resolve_call_path(map_callable(&decorator.expression)) { if decorators .iter() diff --git a/crates/ruff/src/rules/flake8_type_checking/mod.rs b/crates/ruff/src/rules/flake8_type_checking/mod.rs index 06ea6ab839..01aa8c60e4 100644 --- a/crates/ruff/src/rules/flake8_type_checking/mod.rs +++ b/crates/ruff/src/rules/flake8_type_checking/mod.rs @@ -327,7 +327,7 @@ mod tests { fn contents(contents: &str, snapshot: &str) { let diagnostics = test_snippet( contents, - &settings::Settings::for_rules(&Linter::Flake8TypeChecking), + &settings::Settings::for_rules(Linter::Flake8TypeChecking.rules()), ); assert_messages!(snapshot, diagnostics); } diff --git a/crates/ruff/src/rules/flake8_type_checking/rules/empty_type_checking_block.rs b/crates/ruff/src/rules/flake8_type_checking/rules/empty_type_checking_block.rs index 2e90e50284..f3d0929615 100644 --- a/crates/ruff/src/rules/flake8_type_checking/rules/empty_type_checking_block.rs +++ b/crates/ruff/src/rules/flake8_type_checking/rules/empty_type_checking_block.rs @@ -60,15 +60,9 @@ pub(crate) fn empty_type_checking_block(checker: &mut Checker, stmt: &ast::StmtI let mut diagnostic = Diagnostic::new(EmptyTypeCheckingBlock, stmt.range()); if checker.patch(diagnostic.kind.rule()) { // Delete the entire type-checking block. - let stmt = checker.semantic_model().stmt(); - let parent = checker.semantic_model().stmt_parent(); - let edit = autofix::edits::delete_stmt( - stmt, - parent, - checker.locator, - checker.indexer, - checker.stylist, - ); + let stmt = checker.semantic().stmt(); + let parent = checker.semantic().stmt_parent(); + let edit = autofix::edits::delete_stmt(stmt, parent, checker.locator, checker.indexer); diagnostic.set_fix(Fix::automatic(edit).isolate(checker.isolation(parent))); } checker.diagnostics.push(diagnostic); diff --git a/crates/ruff/src/rules/flake8_type_checking/rules/mod.rs b/crates/ruff/src/rules/flake8_type_checking/rules/mod.rs index 3873ebd1e7..15ceb3ddf1 100644 --- a/crates/ruff/src/rules/flake8_type_checking/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_type_checking/rules/mod.rs @@ -1,11 +1,6 @@ -pub(crate) use empty_type_checking_block::{empty_type_checking_block, EmptyTypeCheckingBlock}; -pub(crate) use runtime_import_in_type_checking_block::{ - runtime_import_in_type_checking_block, RuntimeImportInTypeCheckingBlock, -}; -pub(crate) use typing_only_runtime_import::{ - typing_only_runtime_import, TypingOnlyFirstPartyImport, TypingOnlyStandardLibraryImport, - TypingOnlyThirdPartyImport, -}; +pub(crate) use empty_type_checking_block::*; +pub(crate) use runtime_import_in_type_checking_block::*; +pub(crate) use typing_only_runtime_import::*; mod empty_type_checking_block; mod runtime_import_in_type_checking_block; diff --git a/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs b/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs index c4edc8b8d5..da9795bec7 100644 --- a/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs +++ b/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs @@ -4,9 +4,7 @@ use rustc_hash::FxHashMap; use ruff_diagnostics::{AutofixKind, Diagnostic, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::node::NodeId; -use ruff_python_semantic::reference::ReferenceId; -use ruff_python_semantic::scope::Scope; +use ruff_python_semantic::{NodeId, ReferenceId, Scope}; use crate::autofix; use crate::checkers::ast::Checker; @@ -75,7 +73,7 @@ pub(crate) fn runtime_import_in_type_checking_block( let mut ignores_by_statement: FxHashMap> = FxHashMap::default(); for binding_id in scope.binding_ids() { - let binding = &checker.semantic_model().bindings[binding_id]; + let binding = checker.semantic().binding(binding_id); let Some(qualified_name) = binding.qualified_name() else { continue; @@ -88,7 +86,7 @@ pub(crate) fn runtime_import_in_type_checking_block( if binding.context.is_typing() && binding.references().any(|reference_id| { checker - .semantic_model() + .semantic() .reference(reference_id) .context() .is_runtime() @@ -101,17 +99,18 @@ pub(crate) fn runtime_import_in_type_checking_block( let import = Import { qualified_name, reference_id, - trimmed_range: binding.trimmed_range(checker.semantic_model(), checker.locator), - parent_range: binding.parent_range(checker.semantic_model()), + range: binding.range, + parent_range: binding.parent_range(checker.semantic()), }; - if checker.rule_is_ignored( - Rule::RuntimeImportInTypeCheckingBlock, - import.trimmed_range.start(), - ) || import.parent_range.map_or(false, |parent_range| { - checker - .rule_is_ignored(Rule::RuntimeImportInTypeCheckingBlock, parent_range.start()) - }) { + if checker.rule_is_ignored(Rule::RuntimeImportInTypeCheckingBlock, import.range.start()) + || import.parent_range.map_or(false, |parent_range| { + checker.rule_is_ignored( + Rule::RuntimeImportInTypeCheckingBlock, + parent_range.start(), + ) + }) + { ignores_by_statement .entry(stmt_id) .or_default() @@ -133,7 +132,7 @@ pub(crate) fn runtime_import_in_type_checking_block( for Import { qualified_name, - trimmed_range, + range, parent_range, .. } in imports @@ -142,7 +141,7 @@ pub(crate) fn runtime_import_in_type_checking_block( RuntimeImportInTypeCheckingBlock { qualified_name: qualified_name.to_string(), }, - trimmed_range, + range, ); if let Some(range) = parent_range { diagnostic.set_parent(range.start()); @@ -158,7 +157,7 @@ pub(crate) fn runtime_import_in_type_checking_block( // suppression comments aren't marked as unused. for Import { qualified_name, - trimmed_range, + range, parent_range, .. } in ignores_by_statement.into_values().flatten() @@ -167,7 +166,7 @@ pub(crate) fn runtime_import_in_type_checking_block( RuntimeImportInTypeCheckingBlock { qualified_name: qualified_name.to_string(), }, - trimmed_range, + range, ); if let Some(range) = parent_range { diagnostic.set_parent(range.start()); @@ -183,15 +182,15 @@ struct Import<'a> { /// The first reference to the imported symbol. reference_id: ReferenceId, /// The trimmed range of the import (e.g., `List` in `from typing import List`). - trimmed_range: TextRange, + range: TextRange, /// The range of the import's parent statement. parent_range: Option, } /// Generate a [`Fix`] to remove runtime imports from a type-checking block. fn fix_imports(checker: &Checker, stmt_id: NodeId, imports: &[Import]) -> Result { - let stmt = checker.semantic_model().stmts[stmt_id]; - let parent = checker.semantic_model().stmts.parent(stmt); + let stmt = checker.semantic().stmts[stmt_id]; + let parent = checker.semantic().stmts.parent(stmt); let qualified_names: Vec<&str> = imports .iter() .map(|Import { qualified_name, .. }| *qualified_name) @@ -201,11 +200,7 @@ fn fix_imports(checker: &Checker, stmt_id: NodeId, imports: &[Import]) -> Result let at = imports .iter() .map(|Import { reference_id, .. }| { - checker - .semantic_model() - .reference(*reference_id) - .range() - .start() + checker.semantic().reference(*reference_id).range().start() }) .min() .expect("Expected at least one import"); @@ -216,8 +211,8 @@ fn fix_imports(checker: &Checker, stmt_id: NodeId, imports: &[Import]) -> Result stmt, parent, checker.locator, - checker.indexer, checker.stylist, + checker.indexer, )?; // Step 2) Add the import to the top-level. diff --git a/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs b/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs index c509e252db..5d1ee695bd 100644 --- a/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs +++ b/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs @@ -4,10 +4,7 @@ use rustc_hash::FxHashMap; use ruff_diagnostics::{AutofixKind, Diagnostic, DiagnosticKind, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::binding::Binding; -use ruff_python_semantic::node::NodeId; -use ruff_python_semantic::reference::ReferenceId; -use ruff_python_semantic::scope::Scope; +use ruff_python_semantic::{Binding, NodeId, ReferenceId, Scope}; use crate::autofix; use crate::checkers::ast::Checker; @@ -197,7 +194,7 @@ pub(crate) fn typing_only_runtime_import( FxHashMap::default(); for binding_id in scope.binding_ids() { - let binding = &checker.semantic_model().bindings[binding_id]; + let binding = checker.semantic().binding(binding_id); // If we're in un-strict mode, don't flag typing-only imports that are // implicitly loaded by way of a valid runtime import. @@ -233,7 +230,7 @@ pub(crate) fn typing_only_runtime_import( if binding.context.is_runtime() && binding.references().all(|reference_id| { checker - .semantic_model() + .semantic() .reference(reference_id) .context() .is_typing() @@ -281,11 +278,11 @@ pub(crate) fn typing_only_runtime_import( let import = Import { qualified_name, reference_id, - trimmed_range: binding.trimmed_range(checker.semantic_model(), checker.locator), - parent_range: binding.parent_range(checker.semantic_model()), + range: binding.range, + parent_range: binding.parent_range(checker.semantic()), }; - if checker.rule_is_ignored(rule_for(import_type), import.trimmed_range.start()) + if checker.rule_is_ignored(rule_for(import_type), import.range.start()) || import.parent_range.map_or(false, |parent_range| { checker.rule_is_ignored(rule_for(import_type), parent_range.start()) }) @@ -314,14 +311,14 @@ pub(crate) fn typing_only_runtime_import( for Import { qualified_name, - trimmed_range, + range, parent_range, .. } in imports { let mut diagnostic = Diagnostic::new( diagnostic_for(import_type, qualified_name.to_string()), - trimmed_range, + range, ); if let Some(range) = parent_range { diagnostic.set_parent(range.start()); @@ -338,14 +335,14 @@ pub(crate) fn typing_only_runtime_import( for ((_, import_type), imports) in ignores_by_statement { for Import { qualified_name, - trimmed_range, + range, parent_range, .. } in imports { let mut diagnostic = Diagnostic::new( diagnostic_for(import_type, qualified_name.to_string()), - trimmed_range, + range, ); if let Some(range) = parent_range { diagnostic.set_parent(range.start()); @@ -362,7 +359,7 @@ struct Import<'a> { /// The first reference to the imported symbol. reference_id: ReferenceId, /// The trimmed range of the import (e.g., `List` in `from typing import List`). - trimmed_range: TextRange, + range: TextRange, /// The range of the import's parent statement. parent_range: Option, } @@ -416,8 +413,8 @@ fn is_exempt(name: &str, exempt_modules: &[&str]) -> bool { /// Generate a [`Fix`] to remove typing-only imports from a runtime context. fn fix_imports(checker: &Checker, stmt_id: NodeId, imports: &[Import]) -> Result { - let stmt = checker.semantic_model().stmts[stmt_id]; - let parent = checker.semantic_model().stmts.parent(stmt); + let stmt = checker.semantic().stmts[stmt_id]; + let parent = checker.semantic().stmts.parent(stmt); let qualified_names: Vec<&str> = imports .iter() .map(|Import { qualified_name, .. }| *qualified_name) @@ -427,11 +424,7 @@ fn fix_imports(checker: &Checker, stmt_id: NodeId, imports: &[Import]) -> Result let at = imports .iter() .map(|Import { reference_id, .. }| { - checker - .semantic_model() - .reference(*reference_id) - .range() - .start() + checker.semantic().reference(*reference_id).range().start() }) .min() .expect("Expected at least one import"); @@ -442,8 +435,8 @@ fn fix_imports(checker: &Checker, stmt_id: NodeId, imports: &[Import]) -> Result stmt, parent, checker.locator, - checker.indexer, checker.stylist, + checker.indexer, )?; // Step 2) Add the import to a `TYPE_CHECKING` block. @@ -453,7 +446,7 @@ fn fix_imports(checker: &Checker, stmt_id: NodeId, imports: &[Import]) -> Result qualified_names, }, at, - checker.semantic_model(), + checker.semantic(), )?; Ok( diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__no_typing_import.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__no_typing_import.snap index 49b96c6624..51e4ecc7c4 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__no_typing_import.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__no_typing_import.snap @@ -1,12 +1,12 @@ --- source: crates/ruff/src/rules/flake8_type_checking/mod.rs --- -:4:8: TCH002 [*] Move third-party import `pandas` into a type-checking block +:4:18: TCH002 [*] Move third-party import `pandas` into a type-checking block | 2 | from __future__ import annotations 3 | 4 | import pandas as pd - | ^^^^^^^^^^^^ TCH002 + | ^^ TCH002 5 | 6 | def f(x: pd.DataFrame): | diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__strict.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__strict.snap index c969cd7bc4..9ef1a60a95 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__strict.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__strict.snap @@ -117,12 +117,12 @@ strict.py:62:12: TCH002 [*] Move third-party import `pkg` into a type-checking b 64 67 | 65 68 | def test(value: pkg.A): -strict.py:71:12: TCH002 [*] Move third-party import `pkg.foo` into a type-checking block +strict.py:71:23: TCH002 [*] Move third-party import `pkg.foo` into a type-checking block | 69 | def f(): 70 | # In un-strict mode, this shouldn't raise an error, since `pkg.foo.bar` is used at runtime. 71 | import pkg.foo as F - | ^^^^^^^^^^^^ TCH002 + | ^ TCH002 72 | import pkg.foo.bar as B | = help: Move into type-checking block @@ -201,12 +201,12 @@ strict.py:91:12: TCH002 [*] Move third-party import `pkg` into a type-checking b 93 96 | 94 97 | def test(value: pkg.A): -strict.py:101:12: TCH002 [*] Move third-party import `pkg.foo` into a type-checking block +strict.py:101:23: TCH002 [*] Move third-party import `pkg.foo` into a type-checking block | 99 | # In un-strict mode, this shouldn't raise an error, since `pkg` is used at runtime. 100 | import pkg.bar as B 101 | import pkg.foo as F - | ^^^^^^^^^^^^ TCH002 + | ^ TCH002 102 | 103 | def test(value: F.Foo): | diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_after_usage.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_after_usage.snap index d208730b97..8eb591e7db 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_after_usage.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_after_usage.snap @@ -1,12 +1,12 @@ --- source: crates/ruff/src/rules/flake8_type_checking/mod.rs --- -:6:8: TCH002 [*] Move third-party import `pandas` into a type-checking block +:6:18: TCH002 [*] Move third-party import `pandas` into a type-checking block | 4 | from typing import TYPE_CHECKING 5 | 6 | import pandas as pd - | ^^^^^^^^^^^^ TCH002 + | ^^ TCH002 7 | 8 | def f(x: pd.DataFrame): | diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_comment.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_comment.snap index e186f50adb..43ed4cbe88 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_comment.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_comment.snap @@ -1,12 +1,12 @@ --- source: crates/ruff/src/rules/flake8_type_checking/mod.rs --- -:6:8: TCH002 [*] Move third-party import `pandas` into a type-checking block +:6:18: TCH002 [*] Move third-party import `pandas` into a type-checking block | 4 | from typing import TYPE_CHECKING 5 | 6 | import pandas as pd - | ^^^^^^^^^^^^ TCH002 + | ^^ TCH002 7 | 8 | if TYPE_CHECKING: | diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_inline.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_inline.snap index 5fa5f507ca..983345aae9 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_inline.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_inline.snap @@ -1,12 +1,12 @@ --- source: crates/ruff/src/rules/flake8_type_checking/mod.rs --- -:6:8: TCH002 [*] Move third-party import `pandas` into a type-checking block +:6:18: TCH002 [*] Move third-party import `pandas` into a type-checking block | 4 | from typing import TYPE_CHECKING 5 | 6 | import pandas as pd - | ^^^^^^^^^^^^ TCH002 + | ^^ TCH002 7 | 8 | if TYPE_CHECKING: import os | diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_own_line.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_own_line.snap index 3e490001a0..4246c582dc 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_own_line.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_own_line.snap @@ -1,12 +1,12 @@ --- source: crates/ruff/src/rules/flake8_type_checking/mod.rs --- -:6:8: TCH002 [*] Move third-party import `pandas` into a type-checking block +:6:18: TCH002 [*] Move third-party import `pandas` into a type-checking block | 4 | from typing import TYPE_CHECKING 5 | 6 | import pandas as pd - | ^^^^^^^^^^^^ TCH002 + | ^^ TCH002 7 | 8 | if TYPE_CHECKING: | diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_TCH002.py.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_TCH002.py.snap index 19eb823d73..ab4ed38714 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_TCH002.py.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_TCH002.py.snap @@ -1,11 +1,11 @@ --- source: crates/ruff/src/rules/flake8_type_checking/mod.rs --- -TCH002.py:5:12: TCH002 [*] Move third-party import `pandas` into a type-checking block +TCH002.py:5:22: TCH002 [*] Move third-party import `pandas` into a type-checking block | 4 | def f(): 5 | import pandas as pd # TCH002 - | ^^^^^^^^^^^^ TCH002 + | ^^ TCH002 6 | 7 | x: pd.DataFrame | @@ -53,11 +53,11 @@ TCH002.py:11:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a ty 13 16 | x: DataFrame 14 17 | -TCH002.py:17:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a type-checking block +TCH002.py:17:37: TCH002 [*] Move third-party import `pandas.DataFrame` into a type-checking block | 16 | def f(): 17 | from pandas import DataFrame as df # TCH002 - | ^^^^^^^^^^^^^^^ TCH002 + | ^^ TCH002 18 | 19 | x: df | @@ -81,11 +81,11 @@ TCH002.py:17:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a ty 19 22 | x: df 20 23 | -TCH002.py:23:12: TCH002 [*] Move third-party import `pandas` into a type-checking block +TCH002.py:23:22: TCH002 [*] Move third-party import `pandas` into a type-checking block | 22 | def f(): 23 | import pandas as pd # TCH002 - | ^^^^^^^^^^^^ TCH002 + | ^^ TCH002 24 | 25 | x: pd.DataFrame = 1 | @@ -137,11 +137,11 @@ TCH002.py:29:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a ty 31 34 | x: DataFrame = 2 32 35 | -TCH002.py:35:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a type-checking block +TCH002.py:35:37: TCH002 [*] Move third-party import `pandas.DataFrame` into a type-checking block | 34 | def f(): 35 | from pandas import DataFrame as df # TCH002 - | ^^^^^^^^^^^^^^^ TCH002 + | ^^ TCH002 36 | 37 | x: df = 3 | @@ -165,11 +165,11 @@ TCH002.py:35:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a ty 37 40 | x: df = 3 38 41 | -TCH002.py:41:12: TCH002 [*] Move third-party import `pandas` into a type-checking block +TCH002.py:41:22: TCH002 [*] Move third-party import `pandas` into a type-checking block | 40 | def f(): 41 | import pandas as pd # TCH002 - | ^^^^^^^^^^^^ TCH002 + | ^^ TCH002 42 | 43 | x: "pd.DataFrame" = 1 | @@ -193,11 +193,11 @@ TCH002.py:41:12: TCH002 [*] Move third-party import `pandas` into a type-checkin 43 46 | x: "pd.DataFrame" = 1 44 47 | -TCH002.py:47:12: TCH002 [*] Move third-party import `pandas` into a type-checking block +TCH002.py:47:22: TCH002 [*] Move third-party import `pandas` into a type-checking block | 46 | def f(): 47 | import pandas as pd # TCH002 - | ^^^^^^^^^^^^ TCH002 + | ^^ TCH002 48 | 49 | x = dict["pd.DataFrame", "pd.DataFrame"] | @@ -221,4 +221,32 @@ TCH002.py:47:12: TCH002 [*] Move third-party import `pandas` into a type-checkin 49 52 | x = dict["pd.DataFrame", "pd.DataFrame"] 50 53 | +TCH002.py:172:24: TCH002 [*] Move third-party import `module.Member` into a type-checking block + | +170 | global Member +171 | +172 | from module import Member + | ^^^^^^ TCH002 +173 | +174 | x: Member = 1 + | + = help: Move into type-checking block + +ℹ Suggested fix +1 1 | """Tests to determine accurate detection of typing-only imports.""" + 2 |+from typing import TYPE_CHECKING + 3 |+ + 4 |+if TYPE_CHECKING: + 5 |+ from module import Member +2 6 | +3 7 | +4 8 | def f(): +-------------------------------------------------------------------------------- +169 173 | def f(): +170 174 | global Member +171 175 | +172 |- from module import Member +173 176 | +174 177 | x: Member = 1 + diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_runtime_evaluated_base_classes_2.py.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_runtime_evaluated_base_classes_2.py.snap index de8fe6ddd7..c90b345fe7 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_runtime_evaluated_base_classes_2.py.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_runtime_evaluated_base_classes_2.py.snap @@ -1,12 +1,12 @@ --- source: crates/ruff/src/rules/flake8_type_checking/mod.rs --- -runtime_evaluated_base_classes_2.py:3:8: TCH002 [*] Move third-party import `geopandas` into a type-checking block +runtime_evaluated_base_classes_2.py:3:21: TCH002 [*] Move third-party import `geopandas` into a type-checking block | 1 | from __future__ import annotations 2 | 3 | import geopandas as gpd # TCH002 - | ^^^^^^^^^^^^^^^^ TCH002 + | ^^^ TCH002 4 | import pydantic 5 | import pyproj # TCH002 | diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing_import_after_package_import.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing_import_after_package_import.snap index 27235f0f48..41b6a8f207 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing_import_after_package_import.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing_import_after_package_import.snap @@ -1,12 +1,12 @@ --- source: crates/ruff/src/rules/flake8_type_checking/mod.rs --- -:4:8: TCH002 [*] Move third-party import `pandas` into a type-checking block +:4:18: TCH002 [*] Move third-party import `pandas` into a type-checking block | 2 | from __future__ import annotations 3 | 4 | import pandas as pd - | ^^^^^^^^^^^^ TCH002 + | ^^ TCH002 5 | 6 | from typing import TYPE_CHECKING | diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing_import_after_usage.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing_import_after_usage.snap index 4c598b4014..271f52f2df 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing_import_after_usage.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing_import_after_usage.snap @@ -1,12 +1,12 @@ --- source: crates/ruff/src/rules/flake8_type_checking/mod.rs --- -:4:8: TCH002 Move third-party import `pandas` into a type-checking block +:4:18: TCH002 Move third-party import `pandas` into a type-checking block | 2 | from __future__ import annotations 3 | 4 | import pandas as pd - | ^^^^^^^^^^^^ TCH002 + | ^^ TCH002 5 | 6 | def f(x: pd.DataFrame): | diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing_import_before_package_import.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing_import_before_package_import.snap index 4530c20fc5..06752d7221 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing_import_before_package_import.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing_import_before_package_import.snap @@ -1,12 +1,12 @@ --- source: crates/ruff/src/rules/flake8_type_checking/mod.rs --- -:6:8: TCH002 [*] Move third-party import `pandas` into a type-checking block +:6:18: TCH002 [*] Move third-party import `pandas` into a type-checking block | 4 | from typing import TYPE_CHECKING 5 | 6 | import pandas as pd - | ^^^^^^^^^^^^ TCH002 + | ^^ TCH002 7 | 8 | def f(x: pd.DataFrame): | diff --git a/crates/ruff/src/rules/flake8_unused_arguments/rules/mod.rs b/crates/ruff/src/rules/flake8_unused_arguments/rules/mod.rs index 5cf115d8bf..0c46f878f4 100644 --- a/crates/ruff/src/rules/flake8_unused_arguments/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_unused_arguments/rules/mod.rs @@ -1,6 +1,3 @@ -pub(crate) use unused_arguments::{ - unused_arguments, UnusedClassMethodArgument, UnusedFunctionArgument, UnusedLambdaArgument, - UnusedMethodArgument, UnusedStaticMethodArgument, -}; +pub(crate) use unused_arguments::*; mod unused_arguments; diff --git a/crates/ruff/src/rules/flake8_unused_arguments/rules/unused_arguments.rs b/crates/ruff/src/rules/flake8_unused_arguments/rules/unused_arguments.rs index f410b822cc..9c4d406474 100644 --- a/crates/ruff/src/rules/flake8_unused_arguments/rules/unused_arguments.rs +++ b/crates/ruff/src/rules/flake8_unused_arguments/rules/unused_arguments.rs @@ -7,11 +7,8 @@ use rustpython_parser::ast::{Arg, Arguments}; use ruff_diagnostics::DiagnosticKind; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::analyze::function_type; -use ruff_python_semantic::analyze::function_type::FunctionType; -use ruff_python_semantic::analyze::visibility; -use ruff_python_semantic::binding::Bindings; -use ruff_python_semantic::scope::{Scope, ScopeKind}; +use ruff_python_semantic::analyze::{function_type, visibility}; +use ruff_python_semantic::{Scope, ScopeKind, SemanticModel}; use crate::checkers::ast::Checker; use crate::registry::Rule; @@ -221,15 +218,16 @@ fn function( argumentable: Argumentable, args: &Arguments, values: &Scope, - bindings: &Bindings, + semantic: &SemanticModel, dummy_variable_rgx: &Regex, ignore_variadic_names: bool, ) -> Vec { let args = args .posonlyargs .iter() - .chain(args.args.iter()) - .chain(args.kwonlyargs.iter()) + .chain(&args.args) + .chain(&args.kwonlyargs) + .map(|arg_with_default| &arg_with_default.def) .chain( iter::once::>(args.vararg.as_deref()) .flatten() @@ -240,7 +238,7 @@ fn function( .flatten() .skip(usize::from(ignore_variadic_names)), ); - call(argumentable, args, values, bindings, dummy_variable_rgx) + call(argumentable, args, values, semantic, dummy_variable_rgx) } /// Check a method for unused arguments. @@ -248,16 +246,17 @@ fn method( argumentable: Argumentable, args: &Arguments, values: &Scope, - bindings: &Bindings, + semantic: &SemanticModel, dummy_variable_rgx: &Regex, ignore_variadic_names: bool, ) -> Vec { let args = args .posonlyargs .iter() - .chain(args.args.iter()) + .chain(&args.args) + .chain(&args.kwonlyargs) .skip(1) - .chain(args.kwonlyargs.iter()) + .map(|arg_with_default| &arg_with_default.def) .chain( iter::once::>(args.vararg.as_deref()) .flatten() @@ -268,21 +267,21 @@ fn method( .flatten() .skip(usize::from(ignore_variadic_names)), ); - call(argumentable, args, values, bindings, dummy_variable_rgx) + call(argumentable, args, values, semantic, dummy_variable_rgx) } fn call<'a>( argumentable: Argumentable, args: impl Iterator, values: &Scope, - bindings: &Bindings, + semantic: &SemanticModel, dummy_variable_rgx: &Regex, ) -> Vec { let mut diagnostics: Vec = vec![]; for arg in args { if let Some(binding) = values .get(arg.arg.as_str()) - .map(|binding_id| &bindings[binding_id]) + .map(|binding_id| semantic.binding(binding_id)) { if binding.kind.is_argument() && !binding.is_used() @@ -303,7 +302,6 @@ pub(crate) fn unused_arguments( checker: &Checker, parent: &Scope, scope: &Scope, - bindings: &Bindings, ) -> Vec { match &scope.kind { ScopeKind::Function(ast::StmtFunctionDef { @@ -321,22 +319,22 @@ pub(crate) fn unused_arguments( .. }) => { match function_type::classify( - checker.semantic_model(), - parent, name, decorator_list, + parent, + checker.semantic(), &checker.settings.pep8_naming.classmethod_decorators, &checker.settings.pep8_naming.staticmethod_decorators, ) { - FunctionType::Function => { + function_type::FunctionType::Function => { if checker.enabled(Argumentable::Function.rule_code()) - && !visibility::is_overload(checker.semantic_model(), decorator_list) + && !visibility::is_overload(decorator_list, checker.semantic()) { function( Argumentable::Function, args, scope, - bindings, + checker.semantic(), &checker.settings.dummy_variable_rgx, checker .settings @@ -347,22 +345,22 @@ pub(crate) fn unused_arguments( vec![] } } - FunctionType::Method => { + function_type::FunctionType::Method => { if checker.enabled(Argumentable::Method.rule_code()) && !helpers::is_empty(body) && (!visibility::is_magic(name) || visibility::is_init(name) || visibility::is_new(name) || visibility::is_call(name)) - && !visibility::is_abstract(checker.semantic_model(), decorator_list) - && !visibility::is_override(checker.semantic_model(), decorator_list) - && !visibility::is_overload(checker.semantic_model(), decorator_list) + && !visibility::is_abstract(decorator_list, checker.semantic()) + && !visibility::is_override(decorator_list, checker.semantic()) + && !visibility::is_overload(decorator_list, checker.semantic()) { method( Argumentable::Method, args, scope, - bindings, + checker.semantic(), &checker.settings.dummy_variable_rgx, checker .settings @@ -373,22 +371,22 @@ pub(crate) fn unused_arguments( vec![] } } - FunctionType::ClassMethod => { + function_type::FunctionType::ClassMethod => { if checker.enabled(Argumentable::ClassMethod.rule_code()) && !helpers::is_empty(body) && (!visibility::is_magic(name) || visibility::is_init(name) || visibility::is_new(name) || visibility::is_call(name)) - && !visibility::is_abstract(checker.semantic_model(), decorator_list) - && !visibility::is_override(checker.semantic_model(), decorator_list) - && !visibility::is_overload(checker.semantic_model(), decorator_list) + && !visibility::is_abstract(decorator_list, checker.semantic()) + && !visibility::is_override(decorator_list, checker.semantic()) + && !visibility::is_overload(decorator_list, checker.semantic()) { method( Argumentable::ClassMethod, args, scope, - bindings, + checker.semantic(), &checker.settings.dummy_variable_rgx, checker .settings @@ -399,22 +397,22 @@ pub(crate) fn unused_arguments( vec![] } } - FunctionType::StaticMethod => { + function_type::FunctionType::StaticMethod => { if checker.enabled(Argumentable::StaticMethod.rule_code()) && !helpers::is_empty(body) && (!visibility::is_magic(name) || visibility::is_init(name) || visibility::is_new(name) || visibility::is_call(name)) - && !visibility::is_abstract(checker.semantic_model(), decorator_list) - && !visibility::is_override(checker.semantic_model(), decorator_list) - && !visibility::is_overload(checker.semantic_model(), decorator_list) + && !visibility::is_abstract(decorator_list, checker.semantic()) + && !visibility::is_override(decorator_list, checker.semantic()) + && !visibility::is_overload(decorator_list, checker.semantic()) { function( Argumentable::StaticMethod, args, scope, - bindings, + checker.semantic(), &checker.settings.dummy_variable_rgx, checker .settings @@ -433,7 +431,7 @@ pub(crate) fn unused_arguments( Argumentable::Lambda, args, scope, - bindings, + checker.semantic(), &checker.settings.dummy_variable_rgx, checker .settings diff --git a/crates/ruff/src/rules/flake8_use_pathlib/rules/mod.rs b/crates/ruff/src/rules/flake8_use_pathlib/rules/mod.rs index 33eac782e4..dcf45f615c 100644 --- a/crates/ruff/src/rules/flake8_use_pathlib/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_use_pathlib/rules/mod.rs @@ -1,3 +1,3 @@ -pub(crate) use replaceable_by_pathlib::replaceable_by_pathlib; +pub(crate) use replaceable_by_pathlib::*; mod replaceable_by_pathlib; diff --git a/crates/ruff/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs b/crates/ruff/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs index 5862bd427c..98f25b193d 100644 --- a/crates/ruff/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs +++ b/crates/ruff/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs @@ -15,7 +15,7 @@ use crate::settings::types::PythonVersion; pub(crate) fn replaceable_by_pathlib(checker: &mut Checker, expr: &Expr) { if let Some(diagnostic_kind) = checker - .semantic_model() + .semantic() .resolve_call_path(expr) .and_then(|call_path| match call_path.as_slice() { ["os", "path", "abspath"] => Some(OsPathAbspath.into()), diff --git a/crates/ruff/src/rules/flynt/rules/mod.rs b/crates/ruff/src/rules/flynt/rules/mod.rs index ce0b9462d8..b57de8364f 100644 --- a/crates/ruff/src/rules/flynt/rules/mod.rs +++ b/crates/ruff/src/rules/flynt/rules/mod.rs @@ -1,3 +1,3 @@ -pub(crate) use static_join_to_fstring::{static_join_to_fstring, StaticJoinToFString}; +pub(crate) use static_join_to_fstring::*; mod static_join_to_fstring; diff --git a/crates/ruff/src/rules/flynt/rules/static_join_to_fstring.rs b/crates/ruff/src/rules/flynt/rules/static_join_to_fstring.rs index 2faab59911..cd2750006b 100644 --- a/crates/ruff/src/rules/flynt/rules/static_join_to_fstring.rs +++ b/crates/ruff/src/rules/flynt/rules/static_join_to_fstring.rs @@ -88,11 +88,7 @@ fn build_fstring(joiner: &str, joinees: &[Expr]) -> Option { } pub(crate) fn static_join_to_fstring(checker: &mut Checker, expr: &Expr, joiner: &str) { - let Expr::Call(ast::ExprCall { - args, - keywords, - .. - }) = expr else { + let Expr::Call(ast::ExprCall { args, keywords, .. }) = expr else { return; }; @@ -111,7 +107,9 @@ pub(crate) fn static_join_to_fstring(checker: &mut Checker, expr: &Expr, joiner: // Try to build the fstring (internally checks whether e.g. the elements are // convertible to f-string parts). - let Some(new_expr) = build_fstring(joiner, joinees) else { return }; + let Some(new_expr) = build_fstring(joiner, joinees) else { + return; + }; let contents = checker.generator().expr(&new_expr); @@ -122,8 +120,7 @@ pub(crate) fn static_join_to_fstring(checker: &mut Checker, expr: &Expr, joiner: expr.range(), ); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( contents, expr.range(), ))); diff --git a/crates/ruff/src/rules/isort/block.rs b/crates/ruff/src/rules/isort/block.rs index f01511a154..e0eea8ae35 100644 --- a/crates/ruff/src/rules/isort/block.rs +++ b/crates/ruff/src/rules/isort/block.rs @@ -1,11 +1,15 @@ use ruff_text_size::{TextRange, TextSize}; -use rustpython_parser::ast::{self, Excepthandler, MatchCase, Ranged, Stmt}; +use rustpython_parser::ast::{self, ExceptHandler, MatchCase, Ranged, Stmt}; +use std::iter::Peekable; +use std::slice; use ruff_python_ast::source_code::Locator; use ruff_python_ast::statement_visitor::StatementVisitor; use crate::directives::IsortDirectives; +use crate::jupyter::Notebook; use crate::rules::isort::helpers; +use crate::source_kind::SourceKind; /// A block of imports within a Python module. #[derive(Debug, Default)] @@ -28,7 +32,8 @@ pub(crate) struct BlockBuilder<'a> { locator: &'a Locator<'a>, is_stub: bool, blocks: Vec>, - splits: &'a [TextSize], + splits: Peekable>, + cell_offsets: Option>>, exclusions: &'a [TextRange], nested: bool, } @@ -38,14 +43,19 @@ impl<'a> BlockBuilder<'a> { locator: &'a Locator<'a>, directives: &'a IsortDirectives, is_stub: bool, + source_kind: Option<&'a SourceKind>, ) -> Self { Self { locator, is_stub, blocks: vec![Block::default()], - splits: &directives.splits, + splits: directives.splits.iter().peekable(), exclusions: &directives.exclusions, nested: false, + cell_offsets: source_kind + .and_then(SourceKind::notebook) + .map(Notebook::cell_offsets) + .map(|offsets| offsets.iter().peekable()), } } @@ -119,29 +129,58 @@ where 'b: 'a, { fn visit_stmt(&mut self, stmt: &'b Stmt) { - // Track manual splits. - for (index, split) in self.splits.iter().enumerate() { - if stmt.start() >= *split { - self.finalize(self.trailer_for(stmt)); - self.splits = &self.splits[index + 1..]; - } else { - break; + // Track manual splits (e.g., `# isort: split`). + if self + .splits + .next_if(|split| stmt.start() >= **split) + .is_some() + { + // Skip any other splits that occur before the current statement, to support, e.g.: + // ```python + // # isort: split + // # isort: split + // import foo + // ``` + while self + .splits + .peek() + .map_or(false, |split| stmt.start() >= **split) + { + self.splits.next(); } + + self.finalize(self.trailer_for(stmt)); } - // Test if the statement is in an excluded range - let mut is_excluded = false; - for (index, exclusion) in self.exclusions.iter().enumerate() { - if exclusion.end() < stmt.start() { - self.exclusions = &self.exclusions[index + 1..]; - } else { - is_excluded = exclusion.contains(stmt.start()); - break; + // Track Jupyter notebook cell offsets as splits. This will make sure + // that each cell is considered as an individual block to organize the + // imports in. Thus, not creating an edit which spans across multiple + // cells. + if let Some(cell_offsets) = self.cell_offsets.as_mut() { + if cell_offsets + .next_if(|cell_offset| stmt.start() >= **cell_offset) + .is_some() + { + // Skip any other cell offsets that occur before the current statement (e.g., in + // the case of multiple empty cells). + while cell_offsets + .peek() + .map_or(false, |split| stmt.start() >= **split) + { + cell_offsets.next(); + } + + self.finalize(None); } } // Track imports. - if matches!(stmt, Stmt::Import(_) | Stmt::ImportFrom(_)) && !is_excluded { + if matches!(stmt, Stmt::Import(_) | Stmt::ImportFrom(_)) + && !self + .exclusions + .iter() + .any(|exclusion| exclusion.contains(stmt.start())) + { self.track_import(stmt); } else { self.finalize(self.trailer_for(stmt)); @@ -244,8 +283,8 @@ where finalbody, range: _, }) => { - for excepthandler in handlers { - self.visit_excepthandler(excepthandler); + for except_handler in handlers { + self.visit_except_handler(except_handler); } for stmt in body { @@ -268,12 +307,12 @@ where self.nested = prev_nested; } - fn visit_excepthandler(&mut self, excepthandler: &'b Excepthandler) { + fn visit_except_handler(&mut self, except_handler: &'b ExceptHandler) { let prev_nested = self.nested; self.nested = true; - let Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { body, .. }) = - excepthandler; + let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { body, .. }) = + except_handler; for stmt in body { self.visit_stmt(stmt); } diff --git a/crates/ruff/src/rules/isort/categorize.rs b/crates/ruff/src/rules/isort/categorize.rs index ac6348476d..3ed3d8d1a3 100644 --- a/crates/ruff/src/rules/isort/categorize.rs +++ b/crates/ruff/src/rules/isort/categorize.rs @@ -1,14 +1,15 @@ use std::collections::BTreeMap; +use std::hash::BuildHasherDefault; use std::path::{Path, PathBuf}; use std::{fs, iter}; use log::debug; -use rustc_hash::FxHashMap; +use rustc_hash::{FxHashMap, FxHashSet}; use serde::{Deserialize, Serialize}; use strum_macros::EnumIter; use ruff_macros::CacheKey; -use ruff_python_stdlib::sys::KNOWN_STANDARD_LIBRARY; +use ruff_python_stdlib::sys::is_known_standard_library; use crate::settings::types::PythonVersion; use crate::warn_user_once; @@ -82,11 +83,7 @@ pub(crate) fn categorize<'a>( (&ImportSection::Known(ImportType::Future), Reason::Future) } else if let Some((import_type, reason)) = known_modules.categorize(module_name) { (import_type, reason) - } else if KNOWN_STANDARD_LIBRARY - .get(&target_version.as_tuple()) - .unwrap() - .contains(module_base) - { + } else if is_known_standard_library(target_version.minor(), module_base) { ( &ImportSection::Known(ImportType::StandardLibrary), Reason::KnownStandardLibrary, @@ -214,20 +211,20 @@ pub(crate) fn categorize_imports<'a>( #[derive(Debug, Default, CacheKey)] pub struct KnownModules { /// A map of known modules to their section. - known: FxHashMap, + known: Vec<(glob::Pattern, ImportSection)>, /// Whether any of the known modules are submodules (e.g., `foo.bar`, as opposed to `foo`). has_submodules: bool, } impl KnownModules { pub(crate) fn new( - first_party: Vec, - third_party: Vec, - local_folder: Vec, - standard_library: Vec, - user_defined: FxHashMap>, + first_party: Vec, + third_party: Vec, + local_folder: Vec, + standard_library: Vec, + user_defined: FxHashMap>, ) -> Self { - let modules = user_defined + let known: Vec<(glob::Pattern, ImportSection)> = user_defined .into_iter() .flat_map(|(section, modules)| { modules @@ -253,19 +250,23 @@ impl KnownModules { standard_library .into_iter() .map(|module| (module, ImportSection::Known(ImportType::StandardLibrary))), - ); + ) + .collect(); - let mut known = FxHashMap::with_capacity_and_hasher( - modules.size_hint().0, - std::hash::BuildHasherDefault::default(), - ); - modules.for_each(|(module, section)| { - if known.insert(module, section).is_some() { - warn_user_once!("One or more modules are part of multiple import sections."); + // Warn in the case of duplicate modules. + let mut seen = + FxHashSet::with_capacity_and_hasher(known.len(), BuildHasherDefault::default()); + for (module, _) in &known { + if !seen.insert(module) { + warn_user_once!("One or more modules are part of multiple import sections, including: `{module}`"); + break; } - }); + } - let has_submodules = known.keys().any(|module| module.contains('.')); + // Check if any of the known modules are submodules. + let has_submodules = known + .iter() + .any(|(module, _)| module.as_str().contains('.')); Self { known, @@ -300,31 +301,37 @@ impl KnownModules { } fn categorize_submodule(&self, submodule: &str) -> Option<(&ImportSection, Reason)> { - if let Some(section) = self.known.get(submodule) { - let reason = match section { - ImportSection::UserDefined(_) => Reason::UserDefinedSection, - ImportSection::Known(ImportType::FirstParty) => Reason::KnownFirstParty, - ImportSection::Known(ImportType::ThirdParty) => Reason::KnownThirdParty, - ImportSection::Known(ImportType::LocalFolder) => Reason::KnownLocalFolder, - ImportSection::Known(ImportType::StandardLibrary) => Reason::ExtraStandardLibrary, - ImportSection::Known(ImportType::Future) => { - unreachable!("__future__ imports are not known") - } - }; - Some((section, reason)) - } else { - None - } + let section = self.known.iter().find_map(|(pattern, section)| { + if pattern.matches(submodule) { + Some(section) + } else { + None + } + })?; + let reason = match section { + ImportSection::UserDefined(_) => Reason::UserDefinedSection, + ImportSection::Known(ImportType::FirstParty) => Reason::KnownFirstParty, + ImportSection::Known(ImportType::ThirdParty) => Reason::KnownThirdParty, + ImportSection::Known(ImportType::LocalFolder) => Reason::KnownLocalFolder, + ImportSection::Known(ImportType::StandardLibrary) => Reason::ExtraStandardLibrary, + ImportSection::Known(ImportType::Future) => { + unreachable!("__future__ imports are not known") + } + }; + Some((section, reason)) } /// Return the list of modules that are known to be of a given type. - pub(crate) fn modules_for_known_type(&self, import_type: ImportType) -> Vec { + pub(crate) fn modules_for_known_type( + &self, + import_type: ImportType, + ) -> impl Iterator { self.known .iter() - .filter_map(|(module, known_section)| { + .filter_map(move |(module, known_section)| { if let ImportSection::Known(section) = known_section { if *section == import_type { - Some(module.clone()) + Some(module) } else { None } @@ -332,18 +339,17 @@ impl KnownModules { None } }) - .collect() } /// Return the list of user-defined modules, indexed by section. - pub(crate) fn user_defined(&self) -> FxHashMap> { - let mut user_defined: FxHashMap> = FxHashMap::default(); + pub(crate) fn user_defined(&self) -> FxHashMap<&str, Vec<&glob::Pattern>> { + let mut user_defined: FxHashMap<&str, Vec<&glob::Pattern>> = FxHashMap::default(); for (module, section) in &self.known { if let ImportSection::UserDefined(section_name) = section { user_defined - .entry(section_name.clone()) + .entry(section_name.as_str()) .or_default() - .push(module.clone()); + .push(module); } } user_defined diff --git a/crates/ruff/src/rules/isort/mod.rs b/crates/ruff/src/rules/isort/mod.rs index 251b69f6e4..bcc2f1d9a9 100644 --- a/crates/ruff/src/rules/isort/mod.rs +++ b/crates/ruff/src/rules/isort/mod.rs @@ -74,6 +74,7 @@ pub(crate) fn format_imports( combine_as_imports: bool, force_single_line: bool, force_sort_within_sections: bool, + case_sensitive: bool, force_wrap_aliases: bool, force_to_top: &BTreeSet, known_modules: &KnownModules, @@ -114,6 +115,7 @@ pub(crate) fn format_imports( src, package, force_sort_within_sections, + case_sensitive, force_wrap_aliases, force_to_top, known_modules, @@ -171,6 +173,7 @@ fn format_import_block( src: &[PathBuf], package: Option<&Path>, force_sort_within_sections: bool, + case_sensitive: bool, force_wrap_aliases: bool, force_to_top: &BTreeSet, known_modules: &KnownModules, @@ -206,6 +209,7 @@ fn format_import_block( let imports = order_imports( import_block, order_by_type, + case_sensitive, relative_imports_order, classes, constants, @@ -222,7 +226,13 @@ fn format_import_block( .collect::>(); if force_sort_within_sections { imports.sort_by(|import1, import2| { - cmp_either_import(import1, import2, relative_imports_order, force_to_top) + cmp_either_import( + import1, + import2, + relative_imports_order, + force_to_top, + case_sensitive, + ) }); }; imports @@ -359,6 +369,10 @@ mod tests { Ok(()) } + fn pattern(pattern: &str) -> glob::Pattern { + glob::Pattern::new(pattern).unwrap() + } + #[test_case(Path::new("separate_subpackage_first_and_third_party_imports.py"))] fn separate_modules(path: &Path) -> Result<()> { let snapshot = format!("1_{}", path.to_string_lossy()); @@ -367,8 +381,32 @@ mod tests { &Settings { isort: super::settings::Settings { known_modules: KnownModules::new( - vec!["foo.bar".to_string(), "baz".to_string()], - vec!["foo".to_string(), "__future__".to_string()], + vec![pattern("foo.bar"), pattern("baz")], + vec![pattern("foo"), pattern("__future__")], + vec![], + vec![], + FxHashMap::default(), + ), + ..super::settings::Settings::default() + }, + src: vec![test_resource_path("fixtures/isort")], + ..Settings::for_rule(Rule::UnsortedImports) + }, + )?; + assert_messages!(snapshot, diagnostics); + Ok(()) + } + + #[test_case(Path::new("separate_subpackage_first_and_third_party_imports.py"))] + fn separate_modules_glob(path: &Path) -> Result<()> { + let snapshot = format!("glob_1_{}", path.to_string_lossy()); + let diagnostics = test_path( + Path::new("isort").join(path).as_path(), + &Settings { + isort: super::settings::Settings { + known_modules: KnownModules::new( + vec![pattern("foo.*"), pattern("baz")], + vec![pattern("foo"), pattern("__future__")], vec![], vec![], FxHashMap::default(), @@ -391,8 +429,8 @@ mod tests { &Settings { isort: super::settings::Settings { known_modules: KnownModules::new( - vec!["foo".to_string()], - vec!["foo.bar".to_string()], + vec![pattern("foo")], + vec![pattern("foo.bar")], vec![], vec![], FxHashMap::default(), @@ -435,7 +473,7 @@ mod tests { known_modules: KnownModules::new( vec![], vec![], - vec!["ruff".to_string()], + vec![pattern("ruff")], vec![], FxHashMap::default(), ), @@ -449,6 +487,24 @@ mod tests { Ok(()) } + #[test_case(Path::new("case_sensitive.py"))] + fn case_sensitive(path: &Path) -> Result<()> { + let snapshot = format!("case_sensitive_{}", path.to_string_lossy()); + let diagnostics = test_path( + Path::new("isort").join(path).as_path(), + &Settings { + isort: super::settings::Settings { + case_sensitive: true, + ..super::settings::Settings::default() + }, + src: vec![test_resource_path("fixtures/isort")], + ..Settings::for_rule(Rule::UnsortedImports) + }, + )?; + assert_messages!(snapshot, diagnostics); + Ok(()) + } + #[test_case(Path::new("force_to_top.py"))] fn force_to_top(path: &Path) -> Result<()> { let snapshot = format!("force_to_top_{}", path.to_string_lossy()); @@ -933,7 +989,7 @@ mod tests { vec![], vec![], vec![], - FxHashMap::from_iter([("django".to_string(), vec!["django".to_string()])]), + FxHashMap::from_iter([("django".to_string(), vec![pattern("django")])]), ), section_order: vec![ ImportSection::Known(ImportType::Future), @@ -962,11 +1018,11 @@ mod tests { src: vec![test_resource_path("fixtures/isort")], isort: super::settings::Settings { known_modules: KnownModules::new( - vec!["library".to_string()], + vec![pattern("library")], vec![], vec![], vec![], - FxHashMap::from_iter([("django".to_string(), vec!["django".to_string()])]), + FxHashMap::from_iter([("django".to_string(), vec![pattern("django")])]), ), section_order: vec![ ImportSection::Known(ImportType::Future), diff --git a/crates/ruff/src/rules/isort/order.rs b/crates/ruff/src/rules/isort/order.rs index 1867ebf0de..a2210ed43a 100644 --- a/crates/ruff/src/rules/isort/order.rs +++ b/crates/ruff/src/rules/isort/order.rs @@ -9,9 +9,11 @@ use super::settings::RelativeImportsOrder; use super::sorting::{cmp_import_from, cmp_members, cmp_modules}; use super::types::{AliasData, CommentSet, ImportBlock, OrderedImportBlock}; +#[allow(clippy::too_many_arguments)] pub(crate) fn order_imports<'a>( block: ImportBlock<'a>, order_by_type: bool, + case_sensitive: bool, relative_imports_order: RelativeImportsOrder, classes: &'a BTreeSet, constants: &'a BTreeSet, @@ -25,7 +27,9 @@ pub(crate) fn order_imports<'a>( block .import .into_iter() - .sorted_by(|(alias1, _), (alias2, _)| cmp_modules(alias1, alias2, force_to_top)), + .sorted_by(|(alias1, _), (alias2, _)| { + cmp_modules(alias1, alias2, force_to_top, case_sensitive) + }), ); // Sort `Stmt::ImportFrom`. @@ -43,7 +47,7 @@ pub(crate) fn order_imports<'a>( ) .chain( // Include all star imports. - block.import_from_star.into_iter(), + block.import_from_star, ) .map( |( @@ -70,6 +74,7 @@ pub(crate) fn order_imports<'a>( constants, variables, force_to_top, + case_sensitive, ) }) .collect::>(), @@ -83,6 +88,7 @@ pub(crate) fn order_imports<'a>( import_from2, relative_imports_order, force_to_top, + case_sensitive, ) .then_with(|| match (aliases1.first(), aliases2.first()) { (None, None) => Ordering::Equal, @@ -96,6 +102,7 @@ pub(crate) fn order_imports<'a>( constants, variables, force_to_top, + case_sensitive, ), }) }, diff --git a/crates/ruff/src/rules/isort/rules/add_required_imports.rs b/crates/ruff/src/rules/isort/rules/add_required_imports.rs index 1d05832f1f..9abcb3971c 100644 --- a/crates/ruff/src/rules/isort/rules/add_required_imports.rs +++ b/crates/ruff/src/rules/isort/rules/add_required_imports.rs @@ -56,10 +56,7 @@ impl AlwaysAutofixableViolation for MissingRequiredImport { fn includes_import(stmt: &Stmt, target: &AnyImport) -> bool { match target { AnyImport::Import(target) => { - let Stmt::Import(ast::StmtImport { - names, - range: _, - }) = &stmt else { + let Stmt::Import(ast::StmtImport { names, range: _ }) = &stmt else { return false; }; names.iter().any(|alias| { @@ -71,8 +68,9 @@ fn includes_import(stmt: &Stmt, target: &AnyImport) -> bool { module, names, level, - range: _, - }) = &stmt else { + range: _, + }) = &stmt + else { return false; }; module.as_deref() == target.module @@ -118,8 +116,7 @@ fn add_required_import( TextRange::default(), ); if settings.rules.should_fix(Rule::MissingRequiredImport) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified( + diagnostic.set_fix(Fix::automatic( Importer::new(python_ast, locator, stylist) .add_import(required_import, TextSize::default()), )); diff --git a/crates/ruff/src/rules/isort/rules/mod.rs b/crates/ruff/src/rules/isort/rules/mod.rs index 0184926441..4db286f410 100644 --- a/crates/ruff/src/rules/isort/rules/mod.rs +++ b/crates/ruff/src/rules/isort/rules/mod.rs @@ -1,5 +1,5 @@ -pub(crate) use add_required_imports::{add_required_imports, MissingRequiredImport}; -pub(crate) use organize_imports::{organize_imports, UnsortedImports}; +pub(crate) use add_required_imports::*; +pub(crate) use organize_imports::*; pub(crate) mod add_required_imports; pub(crate) mod organize_imports; diff --git a/crates/ruff/src/rules/isort/rules/organize_imports.rs b/crates/ruff/src/rules/isort/rules/organize_imports.rs index d7a3dbe549..89832f9431 100644 --- a/crates/ruff/src/rules/isort/rules/organize_imports.rs +++ b/crates/ruff/src/rules/isort/rules/organize_imports.rs @@ -127,6 +127,7 @@ pub(crate) fn organize_imports( settings.isort.combine_as_imports, settings.isort.force_single_line, settings.isort.force_sort_within_sections, + settings.isort.case_sensitive, settings.isort.force_wrap_aliases, &settings.isort.force_to_top, &settings.isort.known_modules, diff --git a/crates/ruff/src/rules/isort/settings.rs b/crates/ruff/src/rules/isort/settings.rs index 262829b73d..8e9648df1b 100644 --- a/crates/ruff/src/rules/isort/settings.rs +++ b/crates/ruff/src/rules/isort/settings.rs @@ -1,6 +1,8 @@ //! Settings for the `isort` plugin. use std::collections::BTreeSet; +use std::error::Error; +use std::fmt; use std::hash::BuildHasherDefault; use rustc_hash::{FxHashMap, FxHashSet}; @@ -11,6 +13,7 @@ use ruff_macros::{CacheKey, CombineOptions, ConfigurationOptions}; use crate::rules::isort::categorize::KnownModules; use crate::rules::isort::ImportType; +use crate::settings::types::IdentifierPattern; use crate::warn_user_once; use super::categorize::ImportSection; @@ -127,6 +130,15 @@ pub struct Options { /// imports (like `from itertools import groupby`). Instead, sort the /// imports by module, independent of import style. pub force_sort_within_sections: Option, + #[option( + default = r#"false"#, + value_type = "bool", + example = r#" + case-sensitive = true + "# + )] + /// Sort imports taking into account case sensitivity. + pub case_sensitive: Option, #[option( default = r#"[]"#, value_type = "list[str]", @@ -145,6 +157,9 @@ pub struct Options { )] /// A list of modules to consider first-party, regardless of whether they /// can be identified as such via introspection of the local filesystem. + /// + /// Supports glob patterns. For more information on the glob syntax, refer + /// to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax). pub known_first_party: Option>, #[option( default = r#"[]"#, @@ -155,6 +170,9 @@ pub struct Options { )] /// A list of modules to consider third-party, regardless of whether they /// can be identified as such via introspection of the local filesystem. + /// + /// Supports glob patterns. For more information on the glob syntax, refer + /// to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax). pub known_third_party: Option>, #[option( default = r#"[]"#, @@ -165,6 +183,9 @@ pub struct Options { )] /// A list of modules to consider being a local folder. /// Generally, this is reserved for relative imports (`from . import module`). + /// + /// Supports glob patterns. For more information on the glob syntax, refer + /// to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax). pub known_local_folder: Option>, #[option( default = r#"[]"#, @@ -175,6 +196,9 @@ pub struct Options { )] /// A list of modules to consider standard-library, in addition to those /// known to Ruff in advance. + /// + /// Supports glob patterns. For more information on the glob syntax, refer + /// to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax). pub extra_standard_library: Option>, #[option( default = r#"furthest-to-closest"#, @@ -303,6 +327,7 @@ pub struct Settings { pub combine_as_imports: bool, pub force_single_line: bool, pub force_sort_within_sections: bool, + pub case_sensitive: bool, pub force_wrap_aliases: bool, pub force_to_top: BTreeSet, pub known_modules: KnownModules, @@ -327,6 +352,7 @@ impl Default for Settings { combine_as_imports: false, force_single_line: false, force_sort_within_sections: false, + case_sensitive: false, force_wrap_aliases: false, force_to_top: BTreeSet::new(), known_modules: KnownModules::default(), @@ -346,21 +372,63 @@ impl Default for Settings { } } -impl From for Settings { - fn from(options: Options) -> Self { +impl TryFrom for Settings { + type Error = SettingsError; + + fn try_from(options: Options) -> Result { // Extract any configuration options that deal with user-defined sections. let mut section_order: Vec<_> = options .section_order .unwrap_or_else(|| ImportType::iter().map(ImportSection::Known).collect()); - let known_first_party = options.known_first_party.unwrap_or_default(); - let known_third_party = options.known_third_party.unwrap_or_default(); - let known_local_folder = options.known_local_folder.unwrap_or_default(); - let extra_standard_library = options.extra_standard_library.unwrap_or_default(); + let known_first_party = options + .known_first_party + .map(|names| { + names + .into_iter() + .map(|name| IdentifierPattern::new(&name)) + .collect() + }) + .transpose() + .map_err(SettingsError::InvalidKnownFirstParty)? + .unwrap_or_default(); + let known_third_party = options + .known_third_party + .map(|names| { + names + .into_iter() + .map(|name| IdentifierPattern::new(&name)) + .collect() + }) + .transpose() + .map_err(SettingsError::InvalidKnownThirdParty)? + .unwrap_or_default(); + let known_local_folder = options + .known_local_folder + .map(|names| { + names + .into_iter() + .map(|name| IdentifierPattern::new(&name)) + .collect() + }) + .transpose() + .map_err(SettingsError::InvalidKnownLocalFolder)? + .unwrap_or_default(); + let extra_standard_library = options + .extra_standard_library + .map(|names| { + names + .into_iter() + .map(|name| IdentifierPattern::new(&name)) + .collect() + }) + .transpose() + .map_err(SettingsError::InvalidExtraStandardLibrary)? + .unwrap_or_default(); let no_lines_before = options.no_lines_before.unwrap_or_default(); let sections = options.sections.unwrap_or_default(); // Verify that `sections` doesn't contain any built-in sections. - let sections: FxHashMap> = sections + let sections: FxHashMap> = sections .into_iter() .filter_map(|(section, modules)| match section { ImportSection::Known(section) => { @@ -369,7 +437,17 @@ impl From for Settings { } ImportSection::UserDefined(section) => Some((section, modules)), }) - .collect(); + .map(|(section, modules)| { + let modules = modules + .into_iter() + .map(|module| { + IdentifierPattern::new(&module) + .map_err(SettingsError::InvalidUserDefinedSection) + }) + .collect::, Self::Error>>()?; + Ok((section, modules)) + }) + .collect::>()?; // Verify that `section_order` doesn't contain any duplicates. let mut seen = @@ -424,11 +502,12 @@ impl From for Settings { } } - Self { + Ok(Self { required_imports: BTreeSet::from_iter(options.required_imports.unwrap_or_default()), combine_as_imports: options.combine_as_imports.unwrap_or(false), force_single_line: options.force_single_line.unwrap_or(false), force_sort_within_sections: options.force_sort_within_sections.unwrap_or(false), + case_sensitive: options.case_sensitive.unwrap_or(false), force_wrap_aliases: options.force_wrap_aliases.unwrap_or(false), force_to_top: BTreeSet::from_iter(options.force_to_top.unwrap_or_default()), known_modules: KnownModules::new( @@ -452,6 +531,50 @@ impl From for Settings { lines_between_types: options.lines_between_types.unwrap_or_default(), forced_separate: Vec::from_iter(options.forced_separate.unwrap_or_default()), section_order, + }) + } +} + +/// Error returned by the [`TryFrom`] implementation of [`Settings`]. +#[derive(Debug)] +pub enum SettingsError { + InvalidKnownFirstParty(glob::PatternError), + InvalidKnownThirdParty(glob::PatternError), + InvalidKnownLocalFolder(glob::PatternError), + InvalidExtraStandardLibrary(glob::PatternError), + InvalidUserDefinedSection(glob::PatternError), +} + +impl fmt::Display for SettingsError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + SettingsError::InvalidKnownThirdParty(err) => { + write!(f, "invalid known third-party pattern: {err}") + } + SettingsError::InvalidKnownFirstParty(err) => { + write!(f, "invalid known first-party pattern: {err}") + } + SettingsError::InvalidKnownLocalFolder(err) => { + write!(f, "invalid known local folder pattern: {err}") + } + SettingsError::InvalidExtraStandardLibrary(err) => { + write!(f, "invalid extra standard library pattern: {err}") + } + SettingsError::InvalidUserDefinedSection(err) => { + write!(f, "invalid user-defined section pattern: {err}") + } + } + } +} + +impl Error for SettingsError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + SettingsError::InvalidKnownThirdParty(err) => Some(err), + SettingsError::InvalidKnownFirstParty(err) => Some(err), + SettingsError::InvalidKnownLocalFolder(err) => Some(err), + SettingsError::InvalidExtraStandardLibrary(err) => Some(err), + SettingsError::InvalidUserDefinedSection(err) => Some(err), } } } @@ -464,26 +587,35 @@ impl From for Options { extra_standard_library: Some( settings .known_modules - .modules_for_known_type(ImportType::StandardLibrary), + .modules_for_known_type(ImportType::StandardLibrary) + .map(ToString::to_string) + .collect(), ), force_single_line: Some(settings.force_single_line), force_sort_within_sections: Some(settings.force_sort_within_sections), + case_sensitive: Some(settings.case_sensitive), force_wrap_aliases: Some(settings.force_wrap_aliases), force_to_top: Some(settings.force_to_top.into_iter().collect()), known_first_party: Some( settings .known_modules - .modules_for_known_type(ImportType::FirstParty), + .modules_for_known_type(ImportType::FirstParty) + .map(ToString::to_string) + .collect(), ), known_third_party: Some( settings .known_modules - .modules_for_known_type(ImportType::ThirdParty), + .modules_for_known_type(ImportType::ThirdParty) + .map(ToString::to_string) + .collect(), ), known_local_folder: Some( settings .known_modules - .modules_for_known_type(ImportType::LocalFolder), + .modules_for_known_type(ImportType::LocalFolder) + .map(ToString::to_string) + .collect(), ), order_by_type: Some(settings.order_by_type), relative_imports_order: Some(settings.relative_imports_order), @@ -502,7 +634,12 @@ impl From for Options { .known_modules .user_defined() .into_iter() - .map(|(section, modules)| (ImportSection::UserDefined(section), modules)) + .map(|(section, modules)| { + ( + ImportSection::UserDefined(section.to_string()), + modules.into_iter().map(ToString::to_string).collect(), + ) + }) .collect(), ), } diff --git a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__case_sensitive_case_sensitive.py.snap b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__case_sensitive_case_sensitive.py.snap new file mode 100644 index 0000000000..c6fe266802 --- /dev/null +++ b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__case_sensitive_case_sensitive.py.snap @@ -0,0 +1,33 @@ +--- +source: crates/ruff/src/rules/isort/mod.rs +--- +case_sensitive.py:1:1: I001 [*] Import block is un-sorted or un-formatted + | +1 | / import A +2 | | import B +3 | | import b +4 | | import C +5 | | import d +6 | | import E +7 | | import f +8 | | from g import a, B, c +9 | | from h import A, b, C + | + = help: Organize imports + +ℹ Fix +1 1 | import A +2 2 | import B + 3 |+import C + 4 |+import E +3 5 | import b +4 |-import C +5 6 | import d +6 |-import E +7 7 | import f +8 |-from g import a, B, c +9 |-from h import A, b, C + 8 |+from g import B, a, c + 9 |+from h import A, C, b + + diff --git a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__combined_required_imports_docstring.py.snap b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__combined_required_imports_docstring.py.snap index 06f61e148e..c5a0bd69f0 100644 --- a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__combined_required_imports_docstring.py.snap +++ b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__combined_required_imports_docstring.py.snap @@ -10,7 +10,7 @@ docstring.py:1:1: I002 [*] Missing required import: `from __future__ import anno | = help: Insert required import: `from future import annotations` -ℹ Suggested fix +ℹ Fix 1 1 | """Hello, world!""" 2 |+from __future__ import annotations 2 3 | @@ -25,7 +25,7 @@ docstring.py:1:1: I002 [*] Missing required import: `from __future__ import gene | = help: Insert required import: `from future import generator_stop` -ℹ Suggested fix +ℹ Fix 1 1 | """Hello, world!""" 2 |+from __future__ import generator_stop 2 3 | diff --git a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__glob_1_separate_subpackage_first_and_third_party_imports.py.snap b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__glob_1_separate_subpackage_first_and_third_party_imports.py.snap new file mode 100644 index 0000000000..41e6eb47f6 --- /dev/null +++ b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__glob_1_separate_subpackage_first_and_third_party_imports.py.snap @@ -0,0 +1,33 @@ +--- +source: crates/ruff/src/rules/isort/mod.rs +--- +separate_subpackage_first_and_third_party_imports.py:1:1: I001 [*] Import block is un-sorted or un-formatted + | +1 | / import sys +2 | | import baz +3 | | from foo import bar, baz +4 | | from foo.bar import blah, blub +5 | | from foo.bar.baz import something +6 | | import foo +7 | | import foo.bar +8 | | import foo.bar.baz + | + = help: Organize imports + +ℹ Fix +1 1 | import sys + 2 |+ + 3 |+import foo + 4 |+from foo import bar, baz + 5 |+ +2 6 | import baz +3 |-from foo import bar, baz + 7 |+import foo.bar + 8 |+import foo.bar.baz +4 9 | from foo.bar import blah, blub +5 10 | from foo.bar.baz import something +6 |-import foo +7 |-import foo.bar +8 |-import foo.bar.baz + + diff --git a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_comment.py.snap b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_comment.py.snap index 9606d7a44e..ad5b85c1d1 100644 --- a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_comment.py.snap +++ b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_comment.py.snap @@ -10,7 +10,7 @@ comment.py:1:1: I002 [*] Missing required import: `from __future__ import annota | = help: Insert required import: `from future import annotations` -ℹ Suggested fix +ℹ Fix 1 1 | #!/usr/bin/env python3 2 |+from __future__ import annotations 2 3 | diff --git a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_docstring.py.snap b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_docstring.py.snap index 4bad7ae0ec..ae57622574 100644 --- a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_docstring.py.snap +++ b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_docstring.py.snap @@ -10,7 +10,7 @@ docstring.py:1:1: I002 [*] Missing required import: `from __future__ import anno | = help: Insert required import: `from future import annotations` -ℹ Suggested fix +ℹ Fix 1 1 | """Hello, world!""" 2 |+from __future__ import annotations 2 3 | diff --git a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_docstring_with_continuation.py.snap b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_docstring_with_continuation.py.snap index 2e4dfc38c9..47eff34dad 100644 --- a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_docstring_with_continuation.py.snap +++ b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_docstring_with_continuation.py.snap @@ -9,7 +9,7 @@ docstring_with_continuation.py:1:1: I002 [*] Missing required import: `from __fu | = help: Insert required import: `from future import annotations` -ℹ Suggested fix +ℹ Fix 1 |-"""Hello, world!"""; x = \ 1 |+"""Hello, world!"""; from __future__ import annotations; x = \ 2 2 | 1; y = 2 diff --git a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_docstring_with_semicolon.py.snap b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_docstring_with_semicolon.py.snap index 0a084316d8..4e43866591 100644 --- a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_docstring_with_semicolon.py.snap +++ b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_docstring_with_semicolon.py.snap @@ -8,7 +8,7 @@ docstring_with_semicolon.py:1:1: I002 [*] Missing required import: `from __futur | = help: Insert required import: `from future import annotations` -ℹ Suggested fix +ℹ Fix 1 |-"""Hello, world!"""; x = 1 1 |+"""Hello, world!"""; from __future__ import annotations; x = 1 diff --git a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_existing_import.py.snap b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_existing_import.py.snap index d094c96a6c..39faabae25 100644 --- a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_existing_import.py.snap +++ b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_existing_import.py.snap @@ -9,7 +9,7 @@ existing_import.py:1:1: I002 [*] Missing required import: `from __future__ impor | = help: Insert required import: `from future import annotations` -ℹ Suggested fix +ℹ Fix 1 |+from __future__ import annotations 1 2 | from __future__ import generator_stop 2 3 | import os diff --git a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_multiline_docstring.py.snap b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_multiline_docstring.py.snap index f0832f6f76..c5c8bf78df 100644 --- a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_multiline_docstring.py.snap +++ b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_multiline_docstring.py.snap @@ -10,7 +10,7 @@ multiline_docstring.py:1:1: I002 [*] Missing required import: `from __future__ i | = help: Insert required import: `from future import annotations` -ℹ Suggested fix +ℹ Fix 1 1 | """a 2 2 | b""" 3 3 | # b diff --git a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_off.py.snap b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_off.py.snap index 0d506cdcab..ada290ffd3 100644 --- a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_off.py.snap +++ b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_import_off.py.snap @@ -10,7 +10,7 @@ off.py:1:1: I002 [*] Missing required import: `from __future__ import annotation | = help: Insert required import: `from future import annotations` -ℹ Suggested fix +ℹ Fix 1 1 | # isort: off 2 |+from __future__ import annotations 2 3 | diff --git a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_imports_docstring.py.snap b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_imports_docstring.py.snap index 06f61e148e..c5a0bd69f0 100644 --- a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_imports_docstring.py.snap +++ b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__required_imports_docstring.py.snap @@ -10,7 +10,7 @@ docstring.py:1:1: I002 [*] Missing required import: `from __future__ import anno | = help: Insert required import: `from future import annotations` -ℹ Suggested fix +ℹ Fix 1 1 | """Hello, world!""" 2 |+from __future__ import annotations 2 3 | @@ -25,7 +25,7 @@ docstring.py:1:1: I002 [*] Missing required import: `from __future__ import gene | = help: Insert required import: `from future import generator_stop` -ℹ Suggested fix +ℹ Fix 1 1 | """Hello, world!""" 2 |+from __future__ import generator_stop 2 3 | diff --git a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__skip.py.snap b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__skip.py.snap index d3eb858cc5..498421b65b 100644 --- a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__skip.py.snap +++ b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__skip.py.snap @@ -31,6 +31,10 @@ skip.py:27:1: I001 [*] Import block is un-sorted or un-formatted 26 | import os # isort:skip 27 | / import collections 28 | | import abc +29 | | + | |_^ I001 +30 | +31 | def f(): | = help: Organize imports @@ -41,5 +45,24 @@ skip.py:27:1: I001 [*] Import block is un-sorted or un-formatted 27 |+ import abc 27 28 | import collections 28 |- import abc +29 29 | +30 30 | +31 31 | def f(): + +skip.py:34:1: I001 [*] Import block is un-sorted or un-formatted + | +32 | import sys; import os # isort:skip +33 | import sys; import os # isort:skip # isort:skip +34 | / import sys; import os + | + = help: Organize imports + +ℹ Fix +31 31 | def f(): +32 32 | import sys; import os # isort:skip +33 33 | import sys; import os # isort:skip # isort:skip +34 |- import sys; import os + 34 |+ import os + 35 |+ import sys diff --git a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__split.py.snap b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__split.py.snap index 185696f70c..7556899b60 100644 --- a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__split.py.snap +++ b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__split.py.snap @@ -29,6 +29,10 @@ split.py:20:1: I001 [*] Import block is un-sorted or un-formatted 19 | 20 | / import D 21 | | import B +22 | | + | |_^ I001 +23 | +24 | import e | = help: Organize imports @@ -39,5 +43,25 @@ split.py:20:1: I001 [*] Import block is un-sorted or un-formatted 20 |+ import B 20 21 | import D 21 |- import B +22 22 | +23 23 | +24 24 | import e + +split.py:30:1: I001 [*] Import block is un-sorted or un-formatted + | +28 | # isort: split +29 | +30 | / import d +31 | | import c + | + = help: Organize imports + +ℹ Fix +27 27 | # isort: split +28 28 | # isort: split +29 29 | + 30 |+import c +30 31 | import d +31 |-import c diff --git a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__straight_required_import_docstring.py.snap b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__straight_required_import_docstring.py.snap index 7a579953f1..a03f5f7702 100644 --- a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__straight_required_import_docstring.py.snap +++ b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__straight_required_import_docstring.py.snap @@ -10,7 +10,7 @@ docstring.py:1:1: I002 [*] Missing required import: `import os` | = help: Insert required import: `import os` -ℹ Suggested fix +ℹ Fix 1 1 | """Hello, world!""" 2 |+import os 2 3 | diff --git a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__straight_required_import_docstring.pyi.snap b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__straight_required_import_docstring.pyi.snap index 6225322977..41b00e0ee3 100644 --- a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__straight_required_import_docstring.pyi.snap +++ b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__straight_required_import_docstring.pyi.snap @@ -10,7 +10,7 @@ docstring.pyi:1:1: I002 [*] Missing required import: `import os` | = help: Insert required import: `import os` -ℹ Suggested fix +ℹ Fix 1 1 | """Hello, world!""" 2 |+import os 2 3 | diff --git a/crates/ruff/src/rules/isort/sorting.rs b/crates/ruff/src/rules/isort/sorting.rs index 9ce7d63bb3..75bb9a2653 100644 --- a/crates/ruff/src/rules/isort/sorting.rs +++ b/crates/ruff/src/rules/isort/sorting.rs @@ -32,7 +32,7 @@ fn prefix( } else if variables.contains(name) { // Ex) `variable` Prefix::Variables - } else if name.len() > 1 && str::is_upper(name) { + } else if name.len() > 1 && str::is_cased_uppercase(name) { // Ex) `CONSTANT` Prefix::Constants } else if name.chars().next().map_or(false, char::is_uppercase) { @@ -56,10 +56,17 @@ pub(crate) fn cmp_modules( alias1: &AliasData, alias2: &AliasData, force_to_top: &BTreeSet, + case_sensitive: bool, ) -> Ordering { cmp_force_to_top(alias1.name, alias2.name, force_to_top) - .then_with(|| natord::compare_ignore_case(alias1.name, alias2.name)) - .then_with(|| natord::compare(alias1.name, alias2.name)) + .then_with(|| { + if case_sensitive { + natord::compare(alias1.name, alias2.name) + } else { + natord::compare_ignore_case(alias1.name, alias2.name) + .then_with(|| natord::compare(alias1.name, alias2.name)) + } + }) .then_with(|| match (alias1.asname, alias2.asname) { (None, None) => Ordering::Equal, (None, Some(_)) => Ordering::Less, @@ -69,6 +76,7 @@ pub(crate) fn cmp_modules( } /// Compare two member imports within `Stmt::ImportFrom` blocks. +#[allow(clippy::too_many_arguments)] pub(crate) fn cmp_members( alias1: &AliasData, alias2: &AliasData, @@ -77,6 +85,7 @@ pub(crate) fn cmp_members( constants: &BTreeSet, variables: &BTreeSet, force_to_top: &BTreeSet, + case_sensitive: bool, ) -> Ordering { match (alias1.name == "*", alias2.name == "*") { (true, false) => Ordering::Less, @@ -85,9 +94,9 @@ pub(crate) fn cmp_members( if order_by_type { prefix(alias1.name, classes, constants, variables) .cmp(&prefix(alias2.name, classes, constants, variables)) - .then_with(|| cmp_modules(alias1, alias2, force_to_top)) + .then_with(|| cmp_modules(alias1, alias2, force_to_top, case_sensitive)) } else { - cmp_modules(alias1, alias2, force_to_top) + cmp_modules(alias1, alias2, force_to_top, case_sensitive) } } } @@ -116,6 +125,7 @@ pub(crate) fn cmp_import_from( import_from2: &ImportFromData, relative_imports_order: RelativeImportsOrder, force_to_top: &BTreeSet, + case_sensitive: bool, ) -> Ordering { cmp_levels( import_from1.level, @@ -133,8 +143,13 @@ pub(crate) fn cmp_import_from( (None, None) => Ordering::Equal, (None, Some(_)) => Ordering::Less, (Some(_), None) => Ordering::Greater, - (Some(module1), Some(module2)) => natord::compare_ignore_case(module1, module2) - .then_with(|| natord::compare(module1, module2)), + (Some(module1), Some(module2)) => { + if case_sensitive { + natord::compare(module1, module2) + } else { + natord::compare_ignore_case(module1, module2) + } + } }) } @@ -143,9 +158,14 @@ fn cmp_import_import_from( import: &AliasData, import_from: &ImportFromData, force_to_top: &BTreeSet, + case_sensitive: bool, ) -> Ordering { cmp_force_to_top(import.name, &import_from.module_name(), force_to_top).then_with(|| { - natord::compare_ignore_case(import.name, import_from.module.unwrap_or_default()) + if case_sensitive { + natord::compare(import.name, import_from.module.unwrap_or_default()) + } else { + natord::compare_ignore_case(import.name, import_from.module.unwrap_or_default()) + } }) } @@ -156,20 +176,24 @@ pub(crate) fn cmp_either_import( b: &EitherImport, relative_imports_order: RelativeImportsOrder, force_to_top: &BTreeSet, + case_sensitive: bool, ) -> Ordering { match (a, b) { - (Import((alias1, _)), Import((alias2, _))) => cmp_modules(alias1, alias2, force_to_top), + (Import((alias1, _)), Import((alias2, _))) => { + cmp_modules(alias1, alias2, force_to_top, case_sensitive) + } (ImportFrom((import_from, ..)), Import((alias, _))) => { - cmp_import_import_from(alias, import_from, force_to_top).reverse() + cmp_import_import_from(alias, import_from, force_to_top, case_sensitive).reverse() } (Import((alias, _)), ImportFrom((import_from, ..))) => { - cmp_import_import_from(alias, import_from, force_to_top) + cmp_import_import_from(alias, import_from, force_to_top, case_sensitive) } (ImportFrom((import_from1, ..)), ImportFrom((import_from2, ..))) => cmp_import_from( import_from1, import_from2, relative_imports_order, force_to_top, + case_sensitive, ), } } diff --git a/crates/ruff/src/rules/mccabe/rules/function_is_too_complex.rs b/crates/ruff/src/rules/mccabe/rules/function_is_too_complex.rs index 0311470ac7..d67de8efc1 100644 --- a/crates/ruff/src/rules/mccabe/rules/function_is_too_complex.rs +++ b/crates/ruff/src/rules/mccabe/rules/function_is_too_complex.rs @@ -1,25 +1,21 @@ -use rustpython_parser::ast::{self, Excepthandler, Stmt}; +use rustpython_parser::ast::{self, ExceptHandler, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::identifier_range; -use ruff_python_ast::source_code::Locator; +use ruff_python_ast::identifier::Identifier; /// ## What it does /// Checks for functions with a high `McCabe` complexity. /// +/// ## Why is this bad? /// The `McCabe` complexity of a function is a measure of the complexity of /// the control flow graph of the function. It is calculated by adding /// one to the number of decision points in the function. A decision /// point is a place in the code where the program has a choice of two /// or more paths to follow. /// -/// ## Why is this bad? /// Functions with a high complexity are hard to understand and maintain. /// -/// ## Options -/// - `mccabe.max-complexity` -/// /// ## Example /// ```python /// def foo(a, b, c): @@ -46,6 +42,9 @@ use ruff_python_ast::source_code::Locator; /// return 2 /// return 1 /// ``` +/// +/// ## Options +/// - `mccabe.max-complexity` #[violation] pub struct ComplexStructure { name: String, @@ -117,7 +116,7 @@ fn get_complexity_number(stmts: &[Stmt]) -> usize { complexity += get_complexity_number(finalbody); for handler in handlers { complexity += 1; - let Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { + let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { body, .. }) = handler; complexity += get_complexity_number(body); @@ -142,7 +141,6 @@ pub(crate) fn function_is_too_complex( name: &str, body: &[Stmt], max_complexity: usize, - locator: &Locator, ) -> Option { let complexity = get_complexity_number(body) + 1; if complexity > max_complexity { @@ -152,7 +150,7 @@ pub(crate) fn function_is_too_complex( complexity, max_complexity, }, - identifier_range(stmt, locator), + stmt.identifier(), )) } else { None diff --git a/crates/ruff/src/rules/mccabe/rules/mod.rs b/crates/ruff/src/rules/mccabe/rules/mod.rs index b6898b7da2..a23f4cd8d2 100644 --- a/crates/ruff/src/rules/mccabe/rules/mod.rs +++ b/crates/ruff/src/rules/mccabe/rules/mod.rs @@ -1,3 +1,3 @@ -pub(crate) use function_is_too_complex::{function_is_too_complex, ComplexStructure}; +pub(crate) use function_is_too_complex::*; mod function_is_too_complex; diff --git a/crates/ruff/src/rules/mod.rs b/crates/ruff/src/rules/mod.rs index 23083cb2ab..e8fd430cb0 100644 --- a/crates/ruff/src/rules/mod.rs +++ b/crates/ruff/src/rules/mod.rs @@ -11,6 +11,7 @@ pub mod flake8_bugbear; pub mod flake8_builtins; pub mod flake8_commas; pub mod flake8_comprehensions; +pub mod flake8_copyright; pub mod flake8_datetimez; pub mod flake8_debugger; pub mod flake8_django; @@ -44,6 +45,7 @@ pub mod mccabe; pub mod numpy; pub mod pandas_vet; pub mod pep8_naming; +pub mod perflint; pub mod pycodestyle; pub mod pydocstyle; pub mod pyflakes; diff --git a/crates/ruff/src/rules/numpy/mod.rs b/crates/ruff/src/rules/numpy/mod.rs index 37ddc17ffb..2bdb951dac 100644 --- a/crates/ruff/src/rules/numpy/mod.rs +++ b/crates/ruff/src/rules/numpy/mod.rs @@ -15,6 +15,7 @@ mod tests { #[test_case(Rule::NumpyDeprecatedTypeAlias, Path::new("NPY001.py"))] #[test_case(Rule::NumpyLegacyRandom, Path::new("NPY002.py"))] + #[test_case(Rule::NumpyDeprecatedFunction, Path::new("NPY003.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy()); let diagnostics = test_path( diff --git a/crates/ruff/src/rules/numpy/rules/deprecated_function.rs b/crates/ruff/src/rules/numpy/rules/deprecated_function.rs new file mode 100644 index 0000000000..3fb2774cfb --- /dev/null +++ b/crates/ruff/src/rules/numpy/rules/deprecated_function.rs @@ -0,0 +1,97 @@ +use rustpython_parser::ast::{self, Expr, Ranged}; + +use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; +use ruff_macros::{derive_message_formats, violation}; + +use crate::checkers::ast::Checker; +use crate::registry::AsRule; + +/// ## What it does +/// Checks for uses of deprecated NumPy functions. +/// +/// ## Why is this bad? +/// When NumPy functions are deprecated, they are usually replaced with +/// newer, more efficient versions, or with functions that are more +/// consistent with the rest of the NumPy API. +/// +/// Prefer newer APIs over deprecated ones. +/// +/// ## Examples +/// ```python +/// import numpy as np +/// +/// np.alltrue([True, False]) +/// ``` +/// +/// Use instead: +/// ```python +/// import numpy as np +/// +/// np.all([True, False]) +/// ``` +#[violation] +pub struct NumpyDeprecatedFunction { + existing: String, + replacement: String, +} + +impl Violation for NumpyDeprecatedFunction { + const AUTOFIX: AutofixKind = AutofixKind::Sometimes; + + #[derive_message_formats] + fn message(&self) -> String { + let NumpyDeprecatedFunction { + existing, + replacement, + } = self; + format!("`np.{existing}` is deprecated; use `np.{replacement}` instead") + } + + fn autofix_title(&self) -> Option { + let NumpyDeprecatedFunction { replacement, .. } = self; + Some(format!("Replace with `np.{replacement}`")) + } +} + +/// NPY003 +pub(crate) fn deprecated_function(checker: &mut Checker, expr: &Expr) { + if let Some((existing, replacement)) = + checker + .semantic() + .resolve_call_path(expr) + .and_then(|call_path| match call_path.as_slice() { + ["numpy", "round_"] => Some(("round_", "round")), + ["numpy", "product"] => Some(("product", "prod")), + ["numpy", "cumproduct"] => Some(("cumproduct", "cumprod")), + ["numpy", "sometrue"] => Some(("sometrue", "any")), + ["numpy", "alltrue"] => Some(("alltrue", "all")), + _ => None, + }) + { + let mut diagnostic = Diagnostic::new( + NumpyDeprecatedFunction { + existing: existing.to_string(), + replacement: replacement.to_string(), + }, + expr.range(), + ); + if checker.patch(diagnostic.kind.rule()) { + match expr { + Expr::Name(_) => { + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( + replacement.to_string(), + expr.range(), + ))); + } + Expr::Attribute(ast::ExprAttribute { attr, .. }) => { + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( + replacement.to_string(), + attr.range(), + ))); + } + _ => {} + } + } + checker.diagnostics.push(diagnostic); + } +} diff --git a/crates/ruff/src/rules/numpy/rules/deprecated_type_alias.rs b/crates/ruff/src/rules/numpy/rules/deprecated_type_alias.rs index 834400e5eb..853cf8039d 100644 --- a/crates/ruff/src/rules/numpy/rules/deprecated_type_alias.rs +++ b/crates/ruff/src/rules/numpy/rules/deprecated_type_alias.rs @@ -12,7 +12,7 @@ use crate::registry::AsRule; /// ## Why is this bad? /// NumPy's `np.int` has long been an alias of the builtin `int`. The same /// goes for `np.float`, `np.bool`, and others. These aliases exist -/// primarily primarily for historic reasons, and have been a cause of +/// primarily for historic reasons, and have been a cause of /// frequent confusion for newcomers. /// /// These aliases were been deprecated in 1.20, and removed in 1.24. @@ -48,25 +48,22 @@ impl AlwaysAutofixableViolation for NumpyDeprecatedTypeAlias { /// NPY001 pub(crate) fn deprecated_type_alias(checker: &mut Checker, expr: &Expr) { - if let Some(type_name) = - checker - .semantic_model() - .resolve_call_path(expr) - .and_then(|call_path| { - if call_path.as_slice() == ["numpy", "bool"] - || call_path.as_slice() == ["numpy", "int"] - || call_path.as_slice() == ["numpy", "float"] - || call_path.as_slice() == ["numpy", "complex"] - || call_path.as_slice() == ["numpy", "object"] - || call_path.as_slice() == ["numpy", "str"] - || call_path.as_slice() == ["numpy", "long"] - || call_path.as_slice() == ["numpy", "unicode"] - { - Some(call_path[1]) - } else { - None - } - }) + if let Some(type_name) = checker + .semantic() + .resolve_call_path(expr) + .and_then(|call_path| { + if matches!( + call_path.as_slice(), + [ + "numpy", + "bool" | "int" | "float" | "complex" | "object" | "str" | "long" | "unicode" + ] + ) { + Some(call_path[1]) + } else { + None + } + }) { let mut diagnostic = Diagnostic::new( NumpyDeprecatedTypeAlias { @@ -75,8 +72,7 @@ pub(crate) fn deprecated_type_alias(checker: &mut Checker, expr: &Expr) { expr.range(), ); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( match type_name { "unicode" => "str", "long" => "int", diff --git a/crates/ruff/src/rules/numpy/rules/legacy_random.rs b/crates/ruff/src/rules/numpy/rules/legacy_random.rs new file mode 100644 index 0000000000..cdcd68f149 --- /dev/null +++ b/crates/ruff/src/rules/numpy/rules/legacy_random.rs @@ -0,0 +1,137 @@ +use rustpython_parser::ast::{Expr, Ranged}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; + +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for the use of legacy `np.random` function calls. +/// +/// ## Why is this bad? +/// According to the NumPy documentation's [Legacy Random Generation]: +/// +/// > The `RandomState` provides access to legacy generators... This class +/// > should only be used if it is essential to have randoms that are +/// > identical to what would have been produced by previous versions of +/// > NumPy. +/// +/// The members exposed directly on the `random` module are convenience +/// functions that alias to methods on a global singleton `RandomState` +/// instance. NumPy recommends using a dedicated `Generator` instance +/// rather than the random variate generation methods exposed directly on +/// the `random` module, as the new `Generator` is both faster and has +/// better statistical properties. +/// +/// See the documentation on [Random Sampling] and [NEP 19] for further +/// details. +/// +/// ## Examples +/// ```python +/// import numpy as np +/// +/// np.random.seed(1337) +/// np.random.normal() +/// ``` +/// +/// Use instead: +/// ```python +/// rng = np.random.default_rng(1337) +/// rng.normal() +/// ``` +/// +/// [Legacy Random Generation]: https://numpy.org/doc/stable/reference/random/legacy.html#legacy +/// [Random Sampling]: https://numpy.org/doc/stable/reference/random/index.html#random-quick-start +/// [NEP 19]: https://numpy.org/neps/nep-0019-rng-policy.html +#[violation] +pub struct NumpyLegacyRandom { + method_name: String, +} + +impl Violation for NumpyLegacyRandom { + #[derive_message_formats] + fn message(&self) -> String { + let NumpyLegacyRandom { method_name } = self; + format!("Replace legacy `np.random.{method_name}` call with `np.random.Generator`") + } +} + +/// NPY002 +pub(crate) fn legacy_random(checker: &mut Checker, expr: &Expr) { + if let Some(method_name) = checker + .semantic() + .resolve_call_path(expr) + .and_then(|call_path| { + // seeding state + if matches!( + call_path.as_slice(), + [ + "numpy", + "random", + // Seeds + "seed" | + "get_state" | + "set_state" | + // Simple random data + "rand" | + "randn" | + "randint" | + "random_integers" | + "random_sample" | + "choice" | + "bytes" | + // Permutations + "shuffle" | + "permutation" | + // Distributions + "beta" | + "binomial" | + "chisquare" | + "dirichlet" | + "exponential" | + "f" | + "gamma" | + "geometric" | + "gumbel" | + "hypergeometric" | + "laplace" | + "logistic" | + "lognormal" | + "logseries" | + "multinomial" | + "multivariate_normal" | + "negative_binomial" | + "noncentral_chisquare" | + "noncentral_f" | + "normal" | + "pareto" | + "poisson" | + "power" | + "rayleigh" | + "standard_cauchy" | + "standard_exponential" | + "standard_gamma" | + "standard_normal" | + "standard_t" | + "triangular" | + "uniform" | + "vonmises" | + "wald" | + "weibull" | + "zipf" + ] + ) { + Some(call_path[2]) + } else { + None + } + }) + { + checker.diagnostics.push(Diagnostic::new( + NumpyLegacyRandom { + method_name: method_name.to_string(), + }, + expr.range(), + )); + } +} diff --git a/crates/ruff/src/rules/numpy/rules/mod.rs b/crates/ruff/src/rules/numpy/rules/mod.rs index c82e79bc1c..7c46515e76 100644 --- a/crates/ruff/src/rules/numpy/rules/mod.rs +++ b/crates/ruff/src/rules/numpy/rules/mod.rs @@ -1,5 +1,7 @@ -pub(crate) use deprecated_type_alias::{deprecated_type_alias, NumpyDeprecatedTypeAlias}; -pub(crate) use numpy_legacy_random::{numpy_legacy_random, NumpyLegacyRandom}; +pub(crate) use deprecated_function::*; +pub(crate) use deprecated_type_alias::*; +pub(crate) use legacy_random::*; +mod deprecated_function; mod deprecated_type_alias; -mod numpy_legacy_random; +mod legacy_random; diff --git a/crates/ruff/src/rules/numpy/rules/numpy_legacy_random.rs b/crates/ruff/src/rules/numpy/rules/numpy_legacy_random.rs deleted file mode 100644 index 0e52c39176..0000000000 --- a/crates/ruff/src/rules/numpy/rules/numpy_legacy_random.rs +++ /dev/null @@ -1,132 +0,0 @@ -use rustpython_parser::ast::{Expr, Ranged}; - -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, violation}; - -use crate::checkers::ast::Checker; - -/// ## What it does -/// Checks for the use of legacy `np.random` function calls. -/// -/// ## Why is this bad? -/// According to the NumPy documentation's [Legacy Random Generation]: -/// -/// > The `RandomState` provides access to legacy generators... This class -/// > should only be used if it is essential to have randoms that are -/// > identical to what would have been produced by previous versions of -/// > NumPy. -/// -/// The members exposed directly on the `random` module are convenience -/// functions that alias to methods on a global singleton `RandomState` -/// instance. NumPy recommends using a dedicated `Generator` instance -/// rather than the random variate generation methods exposed directly on -/// the `random` module, as the new `Generator` is both faster and has -/// better statistical properties. -/// -/// See the documentation on [Random Sampling] and [NEP 19] for further -/// details. -/// -/// ## Examples -/// ```python -/// import numpy as np -/// -/// np.random.seed(1337) -/// np.random.normal() -/// ``` -/// -/// Use instead: -/// ```python -/// rng = np.random.default_rng(1337) -/// rng.normal() -/// ``` -/// -/// [Legacy Random Generation]: https://numpy.org/doc/stable/reference/random/legacy.html#legacy -/// [Random Sampling]: https://numpy.org/doc/stable/reference/random/index.html#random-quick-start -/// [NEP 19]: https://numpy.org/neps/nep-0019-rng-policy.html -#[violation] -pub struct NumpyLegacyRandom { - method_name: String, -} - -impl Violation for NumpyLegacyRandom { - #[derive_message_formats] - fn message(&self) -> String { - let NumpyLegacyRandom { method_name } = self; - format!("Replace legacy `np.random.{method_name}` call with `np.random.Generator`") - } -} - -/// NPY002 -pub(crate) fn numpy_legacy_random(checker: &mut Checker, expr: &Expr) { - if let Some(method_name) = - checker - .semantic_model() - .resolve_call_path(expr) - .and_then(|call_path| { - // seeding state - if call_path.as_slice() == ["numpy", "random", "seed"] - || call_path.as_slice() == ["numpy", "random", "get_state"] - || call_path.as_slice() == ["numpy", "random", "set_state"] - // simple random data - || call_path.as_slice() == ["numpy", "random", "rand"] - || call_path.as_slice() == ["numpy", "random", "randn"] - || call_path.as_slice() == ["numpy", "random", "randint"] - || call_path.as_slice() == ["numpy", "random", "random_integers"] - || call_path.as_slice() == ["numpy", "random", "random_sample"] - || call_path.as_slice() == ["numpy", "random", "choice"] - || call_path.as_slice() == ["numpy", "random", "bytes"] - // permutations - || call_path.as_slice() == ["numpy", "random", "shuffle"] - || call_path.as_slice() == ["numpy", "random", "permutation"] - // distributions - || call_path.as_slice() == ["numpy", "random", "beta"] - || call_path.as_slice() == ["numpy", "random", "binomial"] - || call_path.as_slice() == ["numpy", "random", "chisquare"] - || call_path.as_slice() == ["numpy", "random", "dirichlet"] - || call_path.as_slice() == ["numpy", "random", "exponential"] - || call_path.as_slice() == ["numpy", "random", "f"] - || call_path.as_slice() == ["numpy", "random", "gamma"] - || call_path.as_slice() == ["numpy", "random", "geometric"] - || call_path.as_slice() == ["numpy", "random", "get_state"] - || call_path.as_slice() == ["numpy", "random", "gumbel"] - || call_path.as_slice() == ["numpy", "random", "hypergeometric"] - || call_path.as_slice() == ["numpy", "random", "laplace"] - || call_path.as_slice() == ["numpy", "random", "logistic"] - || call_path.as_slice() == ["numpy", "random", "lognormal"] - || call_path.as_slice() == ["numpy", "random", "logseries"] - || call_path.as_slice() == ["numpy", "random", "multinomial"] - || call_path.as_slice() == ["numpy", "random", "multivariate_normal"] - || call_path.as_slice() == ["numpy", "random", "negative_binomial"] - || call_path.as_slice() == ["numpy", "random", "noncentral_chisquare"] - || call_path.as_slice() == ["numpy", "random", "noncentral_f"] - || call_path.as_slice() == ["numpy", "random", "normal"] - || call_path.as_slice() == ["numpy", "random", "pareto"] - || call_path.as_slice() == ["numpy", "random", "poisson"] - || call_path.as_slice() == ["numpy", "random", "power"] - || call_path.as_slice() == ["numpy", "random", "rayleigh"] - || call_path.as_slice() == ["numpy", "random", "standard_cauchy"] - || call_path.as_slice() == ["numpy", "random", "standard_exponential"] - || call_path.as_slice() == ["numpy", "random", "standard_gamma"] - || call_path.as_slice() == ["numpy", "random", "standard_normal"] - || call_path.as_slice() == ["numpy", "random", "standard_t"] - || call_path.as_slice() == ["numpy", "random", "triangular"] - || call_path.as_slice() == ["numpy", "random", "uniform"] - || call_path.as_slice() == ["numpy", "random", "vonmises"] - || call_path.as_slice() == ["numpy", "random", "wald"] - || call_path.as_slice() == ["numpy", "random", "weibull"] - || call_path.as_slice() == ["numpy", "random", "zipf"] - { - Some(call_path[2]) - } else { - None - } - }) - { - checker.diagnostics.push(Diagnostic::new( - NumpyLegacyRandom { - method_name: method_name.to_string(), - }, - expr.range(), - )); - } -} diff --git a/crates/ruff/src/rules/numpy/snapshots/ruff__rules__numpy__tests__numpy-deprecated-function_NPY003.py.snap b/crates/ruff/src/rules/numpy/snapshots/ruff__rules__numpy__tests__numpy-deprecated-function_NPY003.py.snap new file mode 100644 index 0000000000..1165e5f488 --- /dev/null +++ b/crates/ruff/src/rules/numpy/snapshots/ruff__rules__numpy__tests__numpy-deprecated-function_NPY003.py.snap @@ -0,0 +1,201 @@ +--- +source: crates/ruff/src/rules/numpy/mod.rs +--- +NPY003.py:3:1: NPY003 [*] `np.round_` is deprecated; use `np.round` instead + | +1 | import numpy as np +2 | +3 | np.round_(np.random.rand(5, 5), 2) + | ^^^^^^^^^ NPY003 +4 | np.product(np.random.rand(5, 5)) +5 | np.cumproduct(np.random.rand(5, 5)) + | + = help: Replace with `np.round` + +ℹ Suggested fix +1 1 | import numpy as np +2 2 | +3 |-np.round_(np.random.rand(5, 5), 2) + 3 |+np.round(np.random.rand(5, 5), 2) +4 4 | np.product(np.random.rand(5, 5)) +5 5 | np.cumproduct(np.random.rand(5, 5)) +6 6 | np.sometrue(np.random.rand(5, 5)) + +NPY003.py:4:1: NPY003 [*] `np.product` is deprecated; use `np.prod` instead + | +3 | np.round_(np.random.rand(5, 5), 2) +4 | np.product(np.random.rand(5, 5)) + | ^^^^^^^^^^ NPY003 +5 | np.cumproduct(np.random.rand(5, 5)) +6 | np.sometrue(np.random.rand(5, 5)) + | + = help: Replace with `np.prod` + +ℹ Suggested fix +1 1 | import numpy as np +2 2 | +3 3 | np.round_(np.random.rand(5, 5), 2) +4 |-np.product(np.random.rand(5, 5)) + 4 |+np.prod(np.random.rand(5, 5)) +5 5 | np.cumproduct(np.random.rand(5, 5)) +6 6 | np.sometrue(np.random.rand(5, 5)) +7 7 | np.alltrue(np.random.rand(5, 5)) + +NPY003.py:5:1: NPY003 [*] `np.cumproduct` is deprecated; use `np.cumprod` instead + | +3 | np.round_(np.random.rand(5, 5), 2) +4 | np.product(np.random.rand(5, 5)) +5 | np.cumproduct(np.random.rand(5, 5)) + | ^^^^^^^^^^^^^ NPY003 +6 | np.sometrue(np.random.rand(5, 5)) +7 | np.alltrue(np.random.rand(5, 5)) + | + = help: Replace with `np.cumprod` + +ℹ Suggested fix +2 2 | +3 3 | np.round_(np.random.rand(5, 5), 2) +4 4 | np.product(np.random.rand(5, 5)) +5 |-np.cumproduct(np.random.rand(5, 5)) + 5 |+np.cumprod(np.random.rand(5, 5)) +6 6 | np.sometrue(np.random.rand(5, 5)) +7 7 | np.alltrue(np.random.rand(5, 5)) +8 8 | + +NPY003.py:6:1: NPY003 [*] `np.sometrue` is deprecated; use `np.any` instead + | +4 | np.product(np.random.rand(5, 5)) +5 | np.cumproduct(np.random.rand(5, 5)) +6 | np.sometrue(np.random.rand(5, 5)) + | ^^^^^^^^^^^ NPY003 +7 | np.alltrue(np.random.rand(5, 5)) + | + = help: Replace with `np.any` + +ℹ Suggested fix +3 3 | np.round_(np.random.rand(5, 5), 2) +4 4 | np.product(np.random.rand(5, 5)) +5 5 | np.cumproduct(np.random.rand(5, 5)) +6 |-np.sometrue(np.random.rand(5, 5)) + 6 |+np.any(np.random.rand(5, 5)) +7 7 | np.alltrue(np.random.rand(5, 5)) +8 8 | +9 9 | from numpy import round_, product, cumproduct, sometrue, alltrue + +NPY003.py:7:1: NPY003 [*] `np.alltrue` is deprecated; use `np.all` instead + | +5 | np.cumproduct(np.random.rand(5, 5)) +6 | np.sometrue(np.random.rand(5, 5)) +7 | np.alltrue(np.random.rand(5, 5)) + | ^^^^^^^^^^ NPY003 +8 | +9 | from numpy import round_, product, cumproduct, sometrue, alltrue + | + = help: Replace with `np.all` + +ℹ Suggested fix +4 4 | np.product(np.random.rand(5, 5)) +5 5 | np.cumproduct(np.random.rand(5, 5)) +6 6 | np.sometrue(np.random.rand(5, 5)) +7 |-np.alltrue(np.random.rand(5, 5)) + 7 |+np.all(np.random.rand(5, 5)) +8 8 | +9 9 | from numpy import round_, product, cumproduct, sometrue, alltrue +10 10 | + +NPY003.py:11:1: NPY003 [*] `np.round_` is deprecated; use `np.round` instead + | + 9 | from numpy import round_, product, cumproduct, sometrue, alltrue +10 | +11 | round_(np.random.rand(5, 5), 2) + | ^^^^^^ NPY003 +12 | product(np.random.rand(5, 5)) +13 | cumproduct(np.random.rand(5, 5)) + | + = help: Replace with `np.round` + +ℹ Suggested fix +8 8 | +9 9 | from numpy import round_, product, cumproduct, sometrue, alltrue +10 10 | +11 |-round_(np.random.rand(5, 5), 2) + 11 |+round(np.random.rand(5, 5), 2) +12 12 | product(np.random.rand(5, 5)) +13 13 | cumproduct(np.random.rand(5, 5)) +14 14 | sometrue(np.random.rand(5, 5)) + +NPY003.py:12:1: NPY003 [*] `np.product` is deprecated; use `np.prod` instead + | +11 | round_(np.random.rand(5, 5), 2) +12 | product(np.random.rand(5, 5)) + | ^^^^^^^ NPY003 +13 | cumproduct(np.random.rand(5, 5)) +14 | sometrue(np.random.rand(5, 5)) + | + = help: Replace with `np.prod` + +ℹ Suggested fix +9 9 | from numpy import round_, product, cumproduct, sometrue, alltrue +10 10 | +11 11 | round_(np.random.rand(5, 5), 2) +12 |-product(np.random.rand(5, 5)) + 12 |+prod(np.random.rand(5, 5)) +13 13 | cumproduct(np.random.rand(5, 5)) +14 14 | sometrue(np.random.rand(5, 5)) +15 15 | alltrue(np.random.rand(5, 5)) + +NPY003.py:13:1: NPY003 [*] `np.cumproduct` is deprecated; use `np.cumprod` instead + | +11 | round_(np.random.rand(5, 5), 2) +12 | product(np.random.rand(5, 5)) +13 | cumproduct(np.random.rand(5, 5)) + | ^^^^^^^^^^ NPY003 +14 | sometrue(np.random.rand(5, 5)) +15 | alltrue(np.random.rand(5, 5)) + | + = help: Replace with `np.cumprod` + +ℹ Suggested fix +10 10 | +11 11 | round_(np.random.rand(5, 5), 2) +12 12 | product(np.random.rand(5, 5)) +13 |-cumproduct(np.random.rand(5, 5)) + 13 |+cumprod(np.random.rand(5, 5)) +14 14 | sometrue(np.random.rand(5, 5)) +15 15 | alltrue(np.random.rand(5, 5)) + +NPY003.py:14:1: NPY003 [*] `np.sometrue` is deprecated; use `np.any` instead + | +12 | product(np.random.rand(5, 5)) +13 | cumproduct(np.random.rand(5, 5)) +14 | sometrue(np.random.rand(5, 5)) + | ^^^^^^^^ NPY003 +15 | alltrue(np.random.rand(5, 5)) + | + = help: Replace with `np.any` + +ℹ Suggested fix +11 11 | round_(np.random.rand(5, 5), 2) +12 12 | product(np.random.rand(5, 5)) +13 13 | cumproduct(np.random.rand(5, 5)) +14 |-sometrue(np.random.rand(5, 5)) + 14 |+any(np.random.rand(5, 5)) +15 15 | alltrue(np.random.rand(5, 5)) + +NPY003.py:15:1: NPY003 [*] `np.alltrue` is deprecated; use `np.all` instead + | +13 | cumproduct(np.random.rand(5, 5)) +14 | sometrue(np.random.rand(5, 5)) +15 | alltrue(np.random.rand(5, 5)) + | ^^^^^^^ NPY003 + | + = help: Replace with `np.all` + +ℹ Suggested fix +12 12 | product(np.random.rand(5, 5)) +13 13 | cumproduct(np.random.rand(5, 5)) +14 14 | sometrue(np.random.rand(5, 5)) +15 |-alltrue(np.random.rand(5, 5)) + 15 |+all(np.random.rand(5, 5)) + + diff --git a/crates/ruff/src/rules/numpy/snapshots/ruff__rules__numpy__tests__numpy-legacy-random_NPY002.py.snap b/crates/ruff/src/rules/numpy/snapshots/ruff__rules__numpy__tests__numpy-legacy-random_NPY002.py.snap index 38890264a4..c06c12bee6 100644 --- a/crates/ruff/src/rules/numpy/snapshots/ruff__rules__numpy__tests__numpy-legacy-random_NPY002.py.snap +++ b/crates/ruff/src/rules/numpy/snapshots/ruff__rules__numpy__tests__numpy-legacy-random_NPY002.py.snap @@ -1,498 +1,497 @@ --- source: crates/ruff/src/rules/numpy/mod.rs --- -NPY002.py:10:8: NPY002 Replace legacy `np.random.standard_normal` call with `np.random.Generator` +NPY002.py:12:8: NPY002 Replace legacy `np.random.standard_normal` call with `np.random.Generator` | - 8 | # instead of this (legacy version) - 9 | from numpy import random -10 | vals = random.standard_normal(10) +10 | from numpy import random +11 | +12 | vals = random.standard_normal(10) | ^^^^^^^^^^^^^^^^^^^^^^ NPY002 -11 | more_vals = random.standard_normal(10) -12 | numbers = random.integers(high, size=5) +13 | more_vals = random.standard_normal(10) +14 | numbers = random.integers(high, size=5) | -NPY002.py:11:13: NPY002 Replace legacy `np.random.standard_normal` call with `np.random.Generator` +NPY002.py:13:13: NPY002 Replace legacy `np.random.standard_normal` call with `np.random.Generator` | - 9 | from numpy import random -10 | vals = random.standard_normal(10) -11 | more_vals = random.standard_normal(10) +12 | vals = random.standard_normal(10) +13 | more_vals = random.standard_normal(10) | ^^^^^^^^^^^^^^^^^^^^^^ NPY002 -12 | numbers = random.integers(high, size=5) +14 | numbers = random.integers(high, size=5) | -NPY002.py:15:1: NPY002 Replace legacy `np.random.seed` call with `np.random.Generator` +NPY002.py:18:1: NPY002 Replace legacy `np.random.seed` call with `np.random.Generator` | -14 | import numpy -15 | numpy.random.seed() +16 | import numpy +17 | +18 | numpy.random.seed() | ^^^^^^^^^^^^^^^^^ NPY002 -16 | numpy.random.get_state() -17 | numpy.random.set_state() +19 | numpy.random.get_state() +20 | numpy.random.set_state() | -NPY002.py:16:1: NPY002 Replace legacy `np.random.get_state` call with `np.random.Generator` +NPY002.py:19:1: NPY002 Replace legacy `np.random.get_state` call with `np.random.Generator` | -14 | import numpy -15 | numpy.random.seed() -16 | numpy.random.get_state() +18 | numpy.random.seed() +19 | numpy.random.get_state() | ^^^^^^^^^^^^^^^^^^^^^^ NPY002 -17 | numpy.random.set_state() -18 | numpy.random.rand() +20 | numpy.random.set_state() +21 | numpy.random.rand() | -NPY002.py:17:1: NPY002 Replace legacy `np.random.set_state` call with `np.random.Generator` +NPY002.py:20:1: NPY002 Replace legacy `np.random.set_state` call with `np.random.Generator` | -15 | numpy.random.seed() -16 | numpy.random.get_state() -17 | numpy.random.set_state() +18 | numpy.random.seed() +19 | numpy.random.get_state() +20 | numpy.random.set_state() | ^^^^^^^^^^^^^^^^^^^^^^ NPY002 -18 | numpy.random.rand() -19 | numpy.random.randn() +21 | numpy.random.rand() +22 | numpy.random.randn() | -NPY002.py:18:1: NPY002 Replace legacy `np.random.rand` call with `np.random.Generator` +NPY002.py:21:1: NPY002 Replace legacy `np.random.rand` call with `np.random.Generator` | -16 | numpy.random.get_state() -17 | numpy.random.set_state() -18 | numpy.random.rand() +19 | numpy.random.get_state() +20 | numpy.random.set_state() +21 | numpy.random.rand() | ^^^^^^^^^^^^^^^^^ NPY002 -19 | numpy.random.randn() -20 | numpy.random.randint() +22 | numpy.random.randn() +23 | numpy.random.randint() | -NPY002.py:19:1: NPY002 Replace legacy `np.random.randn` call with `np.random.Generator` +NPY002.py:22:1: NPY002 Replace legacy `np.random.randn` call with `np.random.Generator` | -17 | numpy.random.set_state() -18 | numpy.random.rand() -19 | numpy.random.randn() +20 | numpy.random.set_state() +21 | numpy.random.rand() +22 | numpy.random.randn() | ^^^^^^^^^^^^^^^^^^ NPY002 -20 | numpy.random.randint() -21 | numpy.random.random_integers() +23 | numpy.random.randint() +24 | numpy.random.random_integers() | -NPY002.py:20:1: NPY002 Replace legacy `np.random.randint` call with `np.random.Generator` +NPY002.py:23:1: NPY002 Replace legacy `np.random.randint` call with `np.random.Generator` | -18 | numpy.random.rand() -19 | numpy.random.randn() -20 | numpy.random.randint() +21 | numpy.random.rand() +22 | numpy.random.randn() +23 | numpy.random.randint() | ^^^^^^^^^^^^^^^^^^^^ NPY002 -21 | numpy.random.random_integers() -22 | numpy.random.random_sample() +24 | numpy.random.random_integers() +25 | numpy.random.random_sample() | -NPY002.py:21:1: NPY002 Replace legacy `np.random.random_integers` call with `np.random.Generator` +NPY002.py:24:1: NPY002 Replace legacy `np.random.random_integers` call with `np.random.Generator` | -19 | numpy.random.randn() -20 | numpy.random.randint() -21 | numpy.random.random_integers() +22 | numpy.random.randn() +23 | numpy.random.randint() +24 | numpy.random.random_integers() | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -22 | numpy.random.random_sample() -23 | numpy.random.choice() +25 | numpy.random.random_sample() +26 | numpy.random.choice() | -NPY002.py:22:1: NPY002 Replace legacy `np.random.random_sample` call with `np.random.Generator` +NPY002.py:25:1: NPY002 Replace legacy `np.random.random_sample` call with `np.random.Generator` | -20 | numpy.random.randint() -21 | numpy.random.random_integers() -22 | numpy.random.random_sample() +23 | numpy.random.randint() +24 | numpy.random.random_integers() +25 | numpy.random.random_sample() | ^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -23 | numpy.random.choice() -24 | numpy.random.bytes() +26 | numpy.random.choice() +27 | numpy.random.bytes() | -NPY002.py:23:1: NPY002 Replace legacy `np.random.choice` call with `np.random.Generator` +NPY002.py:26:1: NPY002 Replace legacy `np.random.choice` call with `np.random.Generator` | -21 | numpy.random.random_integers() -22 | numpy.random.random_sample() -23 | numpy.random.choice() +24 | numpy.random.random_integers() +25 | numpy.random.random_sample() +26 | numpy.random.choice() | ^^^^^^^^^^^^^^^^^^^ NPY002 -24 | numpy.random.bytes() -25 | numpy.random.shuffle() +27 | numpy.random.bytes() +28 | numpy.random.shuffle() | -NPY002.py:24:1: NPY002 Replace legacy `np.random.bytes` call with `np.random.Generator` +NPY002.py:27:1: NPY002 Replace legacy `np.random.bytes` call with `np.random.Generator` | -22 | numpy.random.random_sample() -23 | numpy.random.choice() -24 | numpy.random.bytes() +25 | numpy.random.random_sample() +26 | numpy.random.choice() +27 | numpy.random.bytes() | ^^^^^^^^^^^^^^^^^^ NPY002 -25 | numpy.random.shuffle() -26 | numpy.random.permutation() +28 | numpy.random.shuffle() +29 | numpy.random.permutation() | -NPY002.py:25:1: NPY002 Replace legacy `np.random.shuffle` call with `np.random.Generator` +NPY002.py:28:1: NPY002 Replace legacy `np.random.shuffle` call with `np.random.Generator` | -23 | numpy.random.choice() -24 | numpy.random.bytes() -25 | numpy.random.shuffle() +26 | numpy.random.choice() +27 | numpy.random.bytes() +28 | numpy.random.shuffle() | ^^^^^^^^^^^^^^^^^^^^ NPY002 -26 | numpy.random.permutation() -27 | numpy.random.beta() +29 | numpy.random.permutation() +30 | numpy.random.beta() | -NPY002.py:26:1: NPY002 Replace legacy `np.random.permutation` call with `np.random.Generator` +NPY002.py:29:1: NPY002 Replace legacy `np.random.permutation` call with `np.random.Generator` | -24 | numpy.random.bytes() -25 | numpy.random.shuffle() -26 | numpy.random.permutation() +27 | numpy.random.bytes() +28 | numpy.random.shuffle() +29 | numpy.random.permutation() | ^^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -27 | numpy.random.beta() -28 | numpy.random.binomial() +30 | numpy.random.beta() +31 | numpy.random.binomial() | -NPY002.py:27:1: NPY002 Replace legacy `np.random.beta` call with `np.random.Generator` +NPY002.py:30:1: NPY002 Replace legacy `np.random.beta` call with `np.random.Generator` | -25 | numpy.random.shuffle() -26 | numpy.random.permutation() -27 | numpy.random.beta() +28 | numpy.random.shuffle() +29 | numpy.random.permutation() +30 | numpy.random.beta() | ^^^^^^^^^^^^^^^^^ NPY002 -28 | numpy.random.binomial() -29 | numpy.random.chisquare() +31 | numpy.random.binomial() +32 | numpy.random.chisquare() | -NPY002.py:28:1: NPY002 Replace legacy `np.random.binomial` call with `np.random.Generator` +NPY002.py:31:1: NPY002 Replace legacy `np.random.binomial` call with `np.random.Generator` | -26 | numpy.random.permutation() -27 | numpy.random.beta() -28 | numpy.random.binomial() +29 | numpy.random.permutation() +30 | numpy.random.beta() +31 | numpy.random.binomial() | ^^^^^^^^^^^^^^^^^^^^^ NPY002 -29 | numpy.random.chisquare() -30 | numpy.random.dirichlet() +32 | numpy.random.chisquare() +33 | numpy.random.dirichlet() | -NPY002.py:29:1: NPY002 Replace legacy `np.random.chisquare` call with `np.random.Generator` +NPY002.py:32:1: NPY002 Replace legacy `np.random.chisquare` call with `np.random.Generator` | -27 | numpy.random.beta() -28 | numpy.random.binomial() -29 | numpy.random.chisquare() +30 | numpy.random.beta() +31 | numpy.random.binomial() +32 | numpy.random.chisquare() | ^^^^^^^^^^^^^^^^^^^^^^ NPY002 -30 | numpy.random.dirichlet() -31 | numpy.random.exponential() +33 | numpy.random.dirichlet() +34 | numpy.random.exponential() | -NPY002.py:30:1: NPY002 Replace legacy `np.random.dirichlet` call with `np.random.Generator` +NPY002.py:33:1: NPY002 Replace legacy `np.random.dirichlet` call with `np.random.Generator` | -28 | numpy.random.binomial() -29 | numpy.random.chisquare() -30 | numpy.random.dirichlet() +31 | numpy.random.binomial() +32 | numpy.random.chisquare() +33 | numpy.random.dirichlet() | ^^^^^^^^^^^^^^^^^^^^^^ NPY002 -31 | numpy.random.exponential() -32 | numpy.random.f() +34 | numpy.random.exponential() +35 | numpy.random.f() | -NPY002.py:31:1: NPY002 Replace legacy `np.random.exponential` call with `np.random.Generator` +NPY002.py:34:1: NPY002 Replace legacy `np.random.exponential` call with `np.random.Generator` | -29 | numpy.random.chisquare() -30 | numpy.random.dirichlet() -31 | numpy.random.exponential() +32 | numpy.random.chisquare() +33 | numpy.random.dirichlet() +34 | numpy.random.exponential() | ^^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -32 | numpy.random.f() -33 | numpy.random.gamma() +35 | numpy.random.f() +36 | numpy.random.gamma() | -NPY002.py:32:1: NPY002 Replace legacy `np.random.f` call with `np.random.Generator` +NPY002.py:35:1: NPY002 Replace legacy `np.random.f` call with `np.random.Generator` | -30 | numpy.random.dirichlet() -31 | numpy.random.exponential() -32 | numpy.random.f() +33 | numpy.random.dirichlet() +34 | numpy.random.exponential() +35 | numpy.random.f() | ^^^^^^^^^^^^^^ NPY002 -33 | numpy.random.gamma() -34 | numpy.random.geometric() +36 | numpy.random.gamma() +37 | numpy.random.geometric() | -NPY002.py:33:1: NPY002 Replace legacy `np.random.gamma` call with `np.random.Generator` +NPY002.py:36:1: NPY002 Replace legacy `np.random.gamma` call with `np.random.Generator` | -31 | numpy.random.exponential() -32 | numpy.random.f() -33 | numpy.random.gamma() +34 | numpy.random.exponential() +35 | numpy.random.f() +36 | numpy.random.gamma() | ^^^^^^^^^^^^^^^^^^ NPY002 -34 | numpy.random.geometric() -35 | numpy.random.get_state() +37 | numpy.random.geometric() +38 | numpy.random.get_state() | -NPY002.py:34:1: NPY002 Replace legacy `np.random.geometric` call with `np.random.Generator` +NPY002.py:37:1: NPY002 Replace legacy `np.random.geometric` call with `np.random.Generator` | -32 | numpy.random.f() -33 | numpy.random.gamma() -34 | numpy.random.geometric() +35 | numpy.random.f() +36 | numpy.random.gamma() +37 | numpy.random.geometric() | ^^^^^^^^^^^^^^^^^^^^^^ NPY002 -35 | numpy.random.get_state() -36 | numpy.random.gumbel() +38 | numpy.random.get_state() +39 | numpy.random.gumbel() | -NPY002.py:35:1: NPY002 Replace legacy `np.random.get_state` call with `np.random.Generator` +NPY002.py:38:1: NPY002 Replace legacy `np.random.get_state` call with `np.random.Generator` | -33 | numpy.random.gamma() -34 | numpy.random.geometric() -35 | numpy.random.get_state() +36 | numpy.random.gamma() +37 | numpy.random.geometric() +38 | numpy.random.get_state() | ^^^^^^^^^^^^^^^^^^^^^^ NPY002 -36 | numpy.random.gumbel() -37 | numpy.random.hypergeometric() +39 | numpy.random.gumbel() +40 | numpy.random.hypergeometric() | -NPY002.py:36:1: NPY002 Replace legacy `np.random.gumbel` call with `np.random.Generator` +NPY002.py:39:1: NPY002 Replace legacy `np.random.gumbel` call with `np.random.Generator` | -34 | numpy.random.geometric() -35 | numpy.random.get_state() -36 | numpy.random.gumbel() +37 | numpy.random.geometric() +38 | numpy.random.get_state() +39 | numpy.random.gumbel() | ^^^^^^^^^^^^^^^^^^^ NPY002 -37 | numpy.random.hypergeometric() -38 | numpy.random.laplace() +40 | numpy.random.hypergeometric() +41 | numpy.random.laplace() | -NPY002.py:37:1: NPY002 Replace legacy `np.random.hypergeometric` call with `np.random.Generator` +NPY002.py:40:1: NPY002 Replace legacy `np.random.hypergeometric` call with `np.random.Generator` | -35 | numpy.random.get_state() -36 | numpy.random.gumbel() -37 | numpy.random.hypergeometric() +38 | numpy.random.get_state() +39 | numpy.random.gumbel() +40 | numpy.random.hypergeometric() | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -38 | numpy.random.laplace() -39 | numpy.random.logistic() +41 | numpy.random.laplace() +42 | numpy.random.logistic() | -NPY002.py:38:1: NPY002 Replace legacy `np.random.laplace` call with `np.random.Generator` +NPY002.py:41:1: NPY002 Replace legacy `np.random.laplace` call with `np.random.Generator` | -36 | numpy.random.gumbel() -37 | numpy.random.hypergeometric() -38 | numpy.random.laplace() +39 | numpy.random.gumbel() +40 | numpy.random.hypergeometric() +41 | numpy.random.laplace() | ^^^^^^^^^^^^^^^^^^^^ NPY002 -39 | numpy.random.logistic() -40 | numpy.random.lognormal() +42 | numpy.random.logistic() +43 | numpy.random.lognormal() | -NPY002.py:39:1: NPY002 Replace legacy `np.random.logistic` call with `np.random.Generator` +NPY002.py:42:1: NPY002 Replace legacy `np.random.logistic` call with `np.random.Generator` | -37 | numpy.random.hypergeometric() -38 | numpy.random.laplace() -39 | numpy.random.logistic() +40 | numpy.random.hypergeometric() +41 | numpy.random.laplace() +42 | numpy.random.logistic() | ^^^^^^^^^^^^^^^^^^^^^ NPY002 -40 | numpy.random.lognormal() -41 | numpy.random.logseries() +43 | numpy.random.lognormal() +44 | numpy.random.logseries() | -NPY002.py:40:1: NPY002 Replace legacy `np.random.lognormal` call with `np.random.Generator` +NPY002.py:43:1: NPY002 Replace legacy `np.random.lognormal` call with `np.random.Generator` | -38 | numpy.random.laplace() -39 | numpy.random.logistic() -40 | numpy.random.lognormal() +41 | numpy.random.laplace() +42 | numpy.random.logistic() +43 | numpy.random.lognormal() | ^^^^^^^^^^^^^^^^^^^^^^ NPY002 -41 | numpy.random.logseries() -42 | numpy.random.multinomial() +44 | numpy.random.logseries() +45 | numpy.random.multinomial() | -NPY002.py:41:1: NPY002 Replace legacy `np.random.logseries` call with `np.random.Generator` +NPY002.py:44:1: NPY002 Replace legacy `np.random.logseries` call with `np.random.Generator` | -39 | numpy.random.logistic() -40 | numpy.random.lognormal() -41 | numpy.random.logseries() +42 | numpy.random.logistic() +43 | numpy.random.lognormal() +44 | numpy.random.logseries() | ^^^^^^^^^^^^^^^^^^^^^^ NPY002 -42 | numpy.random.multinomial() -43 | numpy.random.multivariate_normal() +45 | numpy.random.multinomial() +46 | numpy.random.multivariate_normal() | -NPY002.py:42:1: NPY002 Replace legacy `np.random.multinomial` call with `np.random.Generator` +NPY002.py:45:1: NPY002 Replace legacy `np.random.multinomial` call with `np.random.Generator` | -40 | numpy.random.lognormal() -41 | numpy.random.logseries() -42 | numpy.random.multinomial() +43 | numpy.random.lognormal() +44 | numpy.random.logseries() +45 | numpy.random.multinomial() | ^^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -43 | numpy.random.multivariate_normal() -44 | numpy.random.negative_binomial() +46 | numpy.random.multivariate_normal() +47 | numpy.random.negative_binomial() | -NPY002.py:43:1: NPY002 Replace legacy `np.random.multivariate_normal` call with `np.random.Generator` +NPY002.py:46:1: NPY002 Replace legacy `np.random.multivariate_normal` call with `np.random.Generator` | -41 | numpy.random.logseries() -42 | numpy.random.multinomial() -43 | numpy.random.multivariate_normal() +44 | numpy.random.logseries() +45 | numpy.random.multinomial() +46 | numpy.random.multivariate_normal() | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -44 | numpy.random.negative_binomial() -45 | numpy.random.noncentral_chisquare() +47 | numpy.random.negative_binomial() +48 | numpy.random.noncentral_chisquare() | -NPY002.py:44:1: NPY002 Replace legacy `np.random.negative_binomial` call with `np.random.Generator` +NPY002.py:47:1: NPY002 Replace legacy `np.random.negative_binomial` call with `np.random.Generator` | -42 | numpy.random.multinomial() -43 | numpy.random.multivariate_normal() -44 | numpy.random.negative_binomial() +45 | numpy.random.multinomial() +46 | numpy.random.multivariate_normal() +47 | numpy.random.negative_binomial() | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -45 | numpy.random.noncentral_chisquare() -46 | numpy.random.noncentral_f() +48 | numpy.random.noncentral_chisquare() +49 | numpy.random.noncentral_f() | -NPY002.py:45:1: NPY002 Replace legacy `np.random.noncentral_chisquare` call with `np.random.Generator` +NPY002.py:48:1: NPY002 Replace legacy `np.random.noncentral_chisquare` call with `np.random.Generator` | -43 | numpy.random.multivariate_normal() -44 | numpy.random.negative_binomial() -45 | numpy.random.noncentral_chisquare() +46 | numpy.random.multivariate_normal() +47 | numpy.random.negative_binomial() +48 | numpy.random.noncentral_chisquare() | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -46 | numpy.random.noncentral_f() -47 | numpy.random.normal() +49 | numpy.random.noncentral_f() +50 | numpy.random.normal() | -NPY002.py:46:1: NPY002 Replace legacy `np.random.noncentral_f` call with `np.random.Generator` +NPY002.py:49:1: NPY002 Replace legacy `np.random.noncentral_f` call with `np.random.Generator` | -44 | numpy.random.negative_binomial() -45 | numpy.random.noncentral_chisquare() -46 | numpy.random.noncentral_f() +47 | numpy.random.negative_binomial() +48 | numpy.random.noncentral_chisquare() +49 | numpy.random.noncentral_f() | ^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -47 | numpy.random.normal() -48 | numpy.random.pareto() +50 | numpy.random.normal() +51 | numpy.random.pareto() | -NPY002.py:47:1: NPY002 Replace legacy `np.random.normal` call with `np.random.Generator` +NPY002.py:50:1: NPY002 Replace legacy `np.random.normal` call with `np.random.Generator` | -45 | numpy.random.noncentral_chisquare() -46 | numpy.random.noncentral_f() -47 | numpy.random.normal() +48 | numpy.random.noncentral_chisquare() +49 | numpy.random.noncentral_f() +50 | numpy.random.normal() | ^^^^^^^^^^^^^^^^^^^ NPY002 -48 | numpy.random.pareto() -49 | numpy.random.poisson() +51 | numpy.random.pareto() +52 | numpy.random.poisson() | -NPY002.py:48:1: NPY002 Replace legacy `np.random.pareto` call with `np.random.Generator` +NPY002.py:51:1: NPY002 Replace legacy `np.random.pareto` call with `np.random.Generator` | -46 | numpy.random.noncentral_f() -47 | numpy.random.normal() -48 | numpy.random.pareto() +49 | numpy.random.noncentral_f() +50 | numpy.random.normal() +51 | numpy.random.pareto() | ^^^^^^^^^^^^^^^^^^^ NPY002 -49 | numpy.random.poisson() -50 | numpy.random.power() +52 | numpy.random.poisson() +53 | numpy.random.power() | -NPY002.py:49:1: NPY002 Replace legacy `np.random.poisson` call with `np.random.Generator` +NPY002.py:52:1: NPY002 Replace legacy `np.random.poisson` call with `np.random.Generator` | -47 | numpy.random.normal() -48 | numpy.random.pareto() -49 | numpy.random.poisson() +50 | numpy.random.normal() +51 | numpy.random.pareto() +52 | numpy.random.poisson() | ^^^^^^^^^^^^^^^^^^^^ NPY002 -50 | numpy.random.power() -51 | numpy.random.rayleigh() +53 | numpy.random.power() +54 | numpy.random.rayleigh() | -NPY002.py:50:1: NPY002 Replace legacy `np.random.power` call with `np.random.Generator` +NPY002.py:53:1: NPY002 Replace legacy `np.random.power` call with `np.random.Generator` | -48 | numpy.random.pareto() -49 | numpy.random.poisson() -50 | numpy.random.power() +51 | numpy.random.pareto() +52 | numpy.random.poisson() +53 | numpy.random.power() | ^^^^^^^^^^^^^^^^^^ NPY002 -51 | numpy.random.rayleigh() -52 | numpy.random.standard_cauchy() +54 | numpy.random.rayleigh() +55 | numpy.random.standard_cauchy() | -NPY002.py:51:1: NPY002 Replace legacy `np.random.rayleigh` call with `np.random.Generator` +NPY002.py:54:1: NPY002 Replace legacy `np.random.rayleigh` call with `np.random.Generator` | -49 | numpy.random.poisson() -50 | numpy.random.power() -51 | numpy.random.rayleigh() +52 | numpy.random.poisson() +53 | numpy.random.power() +54 | numpy.random.rayleigh() | ^^^^^^^^^^^^^^^^^^^^^ NPY002 -52 | numpy.random.standard_cauchy() -53 | numpy.random.standard_exponential() +55 | numpy.random.standard_cauchy() +56 | numpy.random.standard_exponential() | -NPY002.py:52:1: NPY002 Replace legacy `np.random.standard_cauchy` call with `np.random.Generator` +NPY002.py:55:1: NPY002 Replace legacy `np.random.standard_cauchy` call with `np.random.Generator` | -50 | numpy.random.power() -51 | numpy.random.rayleigh() -52 | numpy.random.standard_cauchy() +53 | numpy.random.power() +54 | numpy.random.rayleigh() +55 | numpy.random.standard_cauchy() | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -53 | numpy.random.standard_exponential() -54 | numpy.random.standard_gamma() +56 | numpy.random.standard_exponential() +57 | numpy.random.standard_gamma() | -NPY002.py:53:1: NPY002 Replace legacy `np.random.standard_exponential` call with `np.random.Generator` +NPY002.py:56:1: NPY002 Replace legacy `np.random.standard_exponential` call with `np.random.Generator` | -51 | numpy.random.rayleigh() -52 | numpy.random.standard_cauchy() -53 | numpy.random.standard_exponential() +54 | numpy.random.rayleigh() +55 | numpy.random.standard_cauchy() +56 | numpy.random.standard_exponential() | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -54 | numpy.random.standard_gamma() -55 | numpy.random.standard_normal() +57 | numpy.random.standard_gamma() +58 | numpy.random.standard_normal() | -NPY002.py:54:1: NPY002 Replace legacy `np.random.standard_gamma` call with `np.random.Generator` +NPY002.py:57:1: NPY002 Replace legacy `np.random.standard_gamma` call with `np.random.Generator` | -52 | numpy.random.standard_cauchy() -53 | numpy.random.standard_exponential() -54 | numpy.random.standard_gamma() +55 | numpy.random.standard_cauchy() +56 | numpy.random.standard_exponential() +57 | numpy.random.standard_gamma() | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -55 | numpy.random.standard_normal() -56 | numpy.random.standard_t() +58 | numpy.random.standard_normal() +59 | numpy.random.standard_t() | -NPY002.py:55:1: NPY002 Replace legacy `np.random.standard_normal` call with `np.random.Generator` +NPY002.py:58:1: NPY002 Replace legacy `np.random.standard_normal` call with `np.random.Generator` | -53 | numpy.random.standard_exponential() -54 | numpy.random.standard_gamma() -55 | numpy.random.standard_normal() +56 | numpy.random.standard_exponential() +57 | numpy.random.standard_gamma() +58 | numpy.random.standard_normal() | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -56 | numpy.random.standard_t() -57 | numpy.random.triangular() +59 | numpy.random.standard_t() +60 | numpy.random.triangular() | -NPY002.py:56:1: NPY002 Replace legacy `np.random.standard_t` call with `np.random.Generator` +NPY002.py:59:1: NPY002 Replace legacy `np.random.standard_t` call with `np.random.Generator` | -54 | numpy.random.standard_gamma() -55 | numpy.random.standard_normal() -56 | numpy.random.standard_t() +57 | numpy.random.standard_gamma() +58 | numpy.random.standard_normal() +59 | numpy.random.standard_t() | ^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -57 | numpy.random.triangular() -58 | numpy.random.uniform() +60 | numpy.random.triangular() +61 | numpy.random.uniform() | -NPY002.py:57:1: NPY002 Replace legacy `np.random.triangular` call with `np.random.Generator` +NPY002.py:60:1: NPY002 Replace legacy `np.random.triangular` call with `np.random.Generator` | -55 | numpy.random.standard_normal() -56 | numpy.random.standard_t() -57 | numpy.random.triangular() +58 | numpy.random.standard_normal() +59 | numpy.random.standard_t() +60 | numpy.random.triangular() | ^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -58 | numpy.random.uniform() -59 | numpy.random.vonmises() +61 | numpy.random.uniform() +62 | numpy.random.vonmises() | -NPY002.py:58:1: NPY002 Replace legacy `np.random.uniform` call with `np.random.Generator` +NPY002.py:61:1: NPY002 Replace legacy `np.random.uniform` call with `np.random.Generator` | -56 | numpy.random.standard_t() -57 | numpy.random.triangular() -58 | numpy.random.uniform() +59 | numpy.random.standard_t() +60 | numpy.random.triangular() +61 | numpy.random.uniform() | ^^^^^^^^^^^^^^^^^^^^ NPY002 -59 | numpy.random.vonmises() -60 | numpy.random.wald() +62 | numpy.random.vonmises() +63 | numpy.random.wald() | -NPY002.py:59:1: NPY002 Replace legacy `np.random.vonmises` call with `np.random.Generator` +NPY002.py:62:1: NPY002 Replace legacy `np.random.vonmises` call with `np.random.Generator` | -57 | numpy.random.triangular() -58 | numpy.random.uniform() -59 | numpy.random.vonmises() +60 | numpy.random.triangular() +61 | numpy.random.uniform() +62 | numpy.random.vonmises() | ^^^^^^^^^^^^^^^^^^^^^ NPY002 -60 | numpy.random.wald() -61 | numpy.random.weibull() +63 | numpy.random.wald() +64 | numpy.random.weibull() | -NPY002.py:60:1: NPY002 Replace legacy `np.random.wald` call with `np.random.Generator` +NPY002.py:63:1: NPY002 Replace legacy `np.random.wald` call with `np.random.Generator` | -58 | numpy.random.uniform() -59 | numpy.random.vonmises() -60 | numpy.random.wald() +61 | numpy.random.uniform() +62 | numpy.random.vonmises() +63 | numpy.random.wald() | ^^^^^^^^^^^^^^^^^ NPY002 -61 | numpy.random.weibull() -62 | numpy.random.zipf() +64 | numpy.random.weibull() +65 | numpy.random.zipf() | -NPY002.py:61:1: NPY002 Replace legacy `np.random.weibull` call with `np.random.Generator` +NPY002.py:64:1: NPY002 Replace legacy `np.random.weibull` call with `np.random.Generator` | -59 | numpy.random.vonmises() -60 | numpy.random.wald() -61 | numpy.random.weibull() +62 | numpy.random.vonmises() +63 | numpy.random.wald() +64 | numpy.random.weibull() | ^^^^^^^^^^^^^^^^^^^^ NPY002 -62 | numpy.random.zipf() +65 | numpy.random.zipf() | -NPY002.py:62:1: NPY002 Replace legacy `np.random.zipf` call with `np.random.Generator` +NPY002.py:65:1: NPY002 Replace legacy `np.random.zipf` call with `np.random.Generator` | -60 | numpy.random.wald() -61 | numpy.random.weibull() -62 | numpy.random.zipf() +63 | numpy.random.wald() +64 | numpy.random.weibull() +65 | numpy.random.zipf() | ^^^^^^^^^^^^^^^^^ NPY002 | diff --git a/crates/ruff/src/rules/pandas_vet/fixes.rs b/crates/ruff/src/rules/pandas_vet/fixes.rs deleted file mode 100644 index 23ac1a5c3e..0000000000 --- a/crates/ruff/src/rules/pandas_vet/fixes.rs +++ /dev/null @@ -1,45 +0,0 @@ -use ruff_text_size::TextRange; -use rustpython_parser::ast::{self, Expr, Keyword, Ranged}; - -use ruff_diagnostics::{Edit, Fix}; -use ruff_python_ast::source_code::Locator; - -use crate::autofix::edits::remove_argument; - -fn match_name(expr: &Expr) -> Option<&str> { - if let Expr::Call(ast::ExprCall { func, .. }) = expr { - if let Expr::Attribute(ast::ExprAttribute { value, .. }) = func.as_ref() { - if let Expr::Name(ast::ExprName { id, .. }) = value.as_ref() { - return Some(id); - } - } - } - None -} - -/// Remove the `inplace` argument from a function call and replace it with an -/// assignment. -pub(super) fn convert_inplace_argument_to_assignment( - locator: &Locator, - expr: &Expr, - violation_range: TextRange, - args: &[Expr], - keywords: &[Keyword], -) -> Option { - // Add the assignment. - let name = match_name(expr)?; - let insert_assignment = Edit::insertion(format!("{name} = "), expr.start()); - - // Remove the `inplace` argument. - let remove_argument = remove_argument( - locator, - expr.start(), - violation_range, - args, - keywords, - false, - ) - .ok()?; - #[allow(deprecated)] - Some(Fix::unspecified_edits(insert_assignment, [remove_argument])) -} diff --git a/crates/ruff/src/rules/pandas_vet/helpers.rs b/crates/ruff/src/rules/pandas_vet/helpers.rs index d011be8daf..b849af1f80 100644 --- a/crates/ruff/src/rules/pandas_vet/helpers.rs +++ b/crates/ruff/src/rules/pandas_vet/helpers.rs @@ -1,8 +1,7 @@ use rustpython_parser::ast; use rustpython_parser::ast::Expr; -use ruff_python_semantic::binding::{BindingKind, Importation}; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::{BindingKind, Import, SemanticModel}; pub(super) enum Resolution { /// The expression resolves to an irrelevant expression type (e.g., a constant). @@ -16,7 +15,7 @@ pub(super) enum Resolution { } /// Test an [`Expr`] for relevance to Pandas-related operations. -pub(super) fn test_expression(expr: &Expr, model: &SemanticModel) -> Resolution { +pub(super) fn test_expression(expr: &Expr, semantic: &SemanticModel) -> Resolution { match expr { Expr::Constant(_) | Expr::Tuple(_) @@ -28,7 +27,7 @@ pub(super) fn test_expression(expr: &Expr, model: &SemanticModel) -> Resolution | Expr::DictComp(_) | Expr::GeneratorExp(_) => Resolution::IrrelevantExpression, Expr::Name(ast::ExprName { id, .. }) => { - model + semantic .find_binding(id) .map_or(Resolution::IrrelevantBinding, |binding| { match binding.kind { @@ -39,8 +38,8 @@ pub(super) fn test_expression(expr: &Expr, model: &SemanticModel) -> Resolution | BindingKind::UnpackedAssignment | BindingKind::LoopVar | BindingKind::Global - | BindingKind::Nonlocal => Resolution::RelevantLocal, - BindingKind::Importation(Importation { + | BindingKind::Nonlocal(_) => Resolution::RelevantLocal, + BindingKind::Import(Import { qualified_name: module, }) if module == "pandas" => Resolution::PandasModule, _ => Resolution::IrrelevantBinding, diff --git a/crates/ruff/src/rules/pandas_vet/mod.rs b/crates/ruff/src/rules/pandas_vet/mod.rs index d3ac303cbc..295a83cd24 100644 --- a/crates/ruff/src/rules/pandas_vet/mod.rs +++ b/crates/ruff/src/rules/pandas_vet/mod.rs @@ -1,5 +1,4 @@ //! Rules from [pandas-vet](https://pypi.org/project/pandas-vet/). -pub(crate) mod fixes; pub(crate) mod helpers; pub(crate) mod rules; @@ -354,8 +353,10 @@ mod tests { "PD901_fail_df_var" )] fn contents(contents: &str, snapshot: &str) { - let diagnostics = - test_snippet(contents, &settings::Settings::for_rules(&Linter::PandasVet)); + let diagnostics = test_snippet( + contents, + &settings::Settings::for_rules(Linter::PandasVet.rules()), + ); assert_messages!(snapshot, diagnostics); } diff --git a/crates/ruff/src/rules/pandas_vet/rules/assignment_to_df.rs b/crates/ruff/src/rules/pandas_vet/rules/assignment_to_df.rs index 6473d4a7a9..ba66f2950c 100644 --- a/crates/ruff/src/rules/pandas_vet/rules/assignment_to_df.rs +++ b/crates/ruff/src/rules/pandas_vet/rules/assignment_to_df.rs @@ -3,6 +3,29 @@ use rustpython_parser::ast::{self, Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +/// ## What it does +/// Checks for assignments to the variable `df`. +/// +/// ## Why is this bad? +/// Although `df` is a common variable name for a Pandas DataFrame, it's not a +/// great variable name for production code, as it's non-descriptive and +/// prone to name conflicts. +/// +/// Instead, use a more descriptive variable name. +/// +/// ## Example +/// ```python +/// import pandas as pd +/// +/// df = pd.read_csv("animals.csv") +/// ``` +/// +/// Use instead: +/// ```python +/// import pandas as pd +/// +/// animals = pd.read_csv("animals.csv") +/// ``` #[violation] pub struct PandasDfVariableName; diff --git a/crates/ruff/src/rules/pandas_vet/rules/attr.rs b/crates/ruff/src/rules/pandas_vet/rules/attr.rs index ccdb5616b1..cb2ec2398d 100644 --- a/crates/ruff/src/rules/pandas_vet/rules/attr.rs +++ b/crates/ruff/src/rules/pandas_vet/rules/attr.rs @@ -8,6 +8,32 @@ use crate::checkers::ast::Checker; use crate::registry::Rule; use crate::rules::pandas_vet::helpers::{test_expression, Resolution}; +/// ## What it does +/// Checks for uses of `.values` on Pandas Series and Index objects. +/// +/// ## Why is this bad? +/// The `.values` attribute is ambiguous as it's return type is unclear. As +/// such, it is no longer recommended by the Pandas documentation. +/// +/// Instead, use `.to_numpy()` to return a NumPy array, or `.array` to return a +/// Pandas `ExtensionArray`. +/// +/// ## Example +/// ```python +/// import pandas as pd +/// +/// animals = pd.read_csv("animals.csv").values # Ambiguous. +/// ``` +/// +/// Use instead: +/// ```python +/// import pandas as pd +/// +/// animals = pd.read_csv("animals.csv").to_numpy() # Explicit. +/// ``` +/// +/// ## References +/// - [Pandas documentation: Accessing the values in a Series or Index](https://pandas.pydata.org/pandas-docs/stable/whatsnew/v0.24.0.html#accessing-the-values-in-a-series-or-index) #[violation] pub struct PandasUseOfDotValues; @@ -19,14 +45,15 @@ impl Violation for PandasUseOfDotValues { } pub(crate) fn attr(checker: &mut Checker, attr: &str, value: &Expr, attr_expr: &Expr) { - let rules = &checker.settings.rules; let violation: DiagnosticKind = match attr { - "values" if rules.enabled(Rule::PandasUseOfDotValues) => PandasUseOfDotValues.into(), + "values" if checker.settings.rules.enabled(Rule::PandasUseOfDotValues) => { + PandasUseOfDotValues.into() + } _ => return, }; // Avoid flagging on function calls (e.g., `df.values()`). - if let Some(parent) = checker.semantic_model().expr_parent() { + if let Some(parent) = checker.semantic().expr_parent() { if matches!(parent, Expr::Call(_)) { return; } @@ -35,7 +62,7 @@ pub(crate) fn attr(checker: &mut Checker, attr: &str, value: &Expr, attr_expr: & // Avoid flagging on non-DataFrames (e.g., `{"a": 1}.values`), and on irrelevant bindings // (like imports). if !matches!( - test_expression(value, checker.semantic_model()), + test_expression(value, checker.semantic()), Resolution::RelevantLocal ) { return; diff --git a/crates/ruff/src/rules/pandas_vet/rules/call.rs b/crates/ruff/src/rules/pandas_vet/rules/call.rs index ecea41b6f8..74bf8de249 100644 --- a/crates/ruff/src/rules/pandas_vet/rules/call.rs +++ b/crates/ruff/src/rules/pandas_vet/rules/call.rs @@ -8,6 +8,36 @@ use crate::checkers::ast::Checker; use crate::registry::Rule; use crate::rules::pandas_vet::helpers::{test_expression, Resolution}; +/// ## What it does +/// Checks for uses of `.isnull` on Pandas objects. +/// +/// ## Why is this bad? +/// In the Pandas API, `.isna` and `.isnull` are equivalent. For consistency, +/// prefer `.isna` over `.isnull`. +/// +/// As a name, `.isna` more accurately reflects the behavior of the method, +/// since these methods check for `NaN` and `NaT` values in addition to `None` +/// values. +/// +/// ## Example +/// ```python +/// import pandas as pd +/// +/// animals_df = pd.read_csv("animals.csv") +/// pd.isnull(animals_df) +/// ``` +/// +/// Use instead: +/// ```python +/// import pandas as pd +/// +/// animals_df = pd.read_csv("animals.csv") +/// pd.isna(animals_df) +/// ``` +/// +/// ## References +/// - [Pandas documentation: `isnull`](https://pandas.pydata.org/docs/reference/api/pandas.isnull.html#pandas.isnull) +/// - [Pandas documentation: `isna`](https://pandas.pydata.org/docs/reference/api/pandas.isna.html#pandas.isna) #[violation] pub struct PandasUseOfDotIsNull; @@ -18,6 +48,36 @@ impl Violation for PandasUseOfDotIsNull { } } +/// ## What it does +/// Checks for uses of `.notnull` on Pandas objects. +/// +/// ## Why is this bad? +/// In the Pandas API, `.notna` and `.notnull` are equivalent. For consistency, +/// prefer `.notna` over `.notnull`. +/// +/// As a name, `.notna` more accurately reflects the behavior of the method, +/// since these methods check for `NaN` and `NaT` values in addition to `None` +/// values. +/// +/// ## Example +/// ```python +/// import pandas as pd +/// +/// animals_df = pd.read_csv("animals.csv") +/// pd.notnull(animals_df) +/// ``` +/// +/// Use instead: +/// ```python +/// import pandas as pd +/// +/// animals_df = pd.read_csv("animals.csv") +/// pd.notna(animals_df) +/// ``` +/// +/// ## References +/// - [Pandas documentation: `notnull`](https://pandas.pydata.org/docs/reference/api/pandas.notnull.html#pandas.notnull) +/// - [Pandas documentation: `notna`](https://pandas.pydata.org/docs/reference/api/pandas.notna.html#pandas.notna) #[violation] pub struct PandasUseOfDotNotNull; @@ -28,6 +88,32 @@ impl Violation for PandasUseOfDotNotNull { } } +/// ## What it does +/// Checks for uses of `.pivot` or `.unstack` on Pandas objects. +/// +/// ## Why is this bad? +/// Prefer `.pivot_table` to `.pivot` or `.unstack`. `.pivot_table` is more general +/// and can be used to implement the same behavior as `.pivot` and `.unstack`. +/// +/// ## Example +/// ```python +/// import pandas as pd +/// +/// df = pd.read_csv("cities.csv") +/// df.pivot(index="city", columns="year", values="population") +/// ``` +/// +/// Use instead: +/// ```python +/// import pandas as pd +/// +/// df = pd.read_csv("cities.csv") +/// df.pivot_table(index="city", columns="year", values="population") +/// ``` +/// +/// ## References +/// - [Pandas documentation: Reshaping and pivot tables](https://pandas.pydata.org/docs/user_guide/reshaping.html) +/// - [Pandas documentation: `pivot_table`](https://pandas.pydata.org/docs/reference/api/pandas.pivot_table.html#pandas.pivot_table) #[violation] pub struct PandasUseOfDotPivotOrUnstack; @@ -40,6 +126,8 @@ impl Violation for PandasUseOfDotPivotOrUnstack { } } +// TODO(tjkuson): Add documentation for this rule once clarified. +// https://github.com/astral-sh/ruff/issues/5628 #[violation] pub struct PandasUseOfDotReadTable; @@ -50,6 +138,32 @@ impl Violation for PandasUseOfDotReadTable { } } +/// ## What it does +/// Checks for uses of `.stack` on Pandas objects. +/// +/// ## Why is this bad? +/// Prefer `.melt` to `.stack`, which has the same functionality but with +/// support for direct column renaming and no dependence on `MultiIndex`. +/// +/// ## Example +/// ```python +/// import pandas as pd +/// +/// cities_df = pd.read_csv("cities.csv") +/// cities_df.set_index("city").stack() +/// ``` +/// +/// Use instead: +/// ```python +/// import pandas as pd +/// +/// cities_df = pd.read_csv("cities.csv") +/// cities_df.melt(id_vars="city") +/// ``` +/// +/// ## References +/// - [Pandas documentation: `melt`](https://pandas.pydata.org/docs/reference/api/pandas.melt.html) +/// - [Pandas documentation: `stack`](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.stack.html) #[violation] pub struct PandasUseOfDotStack; @@ -61,26 +175,41 @@ impl Violation for PandasUseOfDotStack { } pub(crate) fn call(checker: &mut Checker, func: &Expr) { - let rules = &checker.settings.rules; - let Expr::Attribute(ast::ExprAttribute { value, attr, .. } )= func else { + let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = func else { return; }; let violation: DiagnosticKind = match attr.as_str() { - "isnull" if rules.enabled(Rule::PandasUseOfDotIsNull) => PandasUseOfDotIsNull.into(), - "notnull" if rules.enabled(Rule::PandasUseOfDotNotNull) => PandasUseOfDotNotNull.into(), - "pivot" | "unstack" if rules.enabled(Rule::PandasUseOfDotPivotOrUnstack) => { + "isnull" if checker.settings.rules.enabled(Rule::PandasUseOfDotIsNull) => { + PandasUseOfDotIsNull.into() + } + "notnull" if checker.settings.rules.enabled(Rule::PandasUseOfDotNotNull) => { + PandasUseOfDotNotNull.into() + } + "pivot" | "unstack" + if checker + .settings + .rules + .enabled(Rule::PandasUseOfDotPivotOrUnstack) => + { PandasUseOfDotPivotOrUnstack.into() } - "read_table" if rules.enabled(Rule::PandasUseOfDotReadTable) => { + "read_table" + if checker + .settings + .rules + .enabled(Rule::PandasUseOfDotReadTable) => + { PandasUseOfDotReadTable.into() } - "stack" if rules.enabled(Rule::PandasUseOfDotStack) => PandasUseOfDotStack.into(), + "stack" if checker.settings.rules.enabled(Rule::PandasUseOfDotStack) => { + PandasUseOfDotStack.into() + } _ => return, }; // Ignore irrelevant bindings (like imports). if !matches!( - test_expression(value, checker.semantic_model()), + test_expression(value, checker.semantic()), Resolution::RelevantLocal | Resolution::PandasModule ) { return; diff --git a/crates/ruff/src/rules/pandas_vet/rules/inplace_argument.rs b/crates/ruff/src/rules/pandas_vet/rules/inplace_argument.rs index ebb56710ce..98a44a58d6 100644 --- a/crates/ruff/src/rules/pandas_vet/rules/inplace_argument.rs +++ b/crates/ruff/src/rules/pandas_vet/rules/inplace_argument.rs @@ -1,12 +1,15 @@ -use rustpython_parser::ast::{self, Constant, Expr, Keyword, Ranged}; +use ruff_text_size::TextRange; +use rustpython_parser::ast::{Expr, Keyword, Ranged}; -use ruff_diagnostics::{AutofixKind, Diagnostic, Violation}; +use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::binding::{BindingKind, Importation}; +use ruff_python_ast::helpers::is_const_true; +use ruff_python_ast::source_code::Locator; +use ruff_python_semantic::{BindingKind, Import}; +use crate::autofix::edits::remove_argument; use crate::checkers::ast::Checker; use crate::registry::AsRule; -use crate::rules::pandas_vet::fixes::convert_inplace_argument_to_assignment; /// ## What it does /// Checks for `inplace=True` usages in `pandas` function and method @@ -50,47 +53,38 @@ impl Violation for PandasUseOfInplaceArgument { /// PD002 pub(crate) fn inplace_argument( - checker: &Checker, + checker: &mut Checker, expr: &Expr, func: &Expr, args: &[Expr], keywords: &[Keyword], -) -> Option { - let mut seen_star = false; - let mut is_checkable = false; - let mut is_pandas = false; - - if let Some(call_path) = checker.semantic_model().resolve_call_path(func) { - is_checkable = true; - - let module = call_path[0]; - is_pandas = checker - .semantic_model() - .find_binding(module) +) { + // If the function was imported from another module, and it's _not_ Pandas, abort. + if let Some(call_path) = checker.semantic().resolve_call_path(func) { + if !call_path + .first() + .and_then(|module| checker.semantic().find_binding(module)) .map_or(false, |binding| { matches!( binding.kind, - BindingKind::Importation(Importation { + BindingKind::Import(Import { qualified_name: "pandas" }) ) - }); + }) + { + return; + } } + let mut seen_star = false; for keyword in keywords.iter().rev() { let Some(arg) = &keyword.arg else { seen_star = true; continue; }; if arg == "inplace" { - let is_true_literal = match &keyword.value { - Expr::Constant(ast::ExprConstant { - value: Constant::Bool(boolean), - .. - }) => *boolean, - _ => false, - }; - if is_true_literal { + if is_const_true(&keyword.value) { let mut diagnostic = Diagnostic::new(PandasUseOfInplaceArgument, keyword.range()); if checker.patch(diagnostic.kind.rule()) { // Avoid applying the fix if: @@ -103,14 +97,14 @@ pub(crate) fn inplace_argument( // but we don't currently restore expression stacks when parsing deferred nodes, // and so the parent is lost. if !seen_star - && checker.semantic_model().stmt().is_expr_stmt() - && checker.semantic_model().expr_parent().is_none() - && !checker.semantic_model().scope().kind.is_lambda() + && checker.semantic().stmt().is_expr_stmt() + && checker.semantic().expr_parent().is_none() + && !checker.semantic().scope().kind.is_lambda() { if let Some(fix) = convert_inplace_argument_to_assignment( checker.locator, expr, - diagnostic.range(), + keyword.range(), args, keywords, ) { @@ -119,18 +113,35 @@ pub(crate) fn inplace_argument( } } - // Without a static type system, only module-level functions could potentially be - // non-pandas calls. If they're not, `inplace` should be considered safe. - if is_checkable && !is_pandas { - return None; - } - - return Some(diagnostic); + checker.diagnostics.push(diagnostic); } // Duplicate keywords is a syntax error, so we can stop here. break; } } - None +} + +/// Remove the `inplace` argument from a function call and replace it with an +/// assignment. +fn convert_inplace_argument_to_assignment( + locator: &Locator, + expr: &Expr, + expr_range: TextRange, + args: &[Expr], + keywords: &[Keyword], +) -> Option { + // Add the assignment. + let call = expr.as_call_expr()?; + let attr = call.func.as_attribute_expr()?; + let insert_assignment = Edit::insertion( + format!("{name} = ", name = locator.slice(attr.value.range())), + expr.start(), + ); + + // Remove the `inplace` argument. + let remove_argument = + remove_argument(locator, call.func.end(), expr_range, args, keywords, false).ok()?; + + Some(Fix::suggested_edits(insert_assignment, [remove_argument])) } diff --git a/crates/ruff/src/rules/pandas_vet/rules/mod.rs b/crates/ruff/src/rules/pandas_vet/rules/mod.rs index 917c9a2efa..a0c1ef0908 100644 --- a/crates/ruff/src/rules/pandas_vet/rules/mod.rs +++ b/crates/ruff/src/rules/pandas_vet/rules/mod.rs @@ -1,12 +1,9 @@ -pub(crate) use assignment_to_df::{assignment_to_df, PandasDfVariableName}; -pub(crate) use attr::{attr, PandasUseOfDotValues}; -pub(crate) use call::{ - call, PandasUseOfDotIsNull, PandasUseOfDotNotNull, PandasUseOfDotPivotOrUnstack, - PandasUseOfDotReadTable, PandasUseOfDotStack, -}; -pub(crate) use inplace_argument::{inplace_argument, PandasUseOfInplaceArgument}; -pub(crate) use pd_merge::{use_of_pd_merge, PandasUseOfPdMerge}; -pub(crate) use subscript::{subscript, PandasUseOfDotAt, PandasUseOfDotIat, PandasUseOfDotIx}; +pub(crate) use assignment_to_df::*; +pub(crate) use attr::*; +pub(crate) use call::*; +pub(crate) use inplace_argument::*; +pub(crate) use pd_merge::*; +pub(crate) use subscript::*; pub(crate) mod assignment_to_df; pub(crate) mod attr; diff --git a/crates/ruff/src/rules/pandas_vet/rules/pd_merge.rs b/crates/ruff/src/rules/pandas_vet/rules/pd_merge.rs index b6c4120d7d..a515cd3639 100644 --- a/crates/ruff/src/rules/pandas_vet/rules/pd_merge.rs +++ b/crates/ruff/src/rules/pandas_vet/rules/pd_merge.rs @@ -1,8 +1,45 @@ use rustpython_parser::ast::{self, Expr, Ranged}; +use crate::checkers::ast::Checker; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +/// ## What it does +/// Checks for uses of `pd.merge` on Pandas objects. +/// +/// ## Why is this bad? +/// In Pandas, the `.merge` method (exposed on, e.g., DataFrame objects) and +/// the `pd.merge` function (exposed on the Pandas module) are equivalent. +/// +/// For consistency, prefer calling `.merge` on an object over calling +/// `pd.merge` on the Pandas module, as the former is more idiomatic. +/// +/// Further, `pd.merge` is not a method, but a function, which prohibits it +/// from being used in method chains, a common pattern in Pandas code. +/// +/// ## Example +/// ```python +/// import pandas as pd +/// +/// cats_df = pd.read_csv("cats.csv") +/// dogs_df = pd.read_csv("dogs.csv") +/// rabbits_df = pd.read_csv("rabbits.csv") +/// pets_df = pd.merge(pd.merge(cats_df, dogs_df), rabbits_df) # Hard to read. +/// ``` +/// +/// Use instead: +/// ```python +/// import pandas as pd +/// +/// cats_df = pd.read_csv("cats.csv") +/// dogs_df = pd.read_csv("dogs.csv") +/// rabbits_df = pd.read_csv("rabbits.csv") +/// pets_df = cats_df.merge(dogs_df).merge(rabbits_df) +/// ``` +/// +/// ## References +/// - [Pandas documentation: `merge`](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.merge.html#pandas.DataFrame.merge) +/// - [Pandas documentation: `pd.merge`](https://pandas.pydata.org/docs/reference/api/pandas.merge.html#pandas.merge) #[violation] pub struct PandasUseOfPdMerge; @@ -17,13 +54,14 @@ impl Violation for PandasUseOfPdMerge { } /// PD015 -pub(crate) fn use_of_pd_merge(func: &Expr) -> Option { +pub(crate) fn use_of_pd_merge(checker: &mut Checker, func: &Expr) { if let Expr::Attribute(ast::ExprAttribute { attr, value, .. }) = func { if let Expr::Name(ast::ExprName { id, .. }) = value.as_ref() { if id == "pd" && attr == "merge" { - return Some(Diagnostic::new(PandasUseOfPdMerge, func.range())); + checker + .diagnostics + .push(Diagnostic::new(PandasUseOfPdMerge, func.range())); } } } - None } diff --git a/crates/ruff/src/rules/pandas_vet/rules/subscript.rs b/crates/ruff/src/rules/pandas_vet/rules/subscript.rs index 96430e0151..8916c7d29a 100644 --- a/crates/ruff/src/rules/pandas_vet/rules/subscript.rs +++ b/crates/ruff/src/rules/pandas_vet/rules/subscript.rs @@ -8,6 +8,36 @@ use crate::checkers::ast::Checker; use crate::registry::Rule; use crate::rules::pandas_vet::helpers::{test_expression, Resolution}; +/// ## What it does +/// Checks for uses of `.ix` on Pandas objects. +/// +/// ## Why is this bad? +/// The `.ix` method is deprecated as its behavior is ambiguous. Specifically, +/// it's often unclear whether `.ix` is indexing by label or by ordinal position. +/// +/// Instead, prefer the `.loc` method for label-based indexing, and `.iloc` for +/// ordinal indexing. +/// +/// ## Example +/// ```python +/// import pandas as pd +/// +/// students_df = pd.read_csv("students.csv") +/// students_df.ix[0] # 0th row or row with label 0? +/// ``` +/// +/// Use instead: +/// ```python +/// import pandas as pd +/// +/// students_df = pd.read_csv("students.csv") +/// students_df.iloc[0] # 0th row. +/// ``` +/// +/// ## References +/// - [Pandas release notes: Deprecate `.ix`](https://pandas.pydata.org/pandas-docs/version/0.20/whatsnew.html#deprecate-ix) +/// - [Pandas documentation: `loc`](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.loc.html) +/// - [Pandas documentation: `iloc`](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.iloc.html) #[violation] pub struct PandasUseOfDotIx; @@ -18,6 +48,38 @@ impl Violation for PandasUseOfDotIx { } } +/// ## What it does +/// Checks for uses of `.at` on Pandas objects. +/// +/// ## Why is this bad? +/// The `.at` method selects a single value from a DataFrame or Series based on +/// a label index, and is slightly faster than using `.loc`. However, `.loc` is +/// more idiomatic and versatile, as it can be used to select multiple values at +/// once. +/// +/// If performance is an important consideration, convert the object to a NumPy +/// array, which will provide a much greater performance boost than using `.at` +/// over `.loc`. +/// +/// ## Example +/// ```python +/// import pandas as pd +/// +/// students_df = pd.read_csv("students.csv") +/// students_df.at["Maria"] +/// ``` +/// +/// Use instead: +/// ```python +/// import pandas as pd +/// +/// students_df = pd.read_csv("students.csv") +/// students_df.loc["Maria"] +/// ``` +/// +/// ## References +/// - [Pandas documentation: `loc`](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.loc.html) +/// - [Pandas documentation: `at`](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.at.html) #[violation] pub struct PandasUseOfDotAt; @@ -28,6 +90,47 @@ impl Violation for PandasUseOfDotAt { } } +/// ## What it does +/// Checks for uses of `.iat` on Pandas objects. +/// +/// ## Why is this bad? +/// The `.iat` method selects a single value from a DataFrame or Series based +/// on an ordinal index, and is slightly faster than using `.iloc`. However, +/// `.iloc` is more idiomatic and versatile, as it can be used to select +/// multiple values at once. +/// +/// If performance is an important consideration, convert the object to a NumPy +/// array, which will provide a much greater performance boost than using `.iat` +/// over `.iloc`. +/// +/// ## Example +/// ```python +/// import pandas as pd +/// +/// students_df = pd.read_csv("students.csv") +/// students_df.iat[0] +/// ``` +/// +/// Use instead: +/// ```python +/// import pandas as pd +/// +/// students_df = pd.read_csv("students.csv") +/// students_df.iloc[0] +/// ``` +/// +/// Or, using NumPy: +/// ```python +/// import numpy as np +/// import pandas as pd +/// +/// students_df = pd.read_csv("students.csv") +/// students_df.to_numpy()[0] +/// ``` +/// +/// ## References +/// - [Pandas documentation: `iloc`](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.iloc.html) +/// - [Pandas documentation: `iat`](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.iat.html) #[violation] pub struct PandasUseOfDotIat; @@ -43,18 +146,19 @@ pub(crate) fn subscript(checker: &mut Checker, value: &Expr, expr: &Expr) { return; }; - let rules = &checker.settings.rules; let violation: DiagnosticKind = match attr.as_str() { - "ix" if rules.enabled(Rule::PandasUseOfDotIx) => PandasUseOfDotIx.into(), - "at" if rules.enabled(Rule::PandasUseOfDotAt) => PandasUseOfDotAt.into(), - "iat" if rules.enabled(Rule::PandasUseOfDotIat) => PandasUseOfDotIat.into(), + "ix" if checker.settings.rules.enabled(Rule::PandasUseOfDotIx) => PandasUseOfDotIx.into(), + "at" if checker.settings.rules.enabled(Rule::PandasUseOfDotAt) => PandasUseOfDotAt.into(), + "iat" if checker.settings.rules.enabled(Rule::PandasUseOfDotIat) => { + PandasUseOfDotIat.into() + } _ => return, }; // Avoid flagging on non-DataFrames (e.g., `{"a": 1}.at[0]`), and on irrelevant bindings // (like imports). if !matches!( - test_expression(value, checker.semantic_model()), + test_expression(value, checker.semantic()), Resolution::RelevantLocal ) { return; diff --git a/crates/ruff/src/rules/pandas_vet/snapshots/ruff__rules__pandas_vet__tests__PD002_PD002.py.snap b/crates/ruff/src/rules/pandas_vet/snapshots/ruff__rules__pandas_vet__tests__PD002_PD002.py.snap index b837655f46..513c426e64 100644 --- a/crates/ruff/src/rules/pandas_vet/snapshots/ruff__rules__pandas_vet__tests__PD002_PD002.py.snap +++ b/crates/ruff/src/rules/pandas_vet/snapshots/ruff__rules__pandas_vet__tests__PD002_PD002.py.snap @@ -8,7 +8,7 @@ PD002.py:5:23: PD002 [*] `inplace=True` should be avoided; it has inconsistent b 5 | x.drop(["a"], axis=1, inplace=True) | ^^^^^^^^^^^^ PD002 6 | -7 | x.drop(["a"], axis=1, inplace=True) +7 | x.y.drop(["a"], axis=1, inplace=True) | = help: Assign to variable; remove `inplace` arg @@ -19,17 +19,17 @@ PD002.py:5:23: PD002 [*] `inplace=True` should be avoided; it has inconsistent b 5 |-x.drop(["a"], axis=1, inplace=True) 5 |+x = x.drop(["a"], axis=1) 6 6 | -7 7 | x.drop(["a"], axis=1, inplace=True) +7 7 | x.y.drop(["a"], axis=1, inplace=True) 8 8 | -PD002.py:7:23: PD002 [*] `inplace=True` should be avoided; it has inconsistent behavior +PD002.py:7:25: PD002 [*] `inplace=True` should be avoided; it has inconsistent behavior | 5 | x.drop(["a"], axis=1, inplace=True) 6 | -7 | x.drop(["a"], axis=1, inplace=True) - | ^^^^^^^^^^^^ PD002 +7 | x.y.drop(["a"], axis=1, inplace=True) + | ^^^^^^^^^^^^ PD002 8 | -9 | x.drop( +9 | x["y"].drop(["a"], axis=1, inplace=True) | = help: Assign to variable; remove `inplace` arg @@ -37,104 +37,124 @@ PD002.py:7:23: PD002 [*] `inplace=True` should be avoided; it has inconsistent b 4 4 | 5 5 | x.drop(["a"], axis=1, inplace=True) 6 6 | -7 |-x.drop(["a"], axis=1, inplace=True) - 7 |+x = x.drop(["a"], axis=1) +7 |-x.y.drop(["a"], axis=1, inplace=True) + 7 |+x.y = x.y.drop(["a"], axis=1) 8 8 | -9 9 | x.drop( -10 10 | inplace=True, +9 9 | x["y"].drop(["a"], axis=1, inplace=True) +10 10 | -PD002.py:10:5: PD002 [*] `inplace=True` should be avoided; it has inconsistent behavior +PD002.py:9:28: PD002 [*] `inplace=True` should be avoided; it has inconsistent behavior | - 9 | x.drop( -10 | inplace=True, - | ^^^^^^^^^^^^ PD002 -11 | columns=["a"], -12 | axis=1, + 7 | x.y.drop(["a"], axis=1, inplace=True) + 8 | + 9 | x["y"].drop(["a"], axis=1, inplace=True) + | ^^^^^^^^^^^^ PD002 +10 | +11 | x.drop( | = help: Assign to variable; remove `inplace` arg ℹ Suggested fix 6 6 | -7 7 | x.drop(["a"], axis=1, inplace=True) +7 7 | x.y.drop(["a"], axis=1, inplace=True) 8 8 | -9 |-x.drop( -10 |- inplace=True, - 9 |+x = x.drop( -11 10 | columns=["a"], -12 11 | axis=1, -13 12 | ) +9 |-x["y"].drop(["a"], axis=1, inplace=True) + 9 |+x["y"] = x["y"].drop(["a"], axis=1) +10 10 | +11 11 | x.drop( +12 12 | inplace=True, -PD002.py:17:9: PD002 [*] `inplace=True` should be avoided; it has inconsistent behavior +PD002.py:12:5: PD002 [*] `inplace=True` should be avoided; it has inconsistent behavior | -15 | if True: -16 | x.drop( -17 | inplace=True, +11 | x.drop( +12 | inplace=True, + | ^^^^^^^^^^^^ PD002 +13 | columns=["a"], +14 | axis=1, + | + = help: Assign to variable; remove `inplace` arg + +ℹ Suggested fix +8 8 | +9 9 | x["y"].drop(["a"], axis=1, inplace=True) +10 10 | +11 |-x.drop( +12 |- inplace=True, + 11 |+x = x.drop( +13 12 | columns=["a"], +14 13 | axis=1, +15 14 | ) + +PD002.py:19:9: PD002 [*] `inplace=True` should be avoided; it has inconsistent behavior + | +17 | if True: +18 | x.drop( +19 | inplace=True, | ^^^^^^^^^^^^ PD002 -18 | columns=["a"], -19 | axis=1, +20 | columns=["a"], +21 | axis=1, | = help: Assign to variable; remove `inplace` arg ℹ Suggested fix -13 13 | ) -14 14 | -15 15 | if True: -16 |- x.drop( -17 |- inplace=True, - 16 |+ x = x.drop( -18 17 | columns=["a"], -19 18 | axis=1, -20 19 | ) +15 15 | ) +16 16 | +17 17 | if True: +18 |- x.drop( +19 |- inplace=True, + 18 |+ x = x.drop( +20 19 | columns=["a"], +21 20 | axis=1, +22 21 | ) -PD002.py:22:33: PD002 [*] `inplace=True` should be avoided; it has inconsistent behavior +PD002.py:24:33: PD002 [*] `inplace=True` should be avoided; it has inconsistent behavior | -20 | ) -21 | -22 | x.drop(["a"], axis=1, **kwargs, inplace=True) +22 | ) +23 | +24 | x.drop(["a"], axis=1, **kwargs, inplace=True) | ^^^^^^^^^^^^ PD002 -23 | x.drop(["a"], axis=1, inplace=True, **kwargs) -24 | f(x.drop(["a"], axis=1, inplace=True)) +25 | x.drop(["a"], axis=1, inplace=True, **kwargs) +26 | f(x.drop(["a"], axis=1, inplace=True)) | = help: Assign to variable; remove `inplace` arg ℹ Suggested fix -19 19 | axis=1, -20 20 | ) -21 21 | -22 |-x.drop(["a"], axis=1, **kwargs, inplace=True) - 22 |+x = x.drop(["a"], axis=1, **kwargs) -23 23 | x.drop(["a"], axis=1, inplace=True, **kwargs) -24 24 | f(x.drop(["a"], axis=1, inplace=True)) -25 25 | +21 21 | axis=1, +22 22 | ) +23 23 | +24 |-x.drop(["a"], axis=1, **kwargs, inplace=True) + 24 |+x = x.drop(["a"], axis=1, **kwargs) +25 25 | x.drop(["a"], axis=1, inplace=True, **kwargs) +26 26 | f(x.drop(["a"], axis=1, inplace=True)) +27 27 | -PD002.py:23:23: PD002 `inplace=True` should be avoided; it has inconsistent behavior +PD002.py:25:23: PD002 `inplace=True` should be avoided; it has inconsistent behavior | -22 | x.drop(["a"], axis=1, **kwargs, inplace=True) -23 | x.drop(["a"], axis=1, inplace=True, **kwargs) +24 | x.drop(["a"], axis=1, **kwargs, inplace=True) +25 | x.drop(["a"], axis=1, inplace=True, **kwargs) | ^^^^^^^^^^^^ PD002 -24 | f(x.drop(["a"], axis=1, inplace=True)) +26 | f(x.drop(["a"], axis=1, inplace=True)) | = help: Assign to variable; remove `inplace` arg -PD002.py:24:25: PD002 `inplace=True` should be avoided; it has inconsistent behavior +PD002.py:26:25: PD002 `inplace=True` should be avoided; it has inconsistent behavior | -22 | x.drop(["a"], axis=1, **kwargs, inplace=True) -23 | x.drop(["a"], axis=1, inplace=True, **kwargs) -24 | f(x.drop(["a"], axis=1, inplace=True)) +24 | x.drop(["a"], axis=1, **kwargs, inplace=True) +25 | x.drop(["a"], axis=1, inplace=True, **kwargs) +26 | f(x.drop(["a"], axis=1, inplace=True)) | ^^^^^^^^^^^^ PD002 -25 | -26 | x.apply(lambda x: x.sort_values('a', inplace=True)) +27 | +28 | x.apply(lambda x: x.sort_values("a", inplace=True)) | = help: Assign to variable; remove `inplace` arg -PD002.py:26:38: PD002 `inplace=True` should be avoided; it has inconsistent behavior +PD002.py:28:38: PD002 `inplace=True` should be avoided; it has inconsistent behavior | -24 | f(x.drop(["a"], axis=1, inplace=True)) -25 | -26 | x.apply(lambda x: x.sort_values('a', inplace=True)) +26 | f(x.drop(["a"], axis=1, inplace=True)) +27 | +28 | x.apply(lambda x: x.sort_values("a", inplace=True)) | ^^^^^^^^^^^^ PD002 -27 | import torch -28 | torch.m.ReLU(inplace=True) # safe because this isn't a pandas call +29 | import torch | = help: Assign to variable; remove `inplace` arg diff --git a/crates/ruff/src/rules/pep8_naming/helpers.rs b/crates/ruff/src/rules/pep8_naming/helpers.rs index 72efccade2..24017389ff 100644 --- a/crates/ruff/src/rules/pep8_naming/helpers.rs +++ b/crates/ruff/src/rules/pep8_naming/helpers.rs @@ -1,15 +1,15 @@ use itertools::Itertools; use rustpython_parser::ast::{self, Expr, Stmt}; -use ruff_python_semantic::model::SemanticModel; -use ruff_python_stdlib::str::{is_lower, is_upper}; +use ruff_python_semantic::SemanticModel; +use ruff_python_stdlib::str::{is_cased_lowercase, is_cased_uppercase}; pub(super) fn is_camelcase(name: &str) -> bool { - !is_lower(name) && !is_upper(name) && !name.contains('_') + !is_cased_lowercase(name) && !is_cased_uppercase(name) && !name.contains('_') } pub(super) fn is_mixed_case(name: &str) -> bool { - !is_lower(name) + !is_cased_lowercase(name) && name .strip_prefix('_') .unwrap_or(name) @@ -22,14 +22,14 @@ pub(super) fn is_acronym(name: &str, asname: &str) -> bool { name.chars().filter(|c| c.is_uppercase()).join("") == asname } -pub(super) fn is_named_tuple_assignment(model: &SemanticModel, stmt: &Stmt) -> bool { +pub(super) fn is_named_tuple_assignment(stmt: &Stmt, semantic: &SemanticModel) -> bool { let Stmt::Assign(ast::StmtAssign { value, .. }) = stmt else { return false; }; - let Expr::Call(ast::ExprCall {func, ..}) = value.as_ref() else { + let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() else { return false; }; - model.resolve_call_path(func).map_or(false, |call_path| { + semantic.resolve_call_path(func).map_or(false, |call_path| { matches!( call_path.as_slice(), ["collections", "namedtuple"] | ["typing", "NamedTuple"] @@ -37,35 +37,34 @@ pub(super) fn is_named_tuple_assignment(model: &SemanticModel, stmt: &Stmt) -> b }) } -pub(super) fn is_typed_dict_assignment(model: &SemanticModel, stmt: &Stmt) -> bool { +pub(super) fn is_typed_dict_assignment(stmt: &Stmt, semantic: &SemanticModel) -> bool { let Stmt::Assign(ast::StmtAssign { value, .. }) = stmt else { return false; }; - let Expr::Call(ast::ExprCall {func, ..}) = value.as_ref() else { + let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() else { return false; }; - model.resolve_call_path(func).map_or(false, |call_path| { - call_path.as_slice() == ["typing", "TypedDict"] + semantic.resolve_call_path(func).map_or(false, |call_path| { + matches!(call_path.as_slice(), ["typing", "TypedDict"]) }) } -pub(super) fn is_type_var_assignment(model: &SemanticModel, stmt: &Stmt) -> bool { +pub(super) fn is_type_var_assignment(stmt: &Stmt, semantic: &SemanticModel) -> bool { let Stmt::Assign(ast::StmtAssign { value, .. }) = stmt else { return false; }; - let Expr::Call(ast::ExprCall {func, ..}) = value.as_ref() else { + let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() else { return false; }; - model.resolve_call_path(func).map_or(false, |call_path| { - call_path.as_slice() == ["typing", "TypeVar"] - || call_path.as_slice() == ["typing", "NewType"] + semantic.resolve_call_path(func).map_or(false, |call_path| { + matches!(call_path.as_slice(), ["typing", "TypeVar" | "NewType"]) }) } -pub(super) fn is_typed_dict_class(model: &SemanticModel, bases: &[Expr]) -> bool { +pub(super) fn is_typed_dict_class(bases: &[Expr], semantic: &SemanticModel) -> bool { bases .iter() - .any(|base| model.match_typing_expr(base, "TypedDict")) + .any(|base| semantic.match_typing_expr(base, "TypedDict")) } #[cfg(test)] diff --git a/crates/ruff/src/rules/pep8_naming/mod.rs b/crates/ruff/src/rules/pep8_naming/mod.rs index 98f58c1e12..f7e55dfe1d 100644 --- a/crates/ruff/src/rules/pep8_naming/mod.rs +++ b/crates/ruff/src/rules/pep8_naming/mod.rs @@ -5,13 +5,14 @@ pub mod settings; #[cfg(test)] mod tests { - use std::path::Path; + use std::path::{Path, PathBuf}; use anyhow::Result; use test_case::test_case; use crate::registry::Rule; use crate::rules::pep8_naming; + use crate::settings::types::IdentifierPattern; use crate::test::test_path; use crate::{assert_messages, settings}; @@ -104,4 +105,41 @@ mod tests { assert_messages!(diagnostics); Ok(()) } + + #[test_case(Rule::InvalidClassName, "N801.py")] + #[test_case(Rule::InvalidFunctionName, "N802.py")] + #[test_case(Rule::InvalidArgumentName, "N803.py")] + #[test_case(Rule::InvalidFirstArgumentNameForClassMethod, "N804.py")] + #[test_case(Rule::InvalidFirstArgumentNameForMethod, "N805.py")] + #[test_case(Rule::NonLowercaseVariableInFunction, "N806.py")] + #[test_case(Rule::DunderFunctionName, "N807.py")] + #[test_case(Rule::ConstantImportedAsNonConstant, "N811.py")] + #[test_case(Rule::LowercaseImportedAsNonLowercase, "N812.py")] + #[test_case(Rule::CamelcaseImportedAsLowercase, "N813.py")] + #[test_case(Rule::CamelcaseImportedAsConstant, "N814.py")] + #[test_case(Rule::MixedCaseVariableInClassScope, "N815.py")] + #[test_case(Rule::MixedCaseVariableInGlobalScope, "N816.py")] + #[test_case(Rule::CamelcaseImportedAsAcronym, "N817.py")] + #[test_case(Rule::ErrorSuffixOnExceptionName, "N818.py")] + #[test_case(Rule::InvalidModuleName, "N999/badAllowed/__init__.py")] + fn ignore_names(rule_code: Rule, path: &str) -> Result<()> { + let snapshot = format!("ignore_names_{}_{path}", rule_code.noqa_code()); + let diagnostics = test_path( + PathBuf::from_iter(["pep8_naming", "ignore_names", path]).as_path(), + &settings::Settings { + pep8_naming: pep8_naming::settings::Settings { + ignore_names: vec![ + IdentifierPattern::new("*allowed*").unwrap(), + IdentifierPattern::new("*Allowed*").unwrap(), + IdentifierPattern::new("*ALLOWED*").unwrap(), + IdentifierPattern::new("BA").unwrap(), // For N817. + ], + ..Default::default() + }, + ..settings::Settings::for_rule(rule_code) + }, + )?; + assert_messages!(snapshot, diagnostics); + Ok(()) + } } diff --git a/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_acronym.rs b/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_acronym.rs index a4ceec8d8d..b81b30abb2 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_acronym.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_acronym.rs @@ -5,6 +5,7 @@ use ruff_macros::{derive_message_formats, violation}; use ruff_python_stdlib::str::{self}; use crate::rules::pep8_naming::helpers; +use crate::settings::types::IdentifierPattern; /// ## What it does /// Checks for `CamelCase` imports that are aliased as acronyms. @@ -52,10 +53,18 @@ pub(crate) fn camelcase_imported_as_acronym( asname: &str, alias: &Alias, stmt: &Stmt, + ignore_names: &[IdentifierPattern], ) -> Option { + if ignore_names + .iter() + .any(|ignore_name| ignore_name.matches(asname)) + { + return None; + } + if helpers::is_camelcase(name) - && !str::is_lower(asname) - && str::is_upper(asname) + && !str::is_cased_lowercase(asname) + && str::is_cased_uppercase(asname) && helpers::is_acronym(name, asname) { let mut diagnostic = Diagnostic::new( diff --git a/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_constant.rs b/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_constant.rs index 355be20923..b43f54b977 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_constant.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_constant.rs @@ -5,6 +5,7 @@ use ruff_macros::{derive_message_formats, violation}; use ruff_python_stdlib::str::{self}; use crate::rules::pep8_naming::helpers; +use crate::settings::types::IdentifierPattern; /// ## What it does /// Checks for `CamelCase` imports that are aliased to constant-style names. @@ -49,10 +50,18 @@ pub(crate) fn camelcase_imported_as_constant( asname: &str, alias: &Alias, stmt: &Stmt, + ignore_names: &[IdentifierPattern], ) -> Option { + if ignore_names + .iter() + .any(|ignore_name| ignore_name.matches(name)) + { + return None; + } + if helpers::is_camelcase(name) - && !str::is_lower(asname) - && str::is_upper(asname) + && !str::is_cased_lowercase(asname) + && str::is_cased_uppercase(asname) && !helpers::is_acronym(name, asname) { let mut diagnostic = Diagnostic::new( diff --git a/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_lowercase.rs b/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_lowercase.rs index c427e879a5..b5f051de6a 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_lowercase.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/camelcase_imported_as_lowercase.rs @@ -4,6 +4,7 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use crate::rules::pep8_naming::helpers; +use crate::settings::types::IdentifierPattern; /// ## What it does /// Checks for `CamelCase` imports that are aliased to lowercase names. @@ -48,8 +49,16 @@ pub(crate) fn camelcase_imported_as_lowercase( asname: &str, alias: &Alias, stmt: &Stmt, + ignore_names: &[IdentifierPattern], ) -> Option { - if helpers::is_camelcase(name) && ruff_python_stdlib::str::is_lower(asname) { + if ignore_names + .iter() + .any(|ignore_name| ignore_name.matches(asname)) + { + return None; + } + + if helpers::is_camelcase(name) && ruff_python_stdlib::str::is_cased_lowercase(asname) { let mut diagnostic = Diagnostic::new( CamelcaseImportedAsLowercase { name: name.to_string(), diff --git a/crates/ruff/src/rules/pep8_naming/rules/constant_imported_as_non_constant.rs b/crates/ruff/src/rules/pep8_naming/rules/constant_imported_as_non_constant.rs index 79cf3b32af..3ce0ab006f 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/constant_imported_as_non_constant.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/constant_imported_as_non_constant.rs @@ -4,6 +4,8 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_stdlib::str; +use crate::settings::types::IdentifierPattern; + /// ## What it does /// Checks for constant imports that are aliased to non-constant-style /// names. @@ -48,8 +50,16 @@ pub(crate) fn constant_imported_as_non_constant( asname: &str, alias: &Alias, stmt: &Stmt, + ignore_names: &[IdentifierPattern], ) -> Option { - if str::is_upper(name) && !str::is_upper(asname) { + if ignore_names + .iter() + .any(|ignore_name| ignore_name.matches(name)) + { + return None; + } + + if str::is_cased_uppercase(name) && !str::is_cased_uppercase(asname) { let mut diagnostic = Diagnostic::new( ConstantImportedAsNonConstant { name: name.to_string(), diff --git a/crates/ruff/src/rules/pep8_naming/rules/dunder_function_name.rs b/crates/ruff/src/rules/pep8_naming/rules/dunder_function_name.rs index 143576f057..2e0b708469 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/dunder_function_name.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/dunder_function_name.rs @@ -2,9 +2,10 @@ use rustpython_parser::ast::Stmt; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::identifier_range; -use ruff_python_ast::source_code::Locator; -use ruff_python_semantic::scope::{Scope, ScopeKind}; +use ruff_python_ast::identifier::Identifier; +use ruff_python_semantic::{Scope, ScopeKind}; + +use crate::settings::types::IdentifierPattern; /// ## What it does /// Checks for functions with "dunder" names (that is, names with two @@ -45,7 +46,7 @@ pub(crate) fn dunder_function_name( scope: &Scope, stmt: &Stmt, name: &str, - locator: &Locator, + ignore_names: &[IdentifierPattern], ) -> Option { if matches!(scope.kind, ScopeKind::Class(_)) { return None; @@ -57,9 +58,12 @@ pub(crate) fn dunder_function_name( if matches!(scope.kind, ScopeKind::Module) && (name == "__getattr__" || name == "__dir__") { return None; } + if ignore_names + .iter() + .any(|ignore_name| ignore_name.matches(name)) + { + return None; + } - Some(Diagnostic::new( - DunderFunctionName, - identifier_range(stmt, locator), - )) + Some(Diagnostic::new(DunderFunctionName, stmt.identifier())) } diff --git a/crates/ruff/src/rules/pep8_naming/rules/error_suffix_on_exception_name.rs b/crates/ruff/src/rules/pep8_naming/rules/error_suffix_on_exception_name.rs index c75678682c..82edff98eb 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/error_suffix_on_exception_name.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/error_suffix_on_exception_name.rs @@ -2,8 +2,9 @@ use rustpython_parser::ast::{self, Expr, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::identifier_range; -use ruff_python_ast::source_code::Locator; +use ruff_python_ast::identifier::Identifier; + +use crate::settings::types::IdentifierPattern; /// ## What it does /// Checks for custom exception definitions that omit the `Error` suffix. @@ -46,8 +47,15 @@ pub(crate) fn error_suffix_on_exception_name( class_def: &Stmt, bases: &[Expr], name: &str, - locator: &Locator, + ignore_names: &[IdentifierPattern], ) -> Option { + if ignore_names + .iter() + .any(|ignore_name| ignore_name.matches(name)) + { + return None; + } + if !bases.iter().any(|base| { if let Expr::Name(ast::ExprName { id, .. }) = &base { id == "Exception" || id.ends_with("Error") @@ -65,6 +73,6 @@ pub(crate) fn error_suffix_on_exception_name( ErrorSuffixOnExceptionName { name: name.to_string(), }, - identifier_range(class_def, locator), + class_def.identifier(), )) } diff --git a/crates/ruff/src/rules/pep8_naming/rules/invalid_argument_name.rs b/crates/ruff/src/rules/pep8_naming/rules/invalid_argument_name.rs index c2c8fe8d08..be9e86723f 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/invalid_argument_name.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/invalid_argument_name.rs @@ -2,6 +2,9 @@ use rustpython_parser::ast::{Arg, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_stdlib::str; + +use crate::settings::types::IdentifierPattern; /// ## What it does /// Checks for argument names that do not follow the `snake_case` convention. @@ -48,12 +51,15 @@ impl Violation for InvalidArgumentName { pub(crate) fn invalid_argument_name( name: &str, arg: &Arg, - ignore_names: &[String], + ignore_names: &[IdentifierPattern], ) -> Option { - if ignore_names.iter().any(|ignore_name| ignore_name == name) { + if ignore_names + .iter() + .any(|ignore_name| ignore_name.matches(name)) + { return None; } - if name.to_lowercase() != name { + if !str::is_lowercase(name) { return Some(Diagnostic::new( InvalidArgumentName { name: name.to_string(), diff --git a/crates/ruff/src/rules/pep8_naming/rules/invalid_class_name.rs b/crates/ruff/src/rules/pep8_naming/rules/invalid_class_name.rs index d4f8462a55..e1bd799d9f 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/invalid_class_name.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/invalid_class_name.rs @@ -2,8 +2,9 @@ use rustpython_parser::ast::Stmt; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::identifier_range; -use ruff_python_ast::source_code::Locator; +use ruff_python_ast::identifier::Identifier; + +use crate::settings::types::IdentifierPattern; /// ## What it does /// Checks for class names that do not follow the `CamelCase` convention. @@ -51,15 +52,22 @@ impl Violation for InvalidClassName { pub(crate) fn invalid_class_name( class_def: &Stmt, name: &str, - locator: &Locator, + ignore_names: &[IdentifierPattern], ) -> Option { + if ignore_names + .iter() + .any(|ignore_name| ignore_name.matches(name)) + { + return None; + } + let stripped = name.strip_prefix('_').unwrap_or(name); if !stripped.chars().next().map_or(false, char::is_uppercase) || stripped.contains('_') { return Some(Diagnostic::new( InvalidClassName { name: name.to_string(), }, - identifier_range(class_def, locator), + class_def.identifier(), )); } None diff --git a/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_class_method.rs b/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_class_method.rs index 05f42674d1..cc17ffe817 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_class_method.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_class_method.rs @@ -1,9 +1,9 @@ -use rustpython_parser::ast::{Arguments, Decorator, Ranged}; +use rustpython_parser::ast::{ArgWithDefault, Arguments, Decorator, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_semantic::analyze::function_type; -use ruff_python_semantic::scope::Scope; +use ruff_python_semantic::Scope; use crate::checkers::ast::Checker; @@ -21,11 +21,6 @@ use crate::checkers::ast::Checker; /// > append a single trailing underscore rather than use an abbreviation or spelling corruption. /// > Thus `class_` is better than `clss`. (Perhaps better is to avoid such clashes by using a synonym.) /// -/// ## Options -/// - `pep8-naming.classmethod-decorators` -/// - `pep8-naming.staticmethod-decorators` -/// - `pep8-naming.ignore-names` -/// /// ## Example /// ```python /// class Example: @@ -42,8 +37,12 @@ use crate::checkers::ast::Checker; /// ... /// ``` /// -/// [PEP 8]: https://peps.python.org/pep-0008/#function-and-method-arguments +/// ## Options +/// - `pep8-naming.classmethod-decorators` +/// - `pep8-naming.staticmethod-decorators` +/// - `pep8-naming.ignore-names` /// +/// [PEP 8]: https://peps.python.org/pep-0008/#function-and-method-arguments #[violation] pub struct InvalidFirstArgumentNameForClassMethod; @@ -64,10 +63,10 @@ pub(crate) fn invalid_first_argument_name_for_class_method( ) -> Option { if !matches!( function_type::classify( - checker.semantic_model(), - scope, name, decorator_list, + scope, + checker.semantic(), &checker.settings.pep8_naming.classmethod_decorators, &checker.settings.pep8_naming.staticmethod_decorators, ), @@ -75,20 +74,25 @@ pub(crate) fn invalid_first_argument_name_for_class_method( ) { return None; } - if let Some(arg) = args.posonlyargs.first().or_else(|| args.args.first()) { - if &arg.arg != "cls" { + if let Some(ArgWithDefault { + def, + default: _, + range: _, + }) = args.posonlyargs.first().or_else(|| args.args.first()) + { + if &def.arg != "cls" { if checker .settings .pep8_naming .ignore_names .iter() - .any(|ignore_name| ignore_name == name) + .any(|ignore_name| ignore_name.matches(name)) { return None; } return Some(Diagnostic::new( InvalidFirstArgumentNameForClassMethod, - arg.range(), + def.range(), )); } } diff --git a/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_method.rs b/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_method.rs index eeedca93c1..55dd34d104 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_method.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/invalid_first_argument_name_for_method.rs @@ -3,7 +3,7 @@ use rustpython_parser::ast::{Arguments, Decorator, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_semantic::analyze::function_type; -use ruff_python_semantic::scope::Scope; +use ruff_python_semantic::Scope; use crate::checkers::ast::Checker; @@ -21,11 +21,6 @@ use crate::checkers::ast::Checker; /// > append a single trailing underscore rather than use an abbreviation or spelling corruption. /// > Thus `class_` is better than `clss`. (Perhaps better is to avoid such clashes by using a synonym.) /// -/// ## Options -/// - `pep8-naming.classmethod-decorators` -/// - `pep8-naming.staticmethod-decorators` -/// - `pep8-naming.ignore-names` -/// /// ## Example /// ```python /// class Example: @@ -40,6 +35,11 @@ use crate::checkers::ast::Checker; /// ... /// ``` /// +/// ## Options +/// - `pep8-naming.classmethod-decorators` +/// - `pep8-naming.staticmethod-decorators` +/// - `pep8-naming.ignore-names` +/// /// [PEP 8]: https://peps.python.org/pep-0008/#function-and-method-arguments #[violation] pub struct InvalidFirstArgumentNameForMethod; @@ -61,10 +61,10 @@ pub(crate) fn invalid_first_argument_name_for_method( ) -> Option { if !matches!( function_type::classify( - checker.semantic_model(), - scope, name, decorator_list, + scope, + checker.semantic(), &checker.settings.pep8_naming.classmethod_decorators, &checker.settings.pep8_naming.staticmethod_decorators, ), @@ -73,7 +73,7 @@ pub(crate) fn invalid_first_argument_name_for_method( return None; } let arg = args.posonlyargs.first().or_else(|| args.args.first())?; - if &arg.arg == "self" { + if &arg.def.arg == "self" { return None; } if checker @@ -81,12 +81,12 @@ pub(crate) fn invalid_first_argument_name_for_method( .pep8_naming .ignore_names .iter() - .any(|ignore_name| ignore_name == name) + .any(|ignore_name| ignore_name.matches(name)) { return None; } Some(Diagnostic::new( InvalidFirstArgumentNameForMethod, - arg.range(), + arg.def.range(), )) } diff --git a/crates/ruff/src/rules/pep8_naming/rules/invalid_function_name.rs b/crates/ruff/src/rules/pep8_naming/rules/invalid_function_name.rs index 3713bc8ef2..71ca191d40 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/invalid_function_name.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/invalid_function_name.rs @@ -2,10 +2,12 @@ use rustpython_parser::ast::{Decorator, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::identifier_range; -use ruff_python_ast::source_code::Locator; +use ruff_python_ast::identifier::Identifier; use ruff_python_semantic::analyze::visibility; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; +use ruff_python_stdlib::str; + +use crate::settings::types::IdentifierPattern; /// ## What it does /// Checks for functions names that do not follow the `snake_case` naming @@ -18,9 +20,6 @@ use ruff_python_semantic::model::SemanticModel; /// > improve readability. mixedCase is allowed only in contexts where that’s already the /// > prevailing style (e.g. threading.py), to retain backwards compatibility. /// -/// ## Options -/// - `pep8-naming.ignore-names` -/// /// ## Example /// ```python /// def myFunction(): @@ -33,6 +32,9 @@ use ruff_python_semantic::model::SemanticModel; /// pass /// ``` /// +/// ## Options +/// - `pep8-naming.ignore-names` +/// /// [PEP 8]: https://peps.python.org/pep-0008/#function-and-variable-names #[violation] pub struct InvalidFunctionName { @@ -52,23 +54,25 @@ pub(crate) fn invalid_function_name( stmt: &Stmt, name: &str, decorator_list: &[Decorator], - ignore_names: &[String], - model: &SemanticModel, - locator: &Locator, + ignore_names: &[IdentifierPattern], + semantic: &SemanticModel, ) -> Option { // Ignore any explicitly-ignored function names. - if ignore_names.iter().any(|ignore_name| ignore_name == name) { + if ignore_names + .iter() + .any(|ignore_name| ignore_name.matches(name)) + { return None; } // Ignore any function names that are already lowercase. - if name.to_lowercase() == name { + if str::is_lowercase(name) { return None; } // Ignore any functions that are explicitly `@override`. These are defined elsewhere, // so if they're first-party, we'll flag them at the definition site. - if visibility::is_override(model, decorator_list) { + if visibility::is_override(decorator_list, semantic) { return None; } @@ -76,6 +80,6 @@ pub(crate) fn invalid_function_name( InvalidFunctionName { name: name.to_string(), }, - identifier_range(stmt, locator), + stmt.identifier(), )) } diff --git a/crates/ruff/src/rules/pep8_naming/rules/invalid_module_name.rs b/crates/ruff/src/rules/pep8_naming/rules/invalid_module_name.rs index aa42ba8a89..c733db5e2f 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/invalid_module_name.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/invalid_module_name.rs @@ -7,6 +7,8 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_stdlib::identifiers::{is_migration_name, is_module_name}; +use crate::settings::types::IdentifierPattern; + /// ## What it does /// Checks for module names that do not follow the `snake_case` naming /// convention or are otherwise invalid. @@ -46,7 +48,11 @@ impl Violation for InvalidModuleName { } /// N999 -pub(crate) fn invalid_module_name(path: &Path, package: Option<&Path>) -> Option { +pub(crate) fn invalid_module_name( + path: &Path, + package: Option<&Path>, + ignore_names: &[IdentifierPattern], +) -> Option { if !path .extension() .map_or(false, |ext| ext == "py" || ext == "pyi") @@ -61,6 +67,13 @@ pub(crate) fn invalid_module_name(path: &Path, package: Option<&Path>) -> Option path.file_stem().unwrap().to_string_lossy() }; + if ignore_names + .iter() + .any(|ignore_name| ignore_name.matches(&module_name)) + { + return None; + } + // As a special case, we allow files in `versions` and `migrations` directories to start // with a digit (e.g., `0001_initial.py`), to support common conventions used by Django // and other frameworks. diff --git a/crates/ruff/src/rules/pep8_naming/rules/lowercase_imported_as_non_lowercase.rs b/crates/ruff/src/rules/pep8_naming/rules/lowercase_imported_as_non_lowercase.rs index 5d347f1f9d..63dd420c00 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/lowercase_imported_as_non_lowercase.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/lowercase_imported_as_non_lowercase.rs @@ -4,6 +4,8 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_stdlib::str; +use crate::settings::types::IdentifierPattern; + /// ## What it does /// Checks for lowercase imports that are aliased to non-lowercase names. /// @@ -47,8 +49,17 @@ pub(crate) fn lowercase_imported_as_non_lowercase( asname: &str, alias: &Alias, stmt: &Stmt, + ignore_names: &[IdentifierPattern], ) -> Option { - if !str::is_upper(name) && str::is_lower(name) && asname.to_lowercase() != asname { + if ignore_names + .iter() + .any(|ignore_name| ignore_name.matches(asname)) + { + return None; + } + + if !str::is_cased_uppercase(name) && str::is_cased_lowercase(name) && !str::is_lowercase(asname) + { let mut diagnostic = Diagnostic::new( LowercaseImportedAsNonLowercase { name: name.to_string(), diff --git a/crates/ruff/src/rules/pep8_naming/rules/mixed_case_variable_in_class_scope.rs b/crates/ruff/src/rules/pep8_naming/rules/mixed_case_variable_in_class_scope.rs index 2cb0793789..c43b384524 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/mixed_case_variable_in_class_scope.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/mixed_case_variable_in_class_scope.rs @@ -62,13 +62,13 @@ pub(crate) fn mixed_case_variable_in_class_scope( .pep8_naming .ignore_names .iter() - .any(|ignore_name| ignore_name == name) + .any(|ignore_name| ignore_name.matches(name)) { return; } if helpers::is_mixed_case(name) - && !helpers::is_named_tuple_assignment(checker.semantic_model(), stmt) - && !helpers::is_typed_dict_class(checker.semantic_model(), bases) + && !helpers::is_named_tuple_assignment(stmt, checker.semantic()) + && !helpers::is_typed_dict_class(bases, checker.semantic()) { checker.diagnostics.push(Diagnostic::new( MixedCaseVariableInClassScope { diff --git a/crates/ruff/src/rules/pep8_naming/rules/mixed_case_variable_in_global_scope.rs b/crates/ruff/src/rules/pep8_naming/rules/mixed_case_variable_in_global_scope.rs index b0312c50d6..4024ba9998 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/mixed_case_variable_in_global_scope.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/mixed_case_variable_in_global_scope.rs @@ -20,7 +20,7 @@ use crate::rules::pep8_naming::helpers; /// > Modules that are designed for use via from M import * should use the /// __all__ mechanism to prevent exporting globals, or use the older /// convention of prefixing such globals with an underscore (which you might -///want to do to indicate these globals are “module non-public”). +/// want to do to indicate these globals are “module non-public”). /// > /// > ### Function and Variable Names /// > Function names should be lowercase, with words separated by underscores @@ -71,12 +71,11 @@ pub(crate) fn mixed_case_variable_in_global_scope( .pep8_naming .ignore_names .iter() - .any(|ignore_name| ignore_name == name) + .any(|ignore_name| ignore_name.matches(name)) { return; } - if helpers::is_mixed_case(name) - && !helpers::is_named_tuple_assignment(checker.semantic_model(), stmt) + if helpers::is_mixed_case(name) && !helpers::is_named_tuple_assignment(stmt, checker.semantic()) { checker.diagnostics.push(Diagnostic::new( MixedCaseVariableInGlobalScope { diff --git a/crates/ruff/src/rules/pep8_naming/rules/mod.rs b/crates/ruff/src/rules/pep8_naming/rules/mod.rs index f842ca40ea..636ae11477 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/mod.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/mod.rs @@ -1,41 +1,19 @@ -pub(crate) use camelcase_imported_as_acronym::{ - camelcase_imported_as_acronym, CamelcaseImportedAsAcronym, -}; -pub(crate) use camelcase_imported_as_constant::{ - camelcase_imported_as_constant, CamelcaseImportedAsConstant, -}; -pub(crate) use camelcase_imported_as_lowercase::{ - camelcase_imported_as_lowercase, CamelcaseImportedAsLowercase, -}; -pub(crate) use constant_imported_as_non_constant::{ - constant_imported_as_non_constant, ConstantImportedAsNonConstant, -}; -pub(crate) use dunder_function_name::{dunder_function_name, DunderFunctionName}; -pub(crate) use error_suffix_on_exception_name::{ - error_suffix_on_exception_name, ErrorSuffixOnExceptionName, -}; -pub(crate) use invalid_argument_name::{invalid_argument_name, InvalidArgumentName}; -pub(crate) use invalid_class_name::{invalid_class_name, InvalidClassName}; -pub(crate) use invalid_first_argument_name_for_class_method::{ - invalid_first_argument_name_for_class_method, InvalidFirstArgumentNameForClassMethod, -}; -pub(crate) use invalid_first_argument_name_for_method::{ - invalid_first_argument_name_for_method, InvalidFirstArgumentNameForMethod, -}; -pub(crate) use invalid_function_name::{invalid_function_name, InvalidFunctionName}; -pub(crate) use invalid_module_name::{invalid_module_name, InvalidModuleName}; -pub(crate) use lowercase_imported_as_non_lowercase::{ - lowercase_imported_as_non_lowercase, LowercaseImportedAsNonLowercase, -}; -pub(crate) use mixed_case_variable_in_class_scope::{ - mixed_case_variable_in_class_scope, MixedCaseVariableInClassScope, -}; -pub(crate) use mixed_case_variable_in_global_scope::{ - mixed_case_variable_in_global_scope, MixedCaseVariableInGlobalScope, -}; -pub(crate) use non_lowercase_variable_in_function::{ - non_lowercase_variable_in_function, NonLowercaseVariableInFunction, -}; +pub(crate) use camelcase_imported_as_acronym::*; +pub(crate) use camelcase_imported_as_constant::*; +pub(crate) use camelcase_imported_as_lowercase::*; +pub(crate) use constant_imported_as_non_constant::*; +pub(crate) use dunder_function_name::*; +pub(crate) use error_suffix_on_exception_name::*; +pub(crate) use invalid_argument_name::*; +pub(crate) use invalid_class_name::*; +pub(crate) use invalid_first_argument_name_for_class_method::*; +pub(crate) use invalid_first_argument_name_for_method::*; +pub(crate) use invalid_function_name::*; +pub(crate) use invalid_module_name::*; +pub(crate) use lowercase_imported_as_non_lowercase::*; +pub(crate) use mixed_case_variable_in_class_scope::*; +pub(crate) use mixed_case_variable_in_global_scope::*; +pub(crate) use non_lowercase_variable_in_function::*; mod camelcase_imported_as_acronym; mod camelcase_imported_as_constant; diff --git a/crates/ruff/src/rules/pep8_naming/rules/non_lowercase_variable_in_function.rs b/crates/ruff/src/rules/pep8_naming/rules/non_lowercase_variable_in_function.rs index ed7648fb34..4fe3edd36f 100644 --- a/crates/ruff/src/rules/pep8_naming/rules/non_lowercase_variable_in_function.rs +++ b/crates/ruff/src/rules/pep8_naming/rules/non_lowercase_variable_in_function.rs @@ -2,6 +2,7 @@ use rustpython_parser::ast::{Expr, Ranged, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_stdlib::str; use crate::checkers::ast::Checker; use crate::rules::pep8_naming::helpers; @@ -17,9 +18,6 @@ use crate::rules::pep8_naming::helpers; /// > is allowed only in contexts where that's already the prevailing style (e.g. threading.py), /// > to retain backwards compatibility. /// -/// ## Options -/// - `pep8-naming.ignore-names` -/// /// ## Example /// ```python /// def my_function(a): @@ -34,6 +32,9 @@ use crate::rules::pep8_naming::helpers; /// return b /// ``` /// +/// ## Options +/// - `pep8-naming.ignore-names` +/// /// [PEP 8]: https://peps.python.org/pep-0008/#function-and-variable-names #[violation] pub struct NonLowercaseVariableInFunction { @@ -60,15 +61,15 @@ pub(crate) fn non_lowercase_variable_in_function( .pep8_naming .ignore_names .iter() - .any(|ignore_name| ignore_name == name) + .any(|ignore_name| ignore_name.matches(name)) { return; } - if name.to_lowercase() != name - && !helpers::is_named_tuple_assignment(checker.semantic_model(), stmt) - && !helpers::is_typed_dict_assignment(checker.semantic_model(), stmt) - && !helpers::is_type_var_assignment(checker.semantic_model(), stmt) + if !str::is_lowercase(name) + && !helpers::is_named_tuple_assignment(stmt, checker.semantic()) + && !helpers::is_typed_dict_assignment(stmt, checker.semantic()) + && !helpers::is_type_var_assignment(stmt, checker.semantic()) { checker.diagnostics.push(Diagnostic::new( NonLowercaseVariableInFunction { diff --git a/crates/ruff/src/rules/pep8_naming/settings.rs b/crates/ruff/src/rules/pep8_naming/settings.rs index cfacb63632..12e301ecb8 100644 --- a/crates/ruff/src/rules/pep8_naming/settings.rs +++ b/crates/ruff/src/rules/pep8_naming/settings.rs @@ -1,9 +1,14 @@ //! Settings for the `pep8-naming` plugin. +use std::error::Error; +use std::fmt; + use serde::{Deserialize, Serialize}; use ruff_macros::{CacheKey, CombineOptions, ConfigurationOptions}; +use crate::settings::types::IdentifierPattern; + const IGNORE_NAMES: [&str; 12] = [ "setUp", "tearDown", @@ -36,7 +41,7 @@ pub struct Options { ignore-names = ["callMethod"] "# )] - /// A list of names to ignore when considering `pep8-naming` violations. + /// A list of names (or patterns) to ignore when considering `pep8-naming` violations. pub ignore_names: Option>, #[option( default = r#"[]"#, @@ -72,7 +77,7 @@ pub struct Options { #[derive(Debug, CacheKey)] pub struct Settings { - pub ignore_names: Vec, + pub ignore_names: Vec, pub classmethod_decorators: Vec, pub staticmethod_decorators: Vec, } @@ -80,21 +85,59 @@ pub struct Settings { impl Default for Settings { fn default() -> Self { Self { - ignore_names: IGNORE_NAMES.map(String::from).to_vec(), + ignore_names: IGNORE_NAMES + .iter() + .map(|name| IdentifierPattern::new(name).unwrap()) + .collect(), classmethod_decorators: Vec::new(), staticmethod_decorators: Vec::new(), } } } -impl From for Settings { - fn from(options: Options) -> Self { - Self { - ignore_names: options - .ignore_names - .unwrap_or_else(|| IGNORE_NAMES.map(String::from).to_vec()), +impl TryFrom for Settings { + type Error = SettingsError; + + fn try_from(options: Options) -> Result { + Ok(Self { + ignore_names: match options.ignore_names { + Some(names) => names + .into_iter() + .map(|name| { + IdentifierPattern::new(&name).map_err(SettingsError::InvalidIgnoreName) + }) + .collect::, Self::Error>>()?, + None => IGNORE_NAMES + .into_iter() + .map(|name| IdentifierPattern::new(name).unwrap()) + .collect(), + }, classmethod_decorators: options.classmethod_decorators.unwrap_or_default(), staticmethod_decorators: options.staticmethod_decorators.unwrap_or_default(), + }) + } +} + +/// Error returned by the [`TryFrom`] implementation of [`Settings`]. +#[derive(Debug)] +pub enum SettingsError { + InvalidIgnoreName(glob::PatternError), +} + +impl fmt::Display for SettingsError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + SettingsError::InvalidIgnoreName(err) => { + write!(f, "Invalid pattern in ignore-names: {err}") + } + } + } +} + +impl Error for SettingsError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + SettingsError::InvalidIgnoreName(err) => Some(err), } } } @@ -102,7 +145,13 @@ impl From for Settings { impl From for Options { fn from(settings: Settings) -> Self { Self { - ignore_names: Some(settings.ignore_names), + ignore_names: Some( + settings + .ignore_names + .into_iter() + .map(|pattern| pattern.as_str().to_owned()) + .collect(), + ), classmethod_decorators: Some(settings.classmethod_decorators), staticmethod_decorators: Some(settings.staticmethod_decorators), } diff --git a/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__N805_N805.py.snap b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__N805_N805.py.snap index 9c4369878a..714a639ab7 100644 --- a/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__N805_N805.py.snap +++ b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__N805_N805.py.snap @@ -18,29 +18,29 @@ N805.py:12:30: N805 First argument of a method should be named `self` 13 | pass | -N805.py:27:15: N805 First argument of a method should be named `self` - | -26 | @pydantic.validator -27 | def lower(cls, my_field: str) -> str: - | ^^^ N805 -28 | pass - | - N805.py:31:15: N805 First argument of a method should be named `self` | -30 | @pydantic.validator("my_field") +30 | @pydantic.validator 31 | def lower(cls, my_field: str) -> str: | ^^^ N805 32 | pass | -N805.py:60:29: N805 First argument of a method should be named `self` +N805.py:35:15: N805 First argument of a method should be named `self` | -58 | pass -59 | -60 | def bad_method_pos_only(this, blah, /, self, something: str): +34 | @pydantic.validator("my_field") +35 | def lower(cls, my_field: str) -> str: + | ^^^ N805 +36 | pass + | + +N805.py:64:29: N805 First argument of a method should be named `self` + | +62 | pass +63 | +64 | def bad_method_pos_only(this, blah, /, self, something: str): | ^^^^ N805 -61 | pass +65 | pass | diff --git a/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__classmethod_decorators.snap b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__classmethod_decorators.snap index a6d3bc8592..06c9407f28 100644 --- a/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__classmethod_decorators.snap +++ b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__classmethod_decorators.snap @@ -18,13 +18,13 @@ N805.py:12:30: N805 First argument of a method should be named `self` 13 | pass | -N805.py:60:29: N805 First argument of a method should be named `self` +N805.py:64:29: N805 First argument of a method should be named `self` | -58 | pass -59 | -60 | def bad_method_pos_only(this, blah, /, self, something: str): +62 | pass +63 | +64 | def bad_method_pos_only(this, blah, /, self, something: str): | ^^^^ N805 -61 | pass +65 | pass | diff --git a/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N801_N801.py.snap b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N801_N801.py.snap new file mode 100644 index 0000000000..cbe43359c0 --- /dev/null +++ b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N801_N801.py.snap @@ -0,0 +1,22 @@ +--- +source: crates/ruff/src/rules/pep8_naming/mod.rs +--- +N801.py:4:7: N801 Class name `stillBad` should use CapWords convention + | +2 | pass +3 | +4 | class stillBad: + | ^^^^^^^^ N801 +5 | pass + | + +N801.py:10:7: N801 Class name `STILL_BAD` should use CapWords convention + | + 8 | pass + 9 | +10 | class STILL_BAD: + | ^^^^^^^^^ N801 +11 | pass + | + + diff --git a/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N802_N802.py.snap b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N802_N802.py.snap new file mode 100644 index 0000000000..9b33e0be3e --- /dev/null +++ b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N802_N802.py.snap @@ -0,0 +1,22 @@ +--- +source: crates/ruff/src/rules/pep8_naming/mod.rs +--- +N802.py:6:5: N802 Function name `stillBad` should be lowercase + | +4 | pass +5 | +6 | def stillBad(): + | ^^^^^^^^ N802 +7 | pass + | + +N802.py:13:9: N802 Function name `stillBad` should be lowercase + | +11 | return super().tearDown() +12 | +13 | def stillBad(self): + | ^^^^^^^^ N802 +14 | return super().tearDown() + | + + diff --git a/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N803_N803.py.snap b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N803_N803.py.snap new file mode 100644 index 0000000000..92918cc420 --- /dev/null +++ b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N803_N803.py.snap @@ -0,0 +1,22 @@ +--- +source: crates/ruff/src/rules/pep8_naming/mod.rs +--- +N803.py:4:16: N803 Argument name `stillBad` should be lowercase + | +2 | return _, a, badAllowed +3 | +4 | def func(_, a, stillBad): + | ^^^^^^^^ N803 +5 | return _, a, stillBad + | + +N803.py:11:28: N803 Argument name `stillBad` should be lowercase + | + 9 | return _, a, badAllowed +10 | +11 | def method(self, _, a, stillBad): + | ^^^^^^^^ N803 +12 | return _, a, stillBad + | + + diff --git a/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N804_N804.py.snap b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N804_N804.py.snap new file mode 100644 index 0000000000..c308109147 --- /dev/null +++ b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N804_N804.py.snap @@ -0,0 +1,29 @@ +--- +source: crates/ruff/src/rules/pep8_naming/mod.rs +--- +N804.py:5:27: N804 First argument of a class method should be named `cls` + | +4 | class Class: +5 | def __init_subclass__(self, default_name, **kwargs): + | ^^^^ N804 +6 | ... + | + +N804.py:13:18: N804 First argument of a class method should be named `cls` + | +12 | @classmethod +13 | def stillBad(self, x, /, other): + | ^^^^ N804 +14 | ... + | + +N804.py:21:18: N804 First argument of a class method should be named `cls` + | +19 | pass +20 | +21 | def stillBad(self): + | ^^^^ N804 +22 | pass + | + + diff --git a/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N805_N805.py.snap b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N805_N805.py.snap new file mode 100644 index 0000000000..fee3fee254 --- /dev/null +++ b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N805_N805.py.snap @@ -0,0 +1,47 @@ +--- +source: crates/ruff/src/rules/pep8_naming/mod.rs +--- +N805.py:10:18: N805 First argument of a method should be named `self` + | + 8 | pass + 9 | +10 | def stillBad(this): + | ^^^^ N805 +11 | pass + | + +N805.py:18:22: N805 First argument of a method should be named `self` + | +16 | pass +17 | +18 | def stillBad(this): + | ^^^^ N805 +19 | pass + | + +N805.py:26:18: N805 First argument of a method should be named `self` + | +25 | @pydantic.validator +26 | def stillBad(cls, my_field: str) -> str: + | ^^^ N805 +27 | pass + | + +N805.py:34:18: N805 First argument of a method should be named `self` + | +33 | @pydantic.validator("my_field") +34 | def stillBad(cls, my_field: str) -> str: + | ^^^ N805 +35 | pass + | + +N805.py:58:18: N805 First argument of a method should be named `self` + | +56 | pass +57 | +58 | def stillBad(this, blah, /, self, something: str): + | ^^^^ N805 +59 | pass + | + + diff --git a/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N806_N806.py.snap b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N806_N806.py.snap new file mode 100644 index 0000000000..eff8113cce --- /dev/null +++ b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N806_N806.py.snap @@ -0,0 +1,21 @@ +--- +source: crates/ruff/src/rules/pep8_naming/mod.rs +--- +N806.py:3:5: N806 Variable `stillBad` in function should be lowercase + | +1 | def assign(): +2 | badAllowed = 0 +3 | stillBad = 0 + | ^^^^^^^^ N806 +4 | +5 | BAD_ALLOWED = 0 + | + +N806.py:6:5: N806 Variable `STILL_BAD` in function should be lowercase + | +5 | BAD_ALLOWED = 0 +6 | STILL_BAD = 0 + | ^^^^^^^^^ N806 + | + + diff --git a/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N807_N807.py.snap b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N807_N807.py.snap new file mode 100644 index 0000000000..b3ba02329b --- /dev/null +++ b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N807_N807.py.snap @@ -0,0 +1,22 @@ +--- +source: crates/ruff/src/rules/pep8_naming/mod.rs +--- +N807.py:4:5: N807 Function name should not start and end with `__` + | +2 | pass +3 | +4 | def __stillBad__(): + | ^^^^^^^^^^^^ N807 +5 | pass + | + +N807.py:12:9: N807 Function name should not start and end with `__` + | +10 | pass +11 | +12 | def __stillBad__(): + | ^^^^^^^^^^^^ N807 +13 | pass + | + + diff --git a/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N811_N811.py.snap b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N811_N811.py.snap new file mode 100644 index 0000000000..099823a45b --- /dev/null +++ b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N811_N811.py.snap @@ -0,0 +1,20 @@ +--- +source: crates/ruff/src/rules/pep8_naming/mod.rs +--- +N811.py:2:8: N811 Constant `STILL_BAD` imported as non-constant `stillBad` + | +1 | import mod.BAD_ALLOWED as badAllowed +2 | import mod.STILL_BAD as stillBad + | ^^^^^^^^^^^^^^^^^^^^^^^^^ N811 +3 | +4 | from mod import BAD_ALLOWED as badAllowed + | + +N811.py:5:17: N811 Constant `STILL_BAD` imported as non-constant `stillBad` + | +4 | from mod import BAD_ALLOWED as badAllowed +5 | from mod import STILL_BAD as stillBad + | ^^^^^^^^^^^^^^^^^^^^^ N811 + | + + diff --git a/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N812_N812.py.snap b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N812_N812.py.snap new file mode 100644 index 0000000000..3f5caafe57 --- /dev/null +++ b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N812_N812.py.snap @@ -0,0 +1,20 @@ +--- +source: crates/ruff/src/rules/pep8_naming/mod.rs +--- +N812.py:2:8: N812 Lowercase `stillbad` imported as non-lowercase `stillBad` + | +1 | import mod.badallowed as badAllowed +2 | import mod.stillbad as stillBad + | ^^^^^^^^^^^^^^^^^^^^^^^^ N812 +3 | +4 | from mod import badallowed as BadAllowed + | + +N812.py:5:17: N812 Lowercase `stillbad` imported as non-lowercase `StillBad` + | +4 | from mod import badallowed as BadAllowed +5 | from mod import stillbad as StillBad + | ^^^^^^^^^^^^^^^^^^^^ N812 + | + + diff --git a/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N813_N813.py.snap b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N813_N813.py.snap new file mode 100644 index 0000000000..814fb4c88d --- /dev/null +++ b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N813_N813.py.snap @@ -0,0 +1,29 @@ +--- +source: crates/ruff/src/rules/pep8_naming/mod.rs +--- +N813.py:2:8: N813 Camelcase `stillBad` imported as lowercase `stillbad` + | +1 | import mod.BadAllowed as badallowed +2 | import mod.stillBad as stillbad + | ^^^^^^^^^^^^^^^^^^^^^^^^ N813 +3 | +4 | from mod import BadAllowed as badallowed + | + +N813.py:5:17: N813 Camelcase `StillBad` imported as lowercase `stillbad` + | +4 | from mod import BadAllowed as badallowed +5 | from mod import StillBad as stillbad + | ^^^^^^^^^^^^^^^^^^^^ N813 +6 | +7 | from mod import BadAllowed as bad_allowed + | + +N813.py:8:17: N813 Camelcase `StillBad` imported as lowercase `still_bad` + | +7 | from mod import BadAllowed as bad_allowed +8 | from mod import StillBad as still_bad + | ^^^^^^^^^^^^^^^^^^^^^ N813 + | + + diff --git a/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N814_N814.py.snap b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N814_N814.py.snap new file mode 100644 index 0000000000..cd367bfe6a --- /dev/null +++ b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N814_N814.py.snap @@ -0,0 +1,29 @@ +--- +source: crates/ruff/src/rules/pep8_naming/mod.rs +--- +N814.py:2:8: N814 Camelcase `StillBad` imported as constant `STILLBAD` + | +1 | import mod.BadAllowed as BADALLOWED +2 | import mod.StillBad as STILLBAD + | ^^^^^^^^^^^^^^^^^^^^^^^^ N814 +3 | +4 | from mod import BadAllowed as BADALLOWED + | + +N814.py:5:17: N814 Camelcase `StillBad` imported as constant `STILLBAD` + | +4 | from mod import BadAllowed as BADALLOWED +5 | from mod import StillBad as STILLBAD + | ^^^^^^^^^^^^^^^^^^^^ N814 +6 | +7 | from mod import BadAllowed as BAD_ALLOWED + | + +N814.py:8:17: N814 Camelcase `StillBad` imported as constant `STILL_BAD` + | +7 | from mod import BadAllowed as BAD_ALLOWED +8 | from mod import StillBad as STILL_BAD + | ^^^^^^^^^^^^^^^^^^^^^ N814 + | + + diff --git a/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N815_N815.py.snap b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N815_N815.py.snap new file mode 100644 index 0000000000..90cdc67709 --- /dev/null +++ b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N815_N815.py.snap @@ -0,0 +1,58 @@ +--- +source: crates/ruff/src/rules/pep8_naming/mod.rs +--- +N815.py:3:5: N815 Variable `stillBad` in class scope should not be mixedCase + | +1 | class C: +2 | badAllowed = 0 +3 | stillBad = 0 + | ^^^^^^^^ N815 +4 | +5 | _badAllowed = 0 + | + +N815.py:6:5: N815 Variable `_stillBad` in class scope should not be mixedCase + | +5 | _badAllowed = 0 +6 | _stillBad = 0 + | ^^^^^^^^^ N815 +7 | +8 | bad_Allowed = 0 + | + +N815.py:9:5: N815 Variable `still_Bad` in class scope should not be mixedCase + | + 8 | bad_Allowed = 0 + 9 | still_Bad = 0 + | ^^^^^^^^^ N815 +10 | +11 | class D(TypedDict): + | + +N815.py:13:5: N815 Variable `stillBad` in class scope should not be mixedCase + | +11 | class D(TypedDict): +12 | badAllowed: bool +13 | stillBad: bool + | ^^^^^^^^ N815 +14 | +15 | _badAllowed: list + | + +N815.py:16:5: N815 Variable `_stillBad` in class scope should not be mixedCase + | +15 | _badAllowed: list +16 | _stillBad: list + | ^^^^^^^^^ N815 +17 | +18 | bad_Allowed: set + | + +N815.py:19:5: N815 Variable `still_Bad` in class scope should not be mixedCase + | +18 | bad_Allowed: set +19 | still_Bad: set + | ^^^^^^^^^ N815 + | + + diff --git a/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N816_N816.py.snap b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N816_N816.py.snap new file mode 100644 index 0000000000..9535fc1ba6 --- /dev/null +++ b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N816_N816.py.snap @@ -0,0 +1,29 @@ +--- +source: crates/ruff/src/rules/pep8_naming/mod.rs +--- +N816.py:2:1: N816 Variable `stillBad` in global scope should not be mixedCase + | +1 | badAllowed = 0 +2 | stillBad = 0 + | ^^^^^^^^ N816 +3 | +4 | _badAllowed = 0 + | + +N816.py:5:1: N816 Variable `_stillBad` in global scope should not be mixedCase + | +4 | _badAllowed = 0 +5 | _stillBad = 0 + | ^^^^^^^^^ N816 +6 | +7 | bad_Allowed = 0 + | + +N816.py:8:1: N816 Variable `still_Bad` in global scope should not be mixedCase + | +7 | bad_Allowed = 0 +8 | still_Bad = 0 + | ^^^^^^^^^ N816 + | + + diff --git a/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N817_N817.py.snap b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N817_N817.py.snap new file mode 100644 index 0000000000..e67415d87a --- /dev/null +++ b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N817_N817.py.snap @@ -0,0 +1,20 @@ +--- +source: crates/ruff/src/rules/pep8_naming/mod.rs +--- +N817.py:2:8: N817 CamelCase `StillBad` imported as acronym `SB` + | +1 | import mod.BadAllowed as BA +2 | import mod.StillBad as SB + | ^^^^^^^^^^^^^^^^^^ N817 +3 | +4 | from mod import BadAllowed as BA + | + +N817.py:5:17: N817 CamelCase `StillBad` imported as acronym `SB` + | +4 | from mod import BadAllowed as BA +5 | from mod import StillBad as SB + | ^^^^^^^^^^^^^^ N817 + | + + diff --git a/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N818_N818.py.snap b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N818_N818.py.snap new file mode 100644 index 0000000000..b2df749de6 --- /dev/null +++ b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N818_N818.py.snap @@ -0,0 +1,22 @@ +--- +source: crates/ruff/src/rules/pep8_naming/mod.rs +--- +N818.py:4:7: N818 Exception name `StillBad` should be named with an Error suffix + | +2 | pass +3 | +4 | class StillBad(Exception): + | ^^^^^^^^ N818 +5 | pass + | + +N818.py:10:7: N818 Exception name `StillBad` should be named with an Error suffix + | + 8 | pass + 9 | +10 | class StillBad(AnotherError): + | ^^^^^^^^ N818 +11 | pass + | + + diff --git a/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N999_N999__badAllowed____init__.py.snap b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N999_N999__badAllowed____init__.py.snap new file mode 100644 index 0000000000..eb9fd7a59b --- /dev/null +++ b/crates/ruff/src/rules/pep8_naming/snapshots/ruff__rules__pep8_naming__tests__ignore_names_N999_N999__badAllowed____init__.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/pep8_naming/mod.rs +--- + diff --git a/crates/ruff/src/rules/perflint/mod.rs b/crates/ruff/src/rules/perflint/mod.rs new file mode 100644 index 0000000000..291bfcd207 --- /dev/null +++ b/crates/ruff/src/rules/perflint/mod.rs @@ -0,0 +1,30 @@ +//! Rules from [perflint](https://pypi.org/project/perflint/). +pub(crate) mod rules; + +#[cfg(test)] +mod tests { + use std::path::Path; + + use anyhow::Result; + use test_case::test_case; + + use crate::assert_messages; + use crate::registry::Rule; + use crate::settings::Settings; + use crate::test::test_path; + + #[test_case(Rule::UnnecessaryListCast, Path::new("PERF101.py"))] + #[test_case(Rule::IncorrectDictIterator, Path::new("PERF102.py"))] + #[test_case(Rule::TryExceptInLoop, Path::new("PERF203.py"))] + #[test_case(Rule::ManualListComprehension, Path::new("PERF401.py"))] + #[test_case(Rule::ManualListCopy, Path::new("PERF402.py"))] + fn rules(rule_code: Rule, path: &Path) -> Result<()> { + let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); + let diagnostics = test_path( + Path::new("perflint").join(path).as_path(), + &Settings::for_rule(rule_code), + )?; + assert_messages!(snapshot, diagnostics); + Ok(()) + } +} diff --git a/crates/ruff/src/rules/perflint/rules/incorrect_dict_iterator.rs b/crates/ruff/src/rules/perflint/rules/incorrect_dict_iterator.rs new file mode 100644 index 0000000000..af2677f2f1 --- /dev/null +++ b/crates/ruff/src/rules/perflint/rules/incorrect_dict_iterator.rs @@ -0,0 +1,170 @@ +use std::fmt; + +use ruff_text_size::{TextRange, TextSize}; +use rustpython_parser::ast::Expr; +use rustpython_parser::{ast, lexer, Mode, Tok}; + +use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::source_code::Locator; +use rustpython_parser::ast::Ranged; + +use crate::checkers::ast::Checker; +use crate::registry::AsRule; + +/// ## What it does +/// Checks for uses of `dict.items()` that discard either the key or the value +/// when iterating over the dictionary. +/// +/// ## Why is this bad? +/// If you only need the keys or values of a dictionary, you should use +/// `dict.keys()` or `dict.values()` respectively, instead of `dict.items()`. +/// These specialized methods are more efficient than `dict.items()`, as they +/// avoid allocating tuples for every item in the dictionary. They also +/// communicate the intent of the code more clearly. +/// +/// Note that, as with all `perflint` rules, this is only intended as a +/// micro-optimization, and will have a negligible impact on performance in +/// most cases. +/// +/// ## Example +/// ```python +/// some_dict = {"a": 1, "b": 2} +/// for _, val in some_dict.items(): +/// print(val) +/// ``` +/// +/// Use instead: +/// ```python +/// some_dict = {"a": 1, "b": 2} +/// for val in some_dict.values(): +/// print(val) +/// ``` +#[violation] +pub struct IncorrectDictIterator { + subset: DictSubset, +} + +impl AlwaysAutofixableViolation for IncorrectDictIterator { + #[derive_message_formats] + fn message(&self) -> String { + let IncorrectDictIterator { subset } = self; + format!("When using only the {subset} of a dict use the `{subset}()` method") + } + + fn autofix_title(&self) -> String { + let IncorrectDictIterator { subset } = self; + format!("Replace `.items()` with `.{subset}()`") + } +} + +/// PERF102 +pub(crate) fn incorrect_dict_iterator(checker: &mut Checker, target: &Expr, iter: &Expr) { + let Expr::Tuple(ast::ExprTuple { elts, .. }) = target else { + return; + }; + if elts.len() != 2 { + return; + } + let Expr::Call(ast::ExprCall { func, args, .. }) = iter else { + return; + }; + if !args.is_empty() { + return; + } + let Expr::Attribute(ast::ExprAttribute { attr, value, .. }) = func.as_ref() else { + return; + }; + if attr != "items" { + return; + } + + let unused_key = is_ignored_tuple_or_name(&elts[0]); + let unused_value = is_ignored_tuple_or_name(&elts[1]); + + match (unused_key, unused_value) { + (true, true) => { + // Both the key and the value are unused. + } + (false, false) => { + // Neither the key nor the value are unused. + } + (true, false) => { + // The key is unused, so replace with `dict.values()`. + let mut diagnostic = Diagnostic::new( + IncorrectDictIterator { + subset: DictSubset::Values, + }, + func.range(), + ); + if checker.patch(diagnostic.kind.rule()) { + if let Some(range) = attribute_range(value.end(), checker.locator) { + let replace_attribute = Edit::range_replacement("values".to_string(), range); + let replace_target = Edit::range_replacement( + checker.locator.slice(elts[1].range()).to_string(), + target.range(), + ); + diagnostic.set_fix(Fix::suggested_edits(replace_attribute, [replace_target])); + } + } + checker.diagnostics.push(diagnostic); + } + (false, true) => { + // The value is unused, so replace with `dict.keys()`. + let mut diagnostic = Diagnostic::new( + IncorrectDictIterator { + subset: DictSubset::Keys, + }, + func.range(), + ); + if checker.patch(diagnostic.kind.rule()) { + if let Some(range) = attribute_range(value.end(), checker.locator) { + let replace_attribute = Edit::range_replacement("keys".to_string(), range); + let replace_target = Edit::range_replacement( + checker.locator.slice(elts[0].range()).to_string(), + target.range(), + ); + diagnostic.set_fix(Fix::suggested_edits(replace_attribute, [replace_target])); + } + } + checker.diagnostics.push(diagnostic); + } + } +} + +#[derive(Debug, PartialEq, Eq)] +enum DictSubset { + Keys, + Values, +} + +impl fmt::Display for DictSubset { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match self { + DictSubset::Keys => fmt.write_str("keys"), + DictSubset::Values => fmt.write_str("values"), + } + } +} + +/// Returns `true` if the given expression is either an ignored value or a tuple of ignored values. +fn is_ignored_tuple_or_name(expr: &Expr) -> bool { + match expr { + Expr::Tuple(ast::ExprTuple { elts, .. }) => elts.iter().all(is_ignored_tuple_or_name), + Expr::Name(ast::ExprName { id, .. }) => id == "_", + _ => false, + } +} + +/// Returns the range of the attribute identifier after the given location, if any. +fn attribute_range(at: TextSize, locator: &Locator) -> Option { + lexer::lex_starts_at(locator.after(at), Mode::Expression, at) + .flatten() + .find_map(|(tok, range)| { + if matches!(tok, Tok::Name { .. }) { + Some(range) + } else { + None + } + }) +} diff --git a/crates/ruff/src/rules/perflint/rules/manual_list_comprehension.rs b/crates/ruff/src/rules/perflint/rules/manual_list_comprehension.rs new file mode 100644 index 0000000000..b5bf5ea1ae --- /dev/null +++ b/crates/ruff/src/rules/perflint/rules/manual_list_comprehension.rs @@ -0,0 +1,157 @@ +use rustpython_parser::ast::{self, Expr, Stmt}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers::any_over_expr; + +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for `for` loops that can be replaced by a list comprehension. +/// +/// ## Why is this bad? +/// When creating a transformed list from an existing list using a for-loop, +/// prefer a list comprehension. List comprehensions are more readable and +/// more performant. +/// +/// Using the below as an example, the list comprehension is ~10% faster on +/// Python 3.11, and ~25% faster on Python 3.10. +/// +/// Note that, as with all `perflint` rules, this is only intended as a +/// micro-optimization, and will have a negligible impact on performance in +/// most cases. +/// +/// ## Example +/// ```python +/// original = list(range(10000)) +/// filtered = [] +/// for i in original: +/// if i % 2: +/// filtered.append(i) +/// ``` +/// +/// Use instead: +/// ```python +/// original = list(range(10000)) +/// filtered = [x for x in original if x % 2] +/// ``` +/// +/// If you're appending to an existing list, use the `extend` method instead: +/// ```python +/// original = list(range(10000)) +/// filtered.extend(x for x in original if x % 2) +/// ``` +#[violation] +pub struct ManualListComprehension; + +impl Violation for ManualListComprehension { + #[derive_message_formats] + fn message(&self) -> String { + format!("Use a list comprehension to create a transformed list") + } +} + +/// PERF401 +pub(crate) fn manual_list_comprehension(checker: &mut Checker, target: &Expr, body: &[Stmt]) { + let Expr::Name(ast::ExprName { id, .. }) = target else { + return; + }; + + let (stmt, if_test) = match body { + // ```python + // for x in y: + // if z: + // filtered.append(x) + // ``` + [Stmt::If(ast::StmtIf { + body, orelse, test, .. + })] => { + if !orelse.is_empty() { + return; + } + let [stmt] = body.as_slice() else { + return; + }; + (stmt, Some(test)) + } + // ```python + // for x in y: + // filtered.append(f(x)) + // ``` + [stmt] => (stmt, None), + _ => return, + }; + + let Stmt::Expr(ast::StmtExpr { value, .. }) = stmt else { + return; + }; + + let Expr::Call(ast::ExprCall { + func, + range, + args, + keywords, + }) = value.as_ref() + else { + return; + }; + + if !keywords.is_empty() { + return; + } + + let [arg] = args.as_slice() else { + return; + }; + + // Ignore direct list copies (e.g., `for x in y: filtered.append(x)`). + if if_test.is_none() { + if arg.as_name_expr().map_or(false, |arg| arg.id == *id) { + return; + } + } + + let Expr::Attribute(ast::ExprAttribute { attr, value, .. }) = func.as_ref() else { + return; + }; + + if attr.as_str() != "append" { + return; + } + + // Avoid, e.g., `for x in y: filtered[x].append(x * x)`. + if any_over_expr(value, &|expr| { + expr.as_name_expr().map_or(false, |expr| expr.id == *id) + }) { + return; + } + + // Avoid if the value is used in the conditional test, e.g., + // + // ```python + // for x in y: + // if x in filtered: + // filtered.append(x) + // ``` + // + // Converting this to a list comprehension would raise a `NameError` as + // `filtered` is not defined yet: + // + // ```python + // filtered = [x for x in y if x in filtered] + // ``` + if let Some(value_name) = value.as_name_expr() { + if if_test.map_or(false, |test| { + any_over_expr(test, &|expr| { + expr.as_name_expr() + .map_or(false, |expr| expr.id == value_name.id) + }) + }) { + return; + } + } + + checker + .diagnostics + .push(Diagnostic::new(ManualListComprehension, *range)); +} diff --git a/crates/ruff/src/rules/perflint/rules/manual_list_copy.rs b/crates/ruff/src/rules/perflint/rules/manual_list_copy.rs new file mode 100644 index 0000000000..3752c4eb1c --- /dev/null +++ b/crates/ruff/src/rules/perflint/rules/manual_list_copy.rs @@ -0,0 +1,102 @@ +use rustpython_parser::ast::{self, Expr, Stmt}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers::any_over_expr; + +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for `for` loops that can be replaced by a making a copy of a list. +/// +/// ## Why is this bad? +/// When creating a copy of an existing list using a for-loop, prefer +/// `list` or `list.copy` instead. Making a direct copy is more readable and +/// more performant. +/// +/// Using the below as an example, the `list`-based copy is ~2x faster on +/// Python 3.11. +/// +/// Note that, as with all `perflint` rules, this is only intended as a +/// micro-optimization, and will have a negligible impact on performance in +/// most cases. +/// +/// ## Example +/// ```python +/// original = list(range(10000)) +/// filtered = [] +/// for i in original: +/// filtered.append(i) +/// ``` +/// +/// Use instead: +/// ```python +/// original = list(range(10000)) +/// filtered = list(original) +/// ``` +#[violation] +pub struct ManualListCopy; + +impl Violation for ManualListCopy { + #[derive_message_formats] + fn message(&self) -> String { + format!("Use `list` or `list.copy` to create a copy of a list") + } +} + +/// PERF402 +pub(crate) fn manual_list_copy(checker: &mut Checker, target: &Expr, body: &[Stmt]) { + let Expr::Name(ast::ExprName { id, .. }) = target else { + return; + }; + + let [stmt] = body else { + return; + }; + + let Stmt::Expr(ast::StmtExpr { value, .. }) = stmt else { + return; + }; + + let Expr::Call(ast::ExprCall { + func, + range, + args, + keywords, + }) = value.as_ref() + else { + return; + }; + + if !keywords.is_empty() { + return; + } + + let [arg] = args.as_slice() else { + return; + }; + + // Only flag direct list copies (e.g., `for x in y: filtered.append(x)`). + if !arg.as_name_expr().map_or(false, |arg| arg.id == *id) { + return; + } + + let Expr::Attribute(ast::ExprAttribute { attr, value, .. }) = func.as_ref() else { + return; + }; + + if !matches!(attr.as_str(), "append" | "insert") { + return; + } + + // Avoid, e.g., `for x in y: filtered[x].append(x * x)`. + if any_over_expr(value, &|expr| { + expr.as_name_expr().map_or(false, |expr| expr.id == *id) + }) { + return; + } + + checker + .diagnostics + .push(Diagnostic::new(ManualListCopy, *range)); +} diff --git a/crates/ruff/src/rules/perflint/rules/mod.rs b/crates/ruff/src/rules/perflint/rules/mod.rs new file mode 100644 index 0000000000..690b0fc1fe --- /dev/null +++ b/crates/ruff/src/rules/perflint/rules/mod.rs @@ -0,0 +1,11 @@ +pub(crate) use incorrect_dict_iterator::*; +pub(crate) use manual_list_comprehension::*; +pub(crate) use manual_list_copy::*; +pub(crate) use try_except_in_loop::*; +pub(crate) use unnecessary_list_cast::*; + +mod incorrect_dict_iterator; +mod manual_list_comprehension; +mod manual_list_copy; +mod try_except_in_loop; +mod unnecessary_list_cast; diff --git a/crates/ruff/src/rules/perflint/rules/try_except_in_loop.rs b/crates/ruff/src/rules/perflint/rules/try_except_in_loop.rs new file mode 100644 index 0000000000..eeeed136ed --- /dev/null +++ b/crates/ruff/src/rules/perflint/rules/try_except_in_loop.rs @@ -0,0 +1,80 @@ +use rustpython_parser::ast::{self, Ranged, Stmt}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; + +use crate::checkers::ast::Checker; +use crate::settings::types::PythonVersion; + +/// ## What it does +/// Checks for uses of except handling via `try`-`except` within `for` and +/// `while` loops. +/// +/// ## Why is this bad? +/// Exception handling via `try`-`except` blocks incurs some performance +/// overhead, regardless of whether an exception is raised. +/// +/// When possible, refactor your code to put the entire loop into the +/// `try`-`except` block, rather than wrapping each iteration in a separate +/// `try`-`except` block. +/// +/// This rule is only enforced for Python versions prior to 3.11, which +/// introduced "zero cost" exception handling. +/// +/// Note that, as with all `perflint` rules, this is only intended as a +/// micro-optimization, and will have a negligible impact on performance in +/// most cases. +/// +/// ## Example +/// ```python +/// string_numbers: list[str] = ["1", "2", "three", "4", "5"] +/// +/// int_numbers: list[int] = [] +/// for num in string_numbers: +/// try: +/// int_numbers.append(int(num)) +/// except ValueError as e: +/// print(f"Couldn't convert to integer: {e}") +/// ``` +/// +/// Use instead: +/// ```python +/// string_numbers: list[str] = ["1", "2", "three", "4", "5"] +/// +/// int_numbers: list[int] = [] +/// try: +/// for num in string_numbers: +/// int_numbers.append(int(num)) +/// except ValueError as e: +/// print(f"Couldn't convert to integer: {e}") +/// ``` +/// +/// ## Options +/// - `target-version` +#[violation] +pub struct TryExceptInLoop; + +impl Violation for TryExceptInLoop { + #[derive_message_formats] + fn message(&self) -> String { + format!("`try`-`except` within a loop incurs performance overhead") + } +} + +/// PERF203 +pub(crate) fn try_except_in_loop(checker: &mut Checker, body: &[Stmt]) { + if checker.settings.target_version >= PythonVersion::Py311 { + return; + } + + checker.diagnostics.extend(body.iter().filter_map(|stmt| { + if let Stmt::Try(ast::StmtTry { handlers, .. }) = stmt { + handlers + .iter() + .next() + .map(|handler| Diagnostic::new(TryExceptInLoop, handler.range())) + } else { + None + } + })); +} diff --git a/crates/ruff/src/rules/perflint/rules/unnecessary_list_cast.rs b/crates/ruff/src/rules/perflint/rules/unnecessary_list_cast.rs new file mode 100644 index 0000000000..670256d10a --- /dev/null +++ b/crates/ruff/src/rules/perflint/rules/unnecessary_list_cast.rs @@ -0,0 +1,139 @@ +use ruff_text_size::TextRange; +use rustpython_parser::ast::{self, Expr}; + +use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; +use ruff_macros::{derive_message_formats, violation}; +use rustpython_parser::ast::Stmt; + +use crate::checkers::ast::Checker; +use crate::registry::AsRule; + +/// ## What it does +/// Checks for explicit casts to `list` on for-loop iterables. +/// +/// ## Why is this bad? +/// Using a `list()` call to eagerly iterate over an already-iterable type +/// (like a tuple, list, or set) is inefficient, as it forces Python to create +/// a new list unnecessarily. +/// +/// Removing the `list()` call will not change the behavior of the code, but +/// may improve performance. +/// +/// Note that, as with all `perflint` rules, this is only intended as a +/// micro-optimization, and will have a negligible impact on performance in +/// most cases. +/// +/// ## Example +/// ```python +/// items = (1, 2, 3) +/// for i in list(items): +/// print(i) +/// ``` +/// +/// Use instead: +/// ```python +/// items = (1, 2, 3) +/// for i in items: +/// print(i) +/// ``` +#[violation] +pub struct UnnecessaryListCast; + +impl AlwaysAutofixableViolation for UnnecessaryListCast { + #[derive_message_formats] + fn message(&self) -> String { + format!("Do not cast an iterable to `list` before iterating over it") + } + + fn autofix_title(&self) -> String { + format!("Remove `list()` cast") + } +} + +/// PERF101 +pub(crate) fn unnecessary_list_cast(checker: &mut Checker, iter: &Expr) { + let Expr::Call(ast::ExprCall { + func, + args, + range: list_range, + .. + }) = iter + else { + return; + }; + + if args.len() != 1 { + return; + } + + let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() else { + return; + }; + + if !(id == "list" && checker.semantic().is_builtin("list")) { + return; + } + + match &args[0] { + Expr::Tuple(ast::ExprTuple { + range: iterable_range, + .. + }) + | Expr::List(ast::ExprList { + range: iterable_range, + .. + }) + | Expr::Set(ast::ExprSet { + range: iterable_range, + .. + }) => { + let mut diagnostic = Diagnostic::new(UnnecessaryListCast, *list_range); + if checker.patch(diagnostic.kind.rule()) { + diagnostic.set_fix(remove_cast(*list_range, *iterable_range)); + } + checker.diagnostics.push(diagnostic); + } + Expr::Name(ast::ExprName { + id, + range: iterable_range, + .. + }) => { + let scope = checker.semantic().scope(); + if let Some(binding_id) = scope.get(id) { + let binding = checker.semantic().binding(binding_id); + if binding.kind.is_assignment() || binding.kind.is_named_expr_assignment() { + if let Some(parent_id) = binding.source { + let parent = checker.semantic().stmts[parent_id]; + if let Stmt::Assign(ast::StmtAssign { value, .. }) + | Stmt::AnnAssign(ast::StmtAnnAssign { + value: Some(value), .. + }) + | Stmt::AugAssign(ast::StmtAugAssign { value, .. }) = parent + { + if matches!( + value.as_ref(), + Expr::Tuple(_) | Expr::List(_) | Expr::Set(_) + ) { + let mut diagnostic = + Diagnostic::new(UnnecessaryListCast, *list_range); + if checker.patch(diagnostic.kind.rule()) { + diagnostic.set_fix(remove_cast(*list_range, *iterable_range)); + } + checker.diagnostics.push(diagnostic); + } + } + } + } + } + } + _ => {} + } +} + +/// Generate a [`Fix`] to remove a `list` cast from an expression. +fn remove_cast(list_range: TextRange, iterable_range: TextRange) -> Fix { + Fix::automatic_edits( + Edit::deletion(list_range.start(), iterable_range.start()), + [Edit::deletion(iterable_range.end(), list_range.end())], + ) +} diff --git a/crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF101_PERF101.py.snap b/crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF101_PERF101.py.snap new file mode 100644 index 0000000000..54a17b4c4f --- /dev/null +++ b/crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF101_PERF101.py.snap @@ -0,0 +1,183 @@ +--- +source: crates/ruff/src/rules/perflint/mod.rs +--- +PERF101.py:7:10: PERF101 [*] Do not cast an iterable to `list` before iterating over it + | +5 | foo_int = 123 +6 | +7 | for i in list(foo_tuple): # PERF101 + | ^^^^^^^^^^^^^^^ PERF101 +8 | pass + | + = help: Remove `list()` cast + +ℹ Fix +4 4 | foo_dict = {1: 2, 3: 4} +5 5 | foo_int = 123 +6 6 | +7 |-for i in list(foo_tuple): # PERF101 + 7 |+for i in foo_tuple: # PERF101 +8 8 | pass +9 9 | +10 10 | for i in list(foo_list): # PERF101 + +PERF101.py:10:10: PERF101 [*] Do not cast an iterable to `list` before iterating over it + | + 8 | pass + 9 | +10 | for i in list(foo_list): # PERF101 + | ^^^^^^^^^^^^^^ PERF101 +11 | pass + | + = help: Remove `list()` cast + +ℹ Fix +7 7 | for i in list(foo_tuple): # PERF101 +8 8 | pass +9 9 | +10 |-for i in list(foo_list): # PERF101 + 10 |+for i in foo_list: # PERF101 +11 11 | pass +12 12 | +13 13 | for i in list(foo_set): # PERF101 + +PERF101.py:13:10: PERF101 [*] Do not cast an iterable to `list` before iterating over it + | +11 | pass +12 | +13 | for i in list(foo_set): # PERF101 + | ^^^^^^^^^^^^^ PERF101 +14 | pass + | + = help: Remove `list()` cast + +ℹ Fix +10 10 | for i in list(foo_list): # PERF101 +11 11 | pass +12 12 | +13 |-for i in list(foo_set): # PERF101 + 13 |+for i in foo_set: # PERF101 +14 14 | pass +15 15 | +16 16 | for i in list((1, 2, 3)): # PERF101 + +PERF101.py:16:10: PERF101 [*] Do not cast an iterable to `list` before iterating over it + | +14 | pass +15 | +16 | for i in list((1, 2, 3)): # PERF101 + | ^^^^^^^^^^^^^^^ PERF101 +17 | pass + | + = help: Remove `list()` cast + +ℹ Fix +13 13 | for i in list(foo_set): # PERF101 +14 14 | pass +15 15 | +16 |-for i in list((1, 2, 3)): # PERF101 + 16 |+for i in (1, 2, 3): # PERF101 +17 17 | pass +18 18 | +19 19 | for i in list([1, 2, 3]): # PERF101 + +PERF101.py:19:10: PERF101 [*] Do not cast an iterable to `list` before iterating over it + | +17 | pass +18 | +19 | for i in list([1, 2, 3]): # PERF101 + | ^^^^^^^^^^^^^^^ PERF101 +20 | pass + | + = help: Remove `list()` cast + +ℹ Fix +16 16 | for i in list((1, 2, 3)): # PERF101 +17 17 | pass +18 18 | +19 |-for i in list([1, 2, 3]): # PERF101 + 19 |+for i in [1, 2, 3]: # PERF101 +20 20 | pass +21 21 | +22 22 | for i in list({1, 2, 3}): # PERF101 + +PERF101.py:22:10: PERF101 [*] Do not cast an iterable to `list` before iterating over it + | +20 | pass +21 | +22 | for i in list({1, 2, 3}): # PERF101 + | ^^^^^^^^^^^^^^^ PERF101 +23 | pass + | + = help: Remove `list()` cast + +ℹ Fix +19 19 | for i in list([1, 2, 3]): # PERF101 +20 20 | pass +21 21 | +22 |-for i in list({1, 2, 3}): # PERF101 + 22 |+for i in {1, 2, 3}: # PERF101 +23 23 | pass +24 24 | +25 25 | for i in list( + +PERF101.py:25:10: PERF101 [*] Do not cast an iterable to `list` before iterating over it + | +23 | pass +24 | +25 | for i in list( + | __________^ +26 | | { +27 | | 1, +28 | | 2, +29 | | 3, +30 | | } +31 | | ): + | |_^ PERF101 +32 | pass + | + = help: Remove `list()` cast + +ℹ Fix +22 22 | for i in list({1, 2, 3}): # PERF101 +23 23 | pass +24 24 | +25 |-for i in list( +26 |- { + 25 |+for i in { +27 26 | 1, +28 27 | 2, +29 28 | 3, +30 |- } +31 |-): + 29 |+ }: +32 30 | pass +33 31 | +34 32 | for i in list( # Comment + +PERF101.py:34:10: PERF101 [*] Do not cast an iterable to `list` before iterating over it + | +32 | pass +33 | +34 | for i in list( # Comment + | __________^ +35 | | {1, 2, 3} +36 | | ): # PERF101 + | |_^ PERF101 +37 | pass + | + = help: Remove `list()` cast + +ℹ Fix +31 31 | ): +32 32 | pass +33 33 | +34 |-for i in list( # Comment +35 |- {1, 2, 3} +36 |-): # PERF101 + 34 |+for i in {1, 2, 3}: # PERF101 +37 35 | pass +38 36 | +39 37 | for i in list(foo_dict): # Ok + + diff --git a/crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF102_PERF102.py.snap b/crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF102_PERF102.py.snap new file mode 100644 index 0000000000..c62a538e0b --- /dev/null +++ b/crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF102_PERF102.py.snap @@ -0,0 +1,167 @@ +--- +source: crates/ruff/src/rules/perflint/mod.rs +--- +PERF102.py:3:17: PERF102 [*] When using only the values of a dict use the `values()` method + | +1 | some_dict = {"a": 12, "b": 32, "c": 44} +2 | +3 | for _, value in some_dict.items(): # PERF102 + | ^^^^^^^^^^^^^^^ PERF102 +4 | print(value) + | + = help: Replace `.items()` with `.values()` + +ℹ Suggested fix +1 1 | some_dict = {"a": 12, "b": 32, "c": 44} +2 2 | +3 |-for _, value in some_dict.items(): # PERF102 + 3 |+for value in some_dict.values(): # PERF102 +4 4 | print(value) +5 5 | +6 6 | + +PERF102.py:7:15: PERF102 [*] When using only the keys of a dict use the `keys()` method + | +7 | for key, _ in some_dict.items(): # PERF102 + | ^^^^^^^^^^^^^^^ PERF102 +8 | print(key) + | + = help: Replace `.items()` with `.keys()` + +ℹ Suggested fix +4 4 | print(value) +5 5 | +6 6 | +7 |-for key, _ in some_dict.items(): # PERF102 + 7 |+for key in some_dict.keys(): # PERF102 +8 8 | print(key) +9 9 | +10 10 | + +PERF102.py:11:26: PERF102 [*] When using only the keys of a dict use the `keys()` method + | +11 | for weird_arg_name, _ in some_dict.items(): # PERF102 + | ^^^^^^^^^^^^^^^ PERF102 +12 | print(weird_arg_name) + | + = help: Replace `.items()` with `.keys()` + +ℹ Suggested fix +8 8 | print(key) +9 9 | +10 10 | +11 |-for weird_arg_name, _ in some_dict.items(): # PERF102 + 11 |+for weird_arg_name in some_dict.keys(): # PERF102 +12 12 | print(weird_arg_name) +13 13 | +14 14 | + +PERF102.py:15:21: PERF102 [*] When using only the keys of a dict use the `keys()` method + | +15 | for name, (_, _) in some_dict.items(): # PERF102 + | ^^^^^^^^^^^^^^^ PERF102 +16 | pass + | + = help: Replace `.items()` with `.keys()` + +ℹ Suggested fix +12 12 | print(weird_arg_name) +13 13 | +14 14 | +15 |-for name, (_, _) in some_dict.items(): # PERF102 + 15 |+for name in some_dict.keys(): # PERF102 +16 16 | pass +17 17 | +18 18 | + +PERF102.py:23:26: PERF102 [*] When using only the keys of a dict use the `keys()` method + | +23 | for (key1, _), (_, _) in some_dict.items(): # PERF102 + | ^^^^^^^^^^^^^^^ PERF102 +24 | pass + | + = help: Replace `.items()` with `.keys()` + +ℹ Suggested fix +20 20 | pass +21 21 | +22 22 | +23 |-for (key1, _), (_, _) in some_dict.items(): # PERF102 + 23 |+for (key1, _) in some_dict.keys(): # PERF102 +24 24 | pass +25 25 | +26 26 | + +PERF102.py:27:32: PERF102 [*] When using only the values of a dict use the `values()` method + | +27 | for (_, (_, _)), (value, _) in some_dict.items(): # PERF102 + | ^^^^^^^^^^^^^^^ PERF102 +28 | pass + | + = help: Replace `.items()` with `.values()` + +ℹ Suggested fix +24 24 | pass +25 25 | +26 26 | +27 |-for (_, (_, _)), (value, _) in some_dict.items(): # PERF102 + 27 |+for (value, _) in some_dict.values(): # PERF102 +28 28 | pass +29 29 | +30 30 | + +PERF102.py:39:28: PERF102 [*] When using only the keys of a dict use the `keys()` method + | +39 | for ((_, key2), (_, _)) in some_dict.items(): # PERF102 + | ^^^^^^^^^^^^^^^ PERF102 +40 | pass + | + = help: Replace `.items()` with `.keys()` + +ℹ Suggested fix +36 36 | pass +37 37 | +38 38 | +39 |-for ((_, key2), (_, _)) in some_dict.items(): # PERF102 + 39 |+for (_, key2) in some_dict.keys(): # PERF102 +40 40 | pass +41 41 | +42 42 | + +PERF102.py:67:21: PERF102 [*] When using only the keys of a dict use the `keys()` method + | +67 | for name, (_, _) in (some_function()).items(): # PERF102 + | ^^^^^^^^^^^^^^^^^^^^^^^ PERF102 +68 | pass + | + = help: Replace `.items()` with `.keys()` + +ℹ Suggested fix +64 64 | print(value) +65 65 | +66 66 | +67 |-for name, (_, _) in (some_function()).items(): # PERF102 + 67 |+for name in (some_function()).keys(): # PERF102 +68 68 | pass +69 69 | +70 70 | for name, (_, _) in (some_function().some_attribute).items(): # PERF102 + +PERF102.py:70:21: PERF102 [*] When using only the keys of a dict use the `keys()` method + | +68 | pass +69 | +70 | for name, (_, _) in (some_function().some_attribute).items(): # PERF102 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PERF102 +71 | pass + | + = help: Replace `.items()` with `.keys()` + +ℹ Suggested fix +67 67 | for name, (_, _) in (some_function()).items(): # PERF102 +68 68 | pass +69 69 | +70 |-for name, (_, _) in (some_function().some_attribute).items(): # PERF102 + 70 |+for name in (some_function().some_attribute).keys(): # PERF102 +71 71 | pass + + diff --git a/crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF203_PERF203.py.snap b/crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF203_PERF203.py.snap new file mode 100644 index 0000000000..08a287819e --- /dev/null +++ b/crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF203_PERF203.py.snap @@ -0,0 +1,28 @@ +--- +source: crates/ruff/src/rules/perflint/mod.rs +--- +PERF203.py:4:5: PERF203 `try`-`except` within a loop incurs performance overhead + | +2 | try: # PERF203 +3 | print(f"{i}") +4 | except: + | _____^ +5 | | print("error") + | |______________________^ PERF203 +6 | +7 | try: + | + +PERF203.py:17:5: PERF203 `try`-`except` within a loop incurs performance overhead + | +15 | try: +16 | print(f"{i}") +17 | except: + | _____^ +18 | | print("error") + | |______________________^ PERF203 +19 | +20 | i += 1 + | + + diff --git a/crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF401_PERF401.py.snap b/crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF401_PERF401.py.snap new file mode 100644 index 0000000000..cf2e2677c5 --- /dev/null +++ b/crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF401_PERF401.py.snap @@ -0,0 +1,20 @@ +--- +source: crates/ruff/src/rules/perflint/mod.rs +--- +PERF401.py:6:13: PERF401 Use a list comprehension to create a transformed list + | +4 | for i in items: +5 | if i % 2: +6 | result.append(i) # PERF401 + | ^^^^^^^^^^^^^^^^ PERF401 + | + +PERF401.py:13:9: PERF401 Use a list comprehension to create a transformed list + | +11 | result = [] +12 | for i in items: +13 | result.append(i * i) # PERF401 + | ^^^^^^^^^^^^^^^^^^^^ PERF401 + | + + diff --git a/crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF402_PERF402.py.snap b/crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF402_PERF402.py.snap new file mode 100644 index 0000000000..e56584c95e --- /dev/null +++ b/crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF402_PERF402.py.snap @@ -0,0 +1,12 @@ +--- +source: crates/ruff/src/rules/perflint/mod.rs +--- +PERF402.py:5:9: PERF402 Use `list` or `list.copy` to create a copy of a list + | +3 | result = [] +4 | for i in items: +5 | result.append(i) # PERF402 + | ^^^^^^^^^^^^^^^^ PERF402 + | + + diff --git a/crates/ruff/src/rules/pycodestyle/helpers.rs b/crates/ruff/src/rules/pycodestyle/helpers.rs index 967efe60b2..ab8b04537a 100644 --- a/crates/ruff/src/rules/pycodestyle/helpers.rs +++ b/crates/ruff/src/rules/pycodestyle/helpers.rs @@ -1,5 +1,5 @@ use ruff_text_size::{TextLen, TextRange}; -use rustpython_parser::ast::{self, Cmpop, Expr}; +use rustpython_parser::ast::{self, CmpOp, Expr}; use unicode_width::UnicodeWidthStr; use ruff_python_ast::source_code::Generator; @@ -13,7 +13,7 @@ pub(crate) fn is_ambiguous_name(name: &str) -> bool { pub(crate) fn compare( left: &Expr, - ops: &[Cmpop], + ops: &[CmpOp], comparators: &[Expr], generator: Generator, ) -> String { diff --git a/crates/ruff/src/rules/pycodestyle/rules/bare_except.rs b/crates/ruff/src/rules/pycodestyle/rules/bare_except.rs index f2d591e061..beac9e84fc 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/bare_except.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/bare_except.rs @@ -1,8 +1,8 @@ -use rustpython_parser::ast::{self, Excepthandler, Expr, Stmt}; +use rustpython_parser::ast::{self, ExceptHandler, Expr, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::except_range; +use ruff_python_ast::identifier::except; use ruff_python_ast::source_code::Locator; /// ## What it does @@ -12,7 +12,7 @@ use ruff_python_ast::source_code::Locator; /// A bare `except` catches `BaseException` which includes /// `KeyboardInterrupt`, `SystemExit`, `Exception`, and others. Catching /// `BaseException` can make it hard to interrupt the program (e.g., with -/// Ctrl-C) and disguise other problems. +/// Ctrl-C) and can disguise other problems. /// /// ## Example /// ```python @@ -30,9 +30,19 @@ use ruff_python_ast::source_code::Locator; /// handle_error(e) /// ``` /// +/// If you actually need to catch an unknown error, use `Exception` which will +/// catch regular program errors but not important system exceptions. +/// +/// ```python +/// def run_a_function(some_other_fn): +/// try: +/// some_other_fn() +/// except Exception as e: +/// print(f"How exceptional! {e}") +/// ``` +/// /// ## References -/// - [PEP 8](https://www.python.org/dev/peps/pep-0008/#programming-recommendations) -/// - [Python: "Exception hierarchy"](https://docs.python.org/3/library/exceptions.html#exception-hierarchy) +/// - [Python documentation: Exception hierarchy](https://docs.python.org/3/library/exceptions.html#exception-hierarchy) /// - [Google Python Style Guide: "Exceptions"](https://google.github.io/styleguide/pyguide.html#24-exceptions) #[violation] pub struct BareExcept; @@ -48,7 +58,7 @@ impl Violation for BareExcept { pub(crate) fn bare_except( type_: Option<&Expr>, body: &[Stmt], - handler: &Excepthandler, + handler: &ExceptHandler, locator: &Locator, ) -> Option { if type_.is_none() @@ -56,7 +66,7 @@ pub(crate) fn bare_except( .iter() .any(|stmt| matches!(stmt, Stmt::Raise(ast::StmtRaise { exc: None, .. }))) { - Some(Diagnostic::new(BareExcept, except_range(handler, locator))) + Some(Diagnostic::new(BareExcept, except(handler, locator))) } else { None } diff --git a/crates/ruff/src/rules/pycodestyle/rules/compound_statements.rs b/crates/ruff/src/rules/pycodestyle/rules/compound_statements.rs index 908e1c07f9..b3fd153481 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/compound_statements.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/compound_statements.rs @@ -5,6 +5,8 @@ use rustpython_parser::Tok; use ruff_diagnostics::{AlwaysAutofixableViolation, Violation}; use ruff_diagnostics::{Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers; +use ruff_python_ast::source_code::{Indexer, Locator}; use crate::registry::Rule; use crate::settings::Settings; @@ -13,7 +15,7 @@ use crate::settings::Settings; /// Checks for compound statements (multiple statements on the same line). /// /// ## Why is this bad? -/// Per PEP 8, "compound statements are generally discouraged". +/// According to [PEP 8], "compound statements are generally discouraged". /// /// ## Example /// ```python @@ -25,9 +27,8 @@ use crate::settings::Settings; /// if foo == "blah": /// do_blah_thing() /// ``` -/// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#other-recommendations) + +/// [PEP 8]: https://peps.python.org/pep-0008/#other-recommendations #[violation] pub struct MultipleStatementsOnOneLineColon; @@ -42,7 +43,7 @@ impl Violation for MultipleStatementsOnOneLineColon { /// Checks for multiline statements on one line. /// /// ## Why is this bad? -/// Per PEP 8, including multi-clause statements on the same line is +/// According to [PEP 8], including multi-clause statements on the same line is /// discouraged. /// /// ## Example @@ -57,8 +58,7 @@ impl Violation for MultipleStatementsOnOneLineColon { /// do_three() /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#other-recommendations) +/// [PEP 8]: https://peps.python.org/pep-0008/#other-recommendations #[violation] pub struct MultipleStatementsOnOneLineSemicolon; @@ -99,9 +99,13 @@ impl AlwaysAutofixableViolation for UselessSemicolon { } /// E701, E702, E703 -pub(crate) fn compound_statements(lxr: &[LexResult], settings: &Settings) -> Vec { - let mut diagnostics = vec![]; - +pub(crate) fn compound_statements( + diagnostics: &mut Vec, + lxr: &[LexResult], + locator: &Locator, + indexer: &Indexer, + settings: &Settings, +) { // Track the last seen instance of a variety of tokens. let mut colon = None; let mut semi = None; @@ -147,6 +151,12 @@ pub(crate) fn compound_statements(lxr: &[LexResult], settings: &Settings) -> Vec Tok::Rbrace => { brace_count = brace_count.saturating_sub(1); } + Tok::Ellipsis => { + if allow_ellipsis { + allow_ellipsis = false; + continue; + } + } _ => {} } @@ -160,8 +170,11 @@ pub(crate) fn compound_statements(lxr: &[LexResult], settings: &Settings) -> Vec let mut diagnostic = Diagnostic::new(UselessSemicolon, TextRange::new(start, end)); if settings.rules.should_fix(Rule::UselessSemicolon) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::deletion(start, end))); + diagnostic.set_fix(Fix::automatic(Edit::deletion( + helpers::preceded_by_continuations(start, locator, indexer) + .unwrap_or(start), + end, + ))); }; diagnostics.push(diagnostic); } @@ -197,17 +210,15 @@ pub(crate) fn compound_statements(lxr: &[LexResult], settings: &Settings) -> Vec || with.is_some() { colon = Some((range.start(), range.end())); - allow_ellipsis = true; + + // Allow `class C: ...`-style definitions in stubs. + allow_ellipsis = class.is_some(); } } Tok::Semi => { semi = Some((range.start(), range.end())); } Tok::Comment(..) | Tok::Indent | Tok::Dedent | Tok::NonLogicalNewline => {} - Tok::Ellipsis if allow_ellipsis => { - // Allow `class C: ...`-style definitions in stubs. - allow_ellipsis = false; - } _ => { if let Some((start, end)) = semi { diagnostics.push(Diagnostic::new( @@ -299,6 +310,4 @@ pub(crate) fn compound_statements(lxr: &[LexResult], settings: &Settings) -> Vec _ => {} }; } - - diagnostics } diff --git a/crates/ruff/src/rules/pycodestyle/rules/doc_line_too_long.rs b/crates/ruff/src/rules/pycodestyle/rules/doc_line_too_long.rs index 68890d0235..7c047ab120 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/doc_line_too_long.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/doc_line_too_long.rs @@ -10,7 +10,21 @@ use crate::settings::Settings; /// /// ## Why is this bad? /// For flowing long blocks of text (docstrings or comments), overlong lines -/// can hurt readability. +/// can hurt readability. [PEP 8], for example, recommends that such lines be +/// limited to 72 characters. +/// +/// In the context of this rule, a "doc line" is defined as a line consisting +/// of either a standalone comment or a standalone string, like a docstring. +/// +/// In the interest of pragmatism, this rule makes a few exceptions when +/// determining whether a line is overlong. Namely, it ignores lines that +/// consist of a single "word" (i.e., without any whitespace between its +/// characters), and lines that end with a URL (as long as the URL starts +/// before the line-length threshold). +/// +/// If `pycodestyle.ignore_overlong_task_comments` is `true`, this rule will +/// also ignore comments that start with any of the specified `task-tags` +/// (e.g., `# TODO:`). /// /// ## Example /// ```python @@ -26,6 +40,13 @@ use crate::settings::Settings; /// Duis auctor purus ut ex fermentum, at maximus est hendrerit. /// """ /// ``` +/// +/// +/// ## Options +/// - `task-tags` +/// - `pycodestyle.ignore-overlong-task-comments` +/// +/// [PEP 8]: https://peps.python.org/pep-0008/#maximum-line-length #[violation] pub struct DocLineTooLong(pub usize, pub usize); diff --git a/crates/ruff/src/rules/pycodestyle/rules/imports.rs b/crates/ruff/src/rules/pycodestyle/rules/imports.rs index b5b724329e..4504ade301 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/imports.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/imports.rs @@ -10,7 +10,7 @@ use crate::checkers::ast::Checker; /// Check for multiple imports on one line. /// /// ## Why is this bad? -/// Per PEP 8, "imports should usually be on separate lines." +/// According to [PEP 8], "imports should usually be on separate lines." /// /// ## Example /// ```python @@ -23,8 +23,7 @@ use crate::checkers::ast::Checker; /// import sys /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#imports) +/// [PEP 8]: https://peps.python.org/pep-0008/#imports #[violation] pub struct MultipleImportsOnOneLine; @@ -39,7 +38,7 @@ impl Violation for MultipleImportsOnOneLine { /// Checks for imports that are not at the top of the file. /// /// ## Why is this bad? -/// Per PEP 8, "imports are always put at the top of the file, just after any +/// According to [PEP 8], "imports are always put at the top of the file, just after any /// module comments and docstrings, and before module globals and constants." /// /// ## Example @@ -61,8 +60,7 @@ impl Violation for MultipleImportsOnOneLine { /// a = 1 /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#imports) +/// [PEP 8]: https://peps.python.org/pep-0008/#imports #[violation] pub struct ModuleImportNotAtTopOfFile; @@ -88,8 +86,7 @@ pub(crate) fn module_import_not_at_top_of_file( stmt: &Stmt, locator: &Locator, ) { - if checker.semantic_model().seen_import_boundary() && locator.is_at_start_of_line(stmt.start()) - { + if checker.semantic().seen_import_boundary() && locator.is_at_start_of_line(stmt.start()) { checker .diagnostics .push(Diagnostic::new(ModuleImportNotAtTopOfFile, stmt.range())); diff --git a/crates/ruff/src/rules/pycodestyle/rules/invalid_escape_sequence.rs b/crates/ruff/src/rules/pycodestyle/rules/invalid_escape_sequence.rs index ceef5cd796..fab07187d7 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/invalid_escape_sequence.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/invalid_escape_sequence.rs @@ -1,10 +1,9 @@ -use anyhow::{bail, Result}; -use log::error; use ruff_text_size::{TextLen, TextRange, TextSize}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::source_code::Locator; +use ruff_python_ast::str::{leading_quote, trailing_quote}; /// ## What it does /// Checks for invalid escape sequences. @@ -21,8 +20,11 @@ use ruff_python_ast::source_code::Locator; /// ```python /// regex = r"\.png$" /// ``` +/// +/// ## References +/// - [Python documentation: String and Bytes literals](https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals) #[violation] -pub struct InvalidEscapeSequence(pub char); +pub struct InvalidEscapeSequence(char); impl AlwaysAutofixableViolation for InvalidEscapeSequence { #[derive_message_formats] @@ -36,87 +38,125 @@ impl AlwaysAutofixableViolation for InvalidEscapeSequence { } } -// See: https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals -const VALID_ESCAPE_SEQUENCES: &[char; 23] = &[ - '\n', '\\', '\'', '"', 'a', 'b', 'f', 'n', 'r', 't', 'v', '0', '1', '2', '3', '4', '5', '6', - '7', 'x', // Escape sequences only recognized in string literals - 'N', 'u', 'U', -]; - -/// Return the quotation markers used for a String token. -fn extract_quote(text: &str) -> Result<&str> { - for quote in ["'''", "\"\"\"", "'", "\""] { - if text.ends_with(quote) { - return Ok(quote); - } - } - - bail!("Unable to find quotation mark for String token") -} - /// W605 pub(crate) fn invalid_escape_sequence( + diagnostics: &mut Vec, locator: &Locator, range: TextRange, autofix: bool, -) -> Vec { - let mut diagnostics = vec![]; - +) { let text = locator.slice(range); // Determine whether the string is single- or triple-quoted. - let Ok(quote) = extract_quote(text) else { - error!("Unable to find quotation mark for string token"); - return diagnostics; + let Some(leading_quote) = leading_quote(text) else { + return; }; - let quote_pos = text.find(quote).unwrap(); - let prefix = text[..quote_pos].to_lowercase(); - let body = &text[quote_pos + quote.len()..text.len() - quote.len()]; + let Some(trailing_quote) = trailing_quote(text) else { + return; + }; + let body = &text[leading_quote.len()..text.len() - trailing_quote.len()]; - if !prefix.contains('r') { - let start_offset = - range.start() + TextSize::try_from(quote_pos).unwrap() + quote.text_len(); + if leading_quote.contains(['r', 'R']) { + return; + } - let mut chars_iter = body.char_indices().peekable(); + let start_offset = range.start() + TextSize::try_from(leading_quote.len()).unwrap(); - while let Some((i, c)) = chars_iter.next() { - if c != '\\' { - continue; - } + let mut chars_iter = body.char_indices().peekable(); - // If the previous character was also a backslash, skip. - if i > 0 && body.as_bytes()[i - 1] == b'\\' { - continue; - } + let mut contains_valid_escape_sequence = false; - // If we're at the end of the file, skip. - let Some((_, next_char)) = chars_iter.peek() else { - continue; - }; + let mut invalid_escape_sequence = Vec::new(); + while let Some((i, c)) = chars_iter.next() { + if c != '\\' { + continue; + } - // If we're at the end of the line, skip - if matches!(next_char, '\n' | '\r') { - continue; - } + // If the previous character was also a backslash, skip. + if i > 0 && body.as_bytes()[i - 1] == b'\\' { + continue; + } - // If the next character is a valid escape sequence, skip. - if VALID_ESCAPE_SEQUENCES.contains(next_char) { - continue; - } + // If we're at the end of the file, skip. + let Some((_, next_char)) = chars_iter.peek() else { + continue; + }; - let location = start_offset + TextSize::try_from(i).unwrap(); - let range = TextRange::at(location, next_char.text_len() + TextSize::from(1)); - let mut diagnostic = Diagnostic::new(InvalidEscapeSequence(*next_char), range); - if autofix { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::insertion( + // If we're at the end of the line, skip + if matches!(next_char, '\n' | '\r') { + continue; + } + + // If the next character is a valid escape sequence, skip. + // See: https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals. + if matches!( + next_char, + '\n' + | '\\' + | '\'' + | '"' + | 'a' + | 'b' + | 'f' + | 'n' + | 'r' + | 't' + | 'v' + | '0' + | '1' + | '2' + | '3' + | '4' + | '5' + | '6' + | '7' + | 'x' + // Escape sequences only recognized in string literals + | 'N' + | 'u' + | 'U' + ) { + contains_valid_escape_sequence = true; + continue; + } + + let location = start_offset + TextSize::try_from(i).unwrap(); + let range = TextRange::at(location, next_char.text_len() + TextSize::from(1)); + invalid_escape_sequence.push(Diagnostic::new(InvalidEscapeSequence(*next_char), range)); + } + + if autofix { + if contains_valid_escape_sequence { + // Escape with backslash. + for diagnostic in &mut invalid_escape_sequence { + diagnostic.set_fix(Fix::automatic(Edit::insertion( r"\".to_string(), - range.start() + TextSize::from(1), + diagnostic.range().start() + TextSize::from(1), + ))); + } + } else { + // Turn into raw string. + for diagnostic in &mut invalid_escape_sequence { + // If necessary, add a space between any leading keyword (`return`, `yield`, + // `assert`, etc.) and the string. For example, `return"foo"` is valid, but + // `returnr"foo"` is not. + let requires_space = locator + .slice(TextRange::up_to(range.start())) + .chars() + .last() + .map_or(false, |char| char.is_ascii_alphabetic()); + + diagnostic.set_fix(Fix::automatic(Edit::insertion( + if requires_space { + " r".to_string() + } else { + "r".to_string() + }, + range.start(), ))); } - diagnostics.push(diagnostic); } } - diagnostics + diagnostics.extend(invalid_escape_sequence); } diff --git a/crates/ruff/src/rules/pycodestyle/rules/lambda_assignment.rs b/crates/ruff/src/rules/pycodestyle/rules/lambda_assignment.rs index bca493a8d8..8e3f4930e2 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/lambda_assignment.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/lambda_assignment.rs @@ -1,11 +1,13 @@ use ruff_text_size::TextRange; -use rustpython_parser::ast::{self, Arg, Arguments, Constant, Expr, Ranged, Stmt}; +use rustpython_parser::ast::{ + self, Arg, ArgWithDefault, Arguments, Constant, Expr, Identifier, Ranged, Stmt, +}; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::{has_leading_content, has_trailing_content}; use ruff_python_ast::source_code::Generator; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; use ruff_python_whitespace::{leading_indentation, UniversalNewlines}; use crate::checkers::ast::Checker; @@ -33,8 +35,7 @@ use crate::registry::AsRule; /// return 2 * x /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#programming-recommendations) +/// [PEP 8]: https://peps.python.org/pep-0008/#programming-recommendations #[violation] pub struct LambdaAssignment { name: String, @@ -62,65 +63,76 @@ pub(crate) fn lambda_assignment( annotation: Option<&Expr>, stmt: &Stmt, ) { - if let Expr::Name(ast::ExprName { id, .. }) = target { - if let Expr::Lambda(ast::ExprLambda { args, body, .. }) = value { - let mut diagnostic = Diagnostic::new( - LambdaAssignment { - name: id.to_string(), - }, - stmt.range(), - ); + let Expr::Name(ast::ExprName { id, .. }) = target else { + return; + }; - // If the assignment is in a class body, it might not be safe - // to replace it because the assignment might be - // carrying a type annotation that will be used by some - // package like dataclasses, which wouldn't consider the - // rewritten function definition to be equivalent. - // See https://github.com/astral-sh/ruff/issues/3046 - if checker.patch(diagnostic.kind.rule()) - && !checker.semantic_model().scope().kind.is_class() - && !has_leading_content(stmt, checker.locator) - && !has_trailing_content(stmt, checker.locator) + let Expr::Lambda(ast::ExprLambda { args, body, .. }) = value else { + return; + }; + + let mut diagnostic = Diagnostic::new( + LambdaAssignment { + name: id.to_string(), + }, + stmt.range(), + ); + + if checker.patch(diagnostic.kind.rule()) { + if !has_leading_content(stmt.start(), checker.locator) + && !has_trailing_content(stmt.end(), checker.locator) + { + let first_line = checker.locator.line(stmt.start()); + let indentation = leading_indentation(first_line); + let mut indented = String::new(); + for (idx, line) in function( + id, + args, + body, + annotation, + checker.semantic(), + checker.generator(), + ) + .universal_newlines() + .enumerate() { - let first_line = checker.locator.line(stmt.start()); - let indentation = leading_indentation(first_line); - let mut indented = String::new(); - for (idx, line) in function( - checker.semantic_model(), - id, - args, - body, - annotation, - checker.generator(), - ) - .universal_newlines() - .enumerate() - { - if idx == 0 { - indented.push_str(&line); - } else { - indented.push_str(checker.stylist.line_ending().as_str()); - indented.push_str(indentation); - indented.push_str(&line); - } + if idx == 0 { + indented.push_str(&line); + } else { + indented.push_str(checker.stylist.line_ending().as_str()); + indented.push_str(indentation); + indented.push_str(&line); } - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + } + + // If the assignment is in a class body, it might not be safe to replace it because the + // assignment might be carrying a type annotation that will be used by some package like + // dataclasses, which wouldn't consider the rewritten function definition to be + // equivalent. Similarly, if the lambda is shadowing a variable in the current scope, + // rewriting it as a function declaration may break type-checking. + // See: https://github.com/astral-sh/ruff/issues/3046 + // See: https://github.com/astral-sh/ruff/issues/5421 + if (annotation.is_some() && checker.semantic().scope().kind.is_class()) + || checker.semantic().scope().has(id) + { + diagnostic.set_fix(Fix::manual(Edit::range_replacement(indented, stmt.range()))); + } else { + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( indented, stmt.range(), ))); } - - checker.diagnostics.push(diagnostic); } } + + checker.diagnostics.push(diagnostic); } /// Extract the argument types and return type from a `Callable` annotation. /// The `Callable` import can be from either `collections.abc` or `typing`. /// If an ellipsis is used for the argument types, an empty list is returned. /// The returned values are cloned, so they can be used as-is. -fn extract_types(model: &SemanticModel, annotation: &Expr) -> Option<(Vec, Expr)> { +fn extract_types(annotation: &Expr, semantic: &SemanticModel) -> Option<(Vec, Expr)> { let Expr::Subscript(ast::ExprSubscript { value, slice, .. }) = &annotation else { return None; }; @@ -131,10 +143,13 @@ fn extract_types(model: &SemanticModel, annotation: &Expr) -> Option<(Vec, return None; } - if !model.resolve_call_path(value).map_or(false, |call_path| { - call_path.as_slice() == ["collections", "abc", "Callable"] - || model.match_typing_call_path(&call_path, "Callable") - }) { + if !semantic + .resolve_call_path(value) + .map_or(false, |call_path| { + matches!(call_path.as_slice(), ["collections", "abc", "Callable"]) + || semantic.match_typing_call_path(&call_path, "Callable") + }) + { return None; } @@ -156,11 +171,11 @@ fn extract_types(model: &SemanticModel, annotation: &Expr) -> Option<(Vec, } fn function( - model: &SemanticModel, name: &str, args: &Arguments, body: &Expr, annotation: Option<&Expr>, + semantic: &SemanticModel, generator: Generator, ) -> String { let body = Stmt::Return(ast::StmtReturn { @@ -168,33 +183,39 @@ fn function( range: TextRange::default(), }); if let Some(annotation) = annotation { - if let Some((arg_types, return_type)) = extract_types(model, annotation) { + if let Some((arg_types, return_type)) = extract_types(annotation, semantic) { // A `lambda` expression can only have positional and positional-only // arguments. The order is always positional-only first, then positional. let new_posonlyargs = args .posonlyargs .iter() .enumerate() - .map(|(idx, arg)| Arg { - annotation: arg_types - .get(idx) - .map(|arg_type| Box::new(arg_type.clone())), - ..arg.clone() + .map(|(idx, arg_with_default)| ArgWithDefault { + def: Arg { + annotation: arg_types + .get(idx) + .map(|arg_type| Box::new(arg_type.clone())), + ..arg_with_default.def.clone() + }, + ..arg_with_default.clone() }) .collect::>(); let new_args = args .args .iter() .enumerate() - .map(|(idx, arg)| Arg { - annotation: arg_types - .get(idx + new_posonlyargs.len()) - .map(|arg_type| Box::new(arg_type.clone())), - ..arg.clone() + .map(|(idx, arg_with_default)| ArgWithDefault { + def: Arg { + annotation: arg_types + .get(idx + new_posonlyargs.len()) + .map(|arg_type| Box::new(arg_type.clone())), + ..arg_with_default.def.clone() + }, + ..arg_with_default.clone() }) .collect::>(); let func = Stmt::FunctionDef(ast::StmtFunctionDef { - name: name.into(), + name: Identifier::new(name.to_string(), TextRange::default()), args: Box::new(Arguments { posonlyargs: new_posonlyargs, args: new_args, @@ -210,7 +231,7 @@ fn function( } } let func = Stmt::FunctionDef(ast::StmtFunctionDef { - name: name.into(), + name: Identifier::new(name.to_string(), TextRange::default()), args: Box::new(args.clone()), body: vec![body], decorator_list: vec![], diff --git a/crates/ruff/src/rules/pycodestyle/rules/line_too_long.rs b/crates/ruff/src/rules/pycodestyle/rules/line_too_long.rs index e59f61f9a0..780906c598 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/line_too_long.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/line_too_long.rs @@ -9,10 +9,18 @@ use crate::settings::Settings; /// Checks for lines that exceed the specified maximum character length. /// /// ## Why is this bad? -/// Overlong lines can hurt readability. +/// Overlong lines can hurt readability. [PEP 8], for example, recommends +/// limiting lines to 79 characters. /// -/// ## Options -/// - `task-tags` +/// In the interest of pragmatism, this rule makes a few exceptions when +/// determining whether a line is overlong. Namely, it ignores lines that +/// consist of a single "word" (i.e., without any whitespace between its +/// characters), and lines that end with a URL (as long as the URL starts +/// before the line-length threshold). +/// +/// If `pycodestyle.ignore_overlong_task_comments` is `true`, this rule will +/// also ignore comments that start with any of the specified `task-tags` +/// (e.g., `# TODO:`). /// /// ## Example /// ```python @@ -26,6 +34,12 @@ use crate::settings::Settings; /// param6, param7, param8, param9, param10 /// ) /// ``` +/// +/// ## Options +/// - `task-tags` +/// - `pycodestyle.ignore-overlong-task-comments` +/// +/// [PEP 8]: https://peps.python.org/pep-0008/#maximum-line-length #[violation] pub struct LineTooLong(pub usize, pub usize); diff --git a/crates/ruff/src/rules/pycodestyle/rules/literal_comparisons.rs b/crates/ruff/src/rules/pycodestyle/rules/literal_comparisons.rs index 8f4eaff3bc..872c515b0a 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/literal_comparisons.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/literal_comparisons.rs @@ -1,26 +1,27 @@ use itertools::izip; use rustc_hash::FxHashMap; -use rustpython_parser::ast::{self, Cmpop, Constant, Expr, Ranged}; +use rustpython_parser::ast::{self, CmpOp, Constant, Expr, Ranged}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers; +use ruff_python_ast::helpers::is_const_none; use crate::checkers::ast::Checker; use crate::registry::AsRule; use crate::rules::pycodestyle::helpers::compare; #[derive(Debug, PartialEq, Eq, Copy, Clone)] -enum EqCmpop { +enum EqCmpOp { Eq, NotEq, } -impl EqCmpop { - fn try_from(value: Cmpop) -> Option { +impl EqCmpOp { + fn try_from(value: CmpOp) -> Option { match value { - Cmpop::Eq => Some(EqCmpop::Eq), - Cmpop::NotEq => Some(EqCmpop::NotEq), + CmpOp::Eq => Some(EqCmpOp::Eq), + CmpOp::NotEq => Some(EqCmpOp::NotEq), _ => None, } } @@ -30,7 +31,7 @@ impl EqCmpop { /// Checks for comparisons to `None` which are not using the `is` operator. /// /// ## Why is this bad? -/// Per PEP 8, "Comparisons to singletons like None should always be done with +/// According to [PEP 8], "Comparisons to singletons like None should always be done with /// is or is not, never the equality operators." /// /// ## Example @@ -47,26 +48,25 @@ impl EqCmpop { /// pass /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#programming-recommendations) +/// [PEP 8]: https://peps.python.org/pep-0008/#programming-recommendations #[violation] -pub struct NoneComparison(EqCmpop); +pub struct NoneComparison(EqCmpOp); impl AlwaysAutofixableViolation for NoneComparison { #[derive_message_formats] fn message(&self) -> String { let NoneComparison(op) = self; match op { - EqCmpop::Eq => format!("Comparison to `None` should be `cond is None`"), - EqCmpop::NotEq => format!("Comparison to `None` should be `cond is not None`"), + EqCmpOp::Eq => format!("Comparison to `None` should be `cond is None`"), + EqCmpOp::NotEq => format!("Comparison to `None` should be `cond is not None`"), } } fn autofix_title(&self) -> String { let NoneComparison(op) = self; match op { - EqCmpop::Eq => "Replace with `cond is None`".to_string(), - EqCmpop::NotEq => "Replace with `cond is not None`".to_string(), + EqCmpOp::Eq => "Replace with `cond is None`".to_string(), + EqCmpOp::NotEq => "Replace with `cond is not None`".to_string(), } } } @@ -75,7 +75,7 @@ impl AlwaysAutofixableViolation for NoneComparison { /// Checks for comparisons to booleans which are not using the `is` operator. /// /// ## Why is this bad? -/// Per PEP 8, "Comparisons to singletons like None should always be done with +/// According to [PEP 8], "Comparisons to singletons like None should always be done with /// is or is not, never the equality operators." /// /// ## Example @@ -94,26 +94,25 @@ impl AlwaysAutofixableViolation for NoneComparison { /// pass /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#programming-recommendations) +/// [PEP 8]: https://peps.python.org/pep-0008/#programming-recommendations #[violation] -pub struct TrueFalseComparison(bool, EqCmpop); +pub struct TrueFalseComparison(bool, EqCmpOp); impl AlwaysAutofixableViolation for TrueFalseComparison { #[derive_message_formats] fn message(&self) -> String { let TrueFalseComparison(value, op) = self; match (value, op) { - (true, EqCmpop::Eq) => { + (true, EqCmpOp::Eq) => { format!("Comparison to `True` should be `cond is True` or `if cond:`") } - (true, EqCmpop::NotEq) => { + (true, EqCmpOp::NotEq) => { format!("Comparison to `True` should be `cond is not True` or `if not cond:`") } - (false, EqCmpop::Eq) => { + (false, EqCmpOp::Eq) => { format!("Comparison to `False` should be `cond is False` or `if not cond:`") } - (false, EqCmpop::NotEq) => { + (false, EqCmpOp::NotEq) => { format!("Comparison to `False` should be `cond is not False` or `if cond:`") } } @@ -122,10 +121,10 @@ impl AlwaysAutofixableViolation for TrueFalseComparison { fn autofix_title(&self) -> String { let TrueFalseComparison(value, op) = self; match (value, op) { - (true, EqCmpop::Eq) => "Replace with `cond is True`".to_string(), - (true, EqCmpop::NotEq) => "Replace with `cond is not True`".to_string(), - (false, EqCmpop::Eq) => "Replace with `cond is False`".to_string(), - (false, EqCmpop::NotEq) => "Replace with `cond is not False`".to_string(), + (true, EqCmpOp::Eq) => "Replace with `cond is True`".to_string(), + (true, EqCmpOp::NotEq) => "Replace with `cond is not True`".to_string(), + (false, EqCmpOp::Eq) => "Replace with `cond is False`".to_string(), + (false, EqCmpOp::NotEq) => "Replace with `cond is not False`".to_string(), } } } @@ -135,7 +134,7 @@ pub(crate) fn literal_comparisons( checker: &mut Checker, expr: &Expr, left: &Expr, - ops: &[Cmpop], + ops: &[CmpOp], comparators: &[Expr], check_none_comparisons: bool, check_true_false_comparisons: bool, @@ -144,7 +143,7 @@ pub(crate) fn literal_comparisons( // through the list of operators, we apply "dummy" fixes for each error, // then replace the entire expression at the end with one "real" fix, to // avoid conflicts. - let mut bad_ops: FxHashMap = FxHashMap::default(); + let mut bad_ops: FxHashMap = FxHashMap::default(); let mut diagnostics: Vec = vec![]; let op = ops.first().unwrap(); @@ -154,29 +153,20 @@ pub(crate) fn literal_comparisons( let next = &comparators[0]; if !helpers::is_constant_non_singleton(next) { - if let Some(op) = EqCmpop::try_from(*op) { - if check_none_comparisons - && matches!( - comparator, - Expr::Constant(ast::ExprConstant { - value: Constant::None, - kind: None, - range: _ - }) - ) - { + if let Some(op) = EqCmpOp::try_from(*op) { + if check_none_comparisons && is_const_none(comparator) { match op { - EqCmpop::Eq => { + EqCmpOp::Eq => { let diagnostic = Diagnostic::new(NoneComparison(op), comparator.range()); if checker.patch(diagnostic.kind.rule()) { - bad_ops.insert(0, Cmpop::Is); + bad_ops.insert(0, CmpOp::Is); } diagnostics.push(diagnostic); } - EqCmpop::NotEq => { + EqCmpOp::NotEq => { let diagnostic = Diagnostic::new(NoneComparison(op), comparator.range()); if checker.patch(diagnostic.kind.rule()) { - bad_ops.insert(0, Cmpop::IsNot); + bad_ops.insert(0, CmpOp::IsNot); } diagnostics.push(diagnostic); } @@ -191,23 +181,23 @@ pub(crate) fn literal_comparisons( }) = comparator { match op { - EqCmpop::Eq => { + EqCmpOp::Eq => { let diagnostic = Diagnostic::new( TrueFalseComparison(*value, op), comparator.range(), ); if checker.patch(diagnostic.kind.rule()) { - bad_ops.insert(0, Cmpop::Is); + bad_ops.insert(0, CmpOp::Is); } diagnostics.push(diagnostic); } - EqCmpop::NotEq => { + EqCmpOp::NotEq => { let diagnostic = Diagnostic::new( TrueFalseComparison(*value, op), comparator.range(), ); if checker.patch(diagnostic.kind.rule()) { - bad_ops.insert(0, Cmpop::IsNot); + bad_ops.insert(0, CmpOp::IsNot); } diagnostics.push(diagnostic); } @@ -224,29 +214,20 @@ pub(crate) fn literal_comparisons( continue; } - if let Some(op) = EqCmpop::try_from(*op) { - if check_none_comparisons - && matches!( - next, - Expr::Constant(ast::ExprConstant { - value: Constant::None, - kind: None, - range: _ - }) - ) - { + if let Some(op) = EqCmpOp::try_from(*op) { + if check_none_comparisons && is_const_none(next) { match op { - EqCmpop::Eq => { + EqCmpOp::Eq => { let diagnostic = Diagnostic::new(NoneComparison(op), next.range()); if checker.patch(diagnostic.kind.rule()) { - bad_ops.insert(idx, Cmpop::Is); + bad_ops.insert(idx, CmpOp::Is); } diagnostics.push(diagnostic); } - EqCmpop::NotEq => { + EqCmpOp::NotEq => { let diagnostic = Diagnostic::new(NoneComparison(op), next.range()); if checker.patch(diagnostic.kind.rule()) { - bad_ops.insert(idx, Cmpop::IsNot); + bad_ops.insert(idx, CmpOp::IsNot); } diagnostics.push(diagnostic); } @@ -261,19 +242,19 @@ pub(crate) fn literal_comparisons( }) = next { match op { - EqCmpop::Eq => { + EqCmpOp::Eq => { let diagnostic = Diagnostic::new(TrueFalseComparison(*value, op), next.range()); if checker.patch(diagnostic.kind.rule()) { - bad_ops.insert(idx, Cmpop::Is); + bad_ops.insert(idx, CmpOp::Is); } diagnostics.push(diagnostic); } - EqCmpop::NotEq => { + EqCmpOp::NotEq => { let diagnostic = Diagnostic::new(TrueFalseComparison(*value, op), next.range()); if checker.patch(diagnostic.kind.rule()) { - bad_ops.insert(idx, Cmpop::IsNot); + bad_ops.insert(idx, CmpOp::IsNot); } diagnostics.push(diagnostic); } @@ -297,8 +278,7 @@ pub(crate) fn literal_comparisons( .collect::>(); let content = compare(left, &ops, comparators, checker.generator()); for diagnostic in &mut diagnostics { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( content.to_string(), expr.range(), ))); diff --git a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/extraneous_whitespace.rs b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/extraneous_whitespace.rs index d8a3512cf7..ca4b1ed3d0 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/extraneous_whitespace.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/extraneous_whitespace.rs @@ -14,7 +14,7 @@ use super::{LogicalLine, Whitespace}; /// Checks for the use of extraneous whitespace after "(". /// /// ## Why is this bad? -/// PEP 8 recommends the omission of whitespace in the following cases: +/// [PEP 8] recommends the omission of whitespace in the following cases: /// - "Immediately inside parentheses, brackets or braces." /// - "Immediately before a comma, semicolon, or colon." /// @@ -30,8 +30,7 @@ use super::{LogicalLine, Whitespace}; /// spam(ham[1], {eggs: 2}) /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#pet-peeves) +/// [PEP 8]: https://peps.python.org/pep-0008/#pet-peeves #[violation] pub struct WhitespaceAfterOpenBracket { symbol: char, @@ -54,7 +53,7 @@ impl AlwaysAutofixableViolation for WhitespaceAfterOpenBracket { /// Checks for the use of extraneous whitespace before ")". /// /// ## Why is this bad? -/// PEP 8 recommends the omission of whitespace in the following cases: +/// [PEP 8] recommends the omission of whitespace in the following cases: /// - "Immediately inside parentheses, brackets or braces." /// - "Immediately before a comma, semicolon, or colon." /// @@ -70,8 +69,7 @@ impl AlwaysAutofixableViolation for WhitespaceAfterOpenBracket { /// spam(ham[1], {eggs: 2}) /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#pet-peeves) +/// [PEP 8]: https://peps.python.org/pep-0008/#pet-peeves #[violation] pub struct WhitespaceBeforeCloseBracket { symbol: char, @@ -94,7 +92,7 @@ impl AlwaysAutofixableViolation for WhitespaceBeforeCloseBracket { /// Checks for the use of extraneous whitespace before ",", ";" or ":". /// /// ## Why is this bad? -/// PEP 8 recommends the omission of whitespace in the following cases: +/// [PEP 8] recommends the omission of whitespace in the following cases: /// - "Immediately inside parentheses, brackets or braces." /// - "Immediately before a comma, semicolon, or colon." /// @@ -108,8 +106,7 @@ impl AlwaysAutofixableViolation for WhitespaceBeforeCloseBracket { /// if x == 4: print(x, y); x, y = y, x /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#pet-peeves) +/// [PEP 8]: https://peps.python.org/pep-0008/#pet-peeves #[violation] pub struct WhitespaceBeforePunctuation { symbol: char, diff --git a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/indentation.rs b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/indentation.rs index a059ee9b6c..6fa4cb316c 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/indentation.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/indentation.rs @@ -9,7 +9,7 @@ use super::LogicalLine; /// Checks for indentation with a non-multiple of 4 spaces. /// /// ## Why is this bad? -/// Per PEP 8, 4 spaces per indentation level should be preferred. +/// According to [PEP 8], 4 spaces per indentation level should be preferred. /// /// ## Example /// ```python @@ -23,8 +23,7 @@ use super::LogicalLine; /// a = 1 /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#indentation) +/// [PEP 8]: https://peps.python.org/pep-0008/#indentation #[violation] pub struct IndentationWithInvalidMultiple { indent_size: usize, @@ -42,7 +41,7 @@ impl Violation for IndentationWithInvalidMultiple { /// Checks for indentation of comments with a non-multiple of 4 spaces. /// /// ## Why is this bad? -/// Per PEP 8, 4 spaces per indentation level should be preferred. +/// According to [PEP 8], 4 spaces per indentation level should be preferred. /// /// ## Example /// ```python @@ -56,8 +55,7 @@ impl Violation for IndentationWithInvalidMultiple { /// # a = 1 /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#indentation) +/// [PEP 8]: https://peps.python.org/pep-0008/#indentation #[violation] pub struct IndentationWithInvalidMultipleComment { indent_size: usize, @@ -82,7 +80,6 @@ impl Violation for IndentationWithInvalidMultipleComment { /// ```python /// for item in items: /// pass -/// /// ``` /// /// Use instead: @@ -91,8 +88,7 @@ impl Violation for IndentationWithInvalidMultipleComment { /// pass /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#indentation) +/// [PEP 8]: https://peps.python.org/pep-0008/#indentation #[violation] pub struct NoIndentedBlock; @@ -124,8 +120,7 @@ impl Violation for NoIndentedBlock { /// pass /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#indentation) +/// [PEP 8]: https://peps.python.org/pep-0008/#indentation #[violation] pub struct NoIndentedBlockComment; @@ -154,8 +149,7 @@ impl Violation for NoIndentedBlockComment { /// b = 2 /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#indentation) +/// [PEP 8]: https://peps.python.org/pep-0008/#indentation #[violation] pub struct UnexpectedIndentation; @@ -184,8 +178,7 @@ impl Violation for UnexpectedIndentation { /// # b = 2 /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#indentation) +/// [PEP 8]: https://peps.python.org/pep-0008/#indentation #[violation] pub struct UnexpectedIndentationComment; @@ -200,7 +193,7 @@ impl Violation for UnexpectedIndentationComment { /// Checks for over-indented code. /// /// ## Why is this bad? -/// Per PEP 8, 4 spaces per indentation level should be preferred. Increased +/// According to [PEP 8], 4 spaces per indentation level should be preferred. Increased /// indentation can lead to inconsistent formatting, which can hurt /// readability. /// @@ -216,8 +209,7 @@ impl Violation for UnexpectedIndentationComment { /// pass /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#indentation) +/// [PEP 8]: https://peps.python.org/pep-0008/#indentation #[violation] pub struct OverIndented { is_comment: bool, diff --git a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/missing_whitespace.rs b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/missing_whitespace.rs index 385ea9efe0..1c50f85e35 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/missing_whitespace.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/missing_whitespace.rs @@ -92,8 +92,7 @@ pub(crate) fn missing_whitespace( let mut diagnostic = Diagnostic::new(kind, token.range()); if autofix { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::insertion( + diagnostic.set_fix(Fix::automatic(Edit::insertion( " ".to_string(), token.end(), ))); diff --git a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/mod.rs b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/mod.rs index ca87e1264a..9158cac000 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/mod.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/mod.rs @@ -1,50 +1,24 @@ -use bitflags::bitflags; -use ruff_text_size::{TextLen, TextRange, TextSize}; -use rustpython_parser::lexer::LexResult; +pub(crate) use extraneous_whitespace::*; +pub(crate) use indentation::*; +pub(crate) use missing_whitespace::*; +pub(crate) use missing_whitespace_after_keyword::*; +pub(crate) use missing_whitespace_around_operator::*; +pub(crate) use space_around_operator::*; +pub(crate) use whitespace_around_keywords::*; +pub(crate) use whitespace_around_named_parameter_equals::*; +pub(crate) use whitespace_before_comment::*; +pub(crate) use whitespace_before_parameters::*; + use std::fmt::{Debug, Formatter}; use std::iter::FusedIterator; +use bitflags::bitflags; +use ruff_text_size::{TextLen, TextRange, TextSize}; +use rustpython_parser::lexer::LexResult; + use ruff_python_ast::source_code::Locator; use ruff_python_ast::token_kind::TokenKind; - -pub(crate) use extraneous_whitespace::{ - extraneous_whitespace, WhitespaceAfterOpenBracket, WhitespaceBeforeCloseBracket, - WhitespaceBeforePunctuation, -}; -pub(crate) use indentation::{ - indentation, IndentationWithInvalidMultiple, IndentationWithInvalidMultipleComment, - NoIndentedBlock, NoIndentedBlockComment, OverIndented, UnexpectedIndentation, - UnexpectedIndentationComment, -}; -pub(crate) use missing_whitespace::{missing_whitespace, MissingWhitespace}; -pub(crate) use missing_whitespace_after_keyword::{ - missing_whitespace_after_keyword, MissingWhitespaceAfterKeyword, -}; -pub(crate) use missing_whitespace_around_operator::{ - missing_whitespace_around_operator, MissingWhitespaceAroundArithmeticOperator, - MissingWhitespaceAroundBitwiseOrShiftOperator, MissingWhitespaceAroundModuloOperator, - MissingWhitespaceAroundOperator, -}; use ruff_python_whitespace::is_python_whitespace; -pub(crate) use space_around_operator::{ - space_around_operator, MultipleSpacesAfterOperator, MultipleSpacesBeforeOperator, - TabAfterOperator, TabBeforeOperator, -}; -pub(crate) use whitespace_around_keywords::{ - whitespace_around_keywords, MultipleSpacesAfterKeyword, MultipleSpacesBeforeKeyword, - TabAfterKeyword, TabBeforeKeyword, -}; -pub(crate) use whitespace_around_named_parameter_equals::{ - whitespace_around_named_parameter_equals, MissingWhitespaceAroundParameterEquals, - UnexpectedSpacesAroundKeywordParameterEquals, -}; -pub(crate) use whitespace_before_comment::{ - whitespace_before_comment, MultipleLeadingHashesForBlockComment, NoSpaceAfterBlockComment, - NoSpaceAfterInlineComment, TooFewSpacesBeforeInlineComment, -}; -pub(crate) use whitespace_before_parameters::{ - whitespace_before_parameters, WhitespaceBeforeParameters, -}; mod extraneous_whitespace; mod indentation; @@ -121,8 +95,8 @@ impl Debug for LogicalLines<'_> { } impl<'a> IntoIterator for &'a LogicalLines<'a> { - type Item = LogicalLine<'a>; type IntoIter = LogicalLinesIter<'a>; + type Item = LogicalLine<'a>; fn into_iter(self) -> Self::IntoIter { LogicalLinesIter { diff --git a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/space_around_operator.rs b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/space_around_operator.rs index 5c111773de..d7ea88a803 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/space_around_operator.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/space_around_operator.rs @@ -12,7 +12,7 @@ use super::{LogicalLine, Whitespace}; /// Checks for extraneous tabs before an operator. /// /// ## Why is this bad? -/// Per PEP 8, operators should be surrounded by at most a single space on either +/// According to [PEP 8], operators should be surrounded by at most a single space on either /// side. /// /// ## Example @@ -25,8 +25,7 @@ use super::{LogicalLine, Whitespace}; /// a = 12 + 3 /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#whitespace-in-expressions-and-statements) +/// [PEP 8]: https://peps.python.org/pep-0008/#whitespace-in-expressions-and-statements #[violation] pub struct TabBeforeOperator; @@ -41,7 +40,7 @@ impl Violation for TabBeforeOperator { /// Checks for extraneous whitespace before an operator. /// /// ## Why is this bad? -/// Per PEP 8, operators should be surrounded by at most a single space on either +/// According to [PEP 8], operators should be surrounded by at most a single space on either /// side. /// /// ## Example @@ -54,8 +53,7 @@ impl Violation for TabBeforeOperator { /// a = 12 + 3 /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#whitespace-in-expressions-and-statements) +/// [PEP 8]: https://peps.python.org/pep-0008/#whitespace-in-expressions-and-statements #[violation] pub struct MultipleSpacesBeforeOperator; @@ -70,7 +68,7 @@ impl Violation for MultipleSpacesBeforeOperator { /// Checks for extraneous tabs after an operator. /// /// ## Why is this bad? -/// Per PEP 8, operators should be surrounded by at most a single space on either +/// According to [PEP 8], operators should be surrounded by at most a single space on either /// side. /// /// ## Example @@ -83,8 +81,7 @@ impl Violation for MultipleSpacesBeforeOperator { /// a = 12 + 3 /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#whitespace-in-expressions-and-statements) +/// [PEP 8]: https://peps.python.org/pep-0008/#whitespace-in-expressions-and-statements #[violation] pub struct TabAfterOperator; @@ -99,7 +96,7 @@ impl Violation for TabAfterOperator { /// Checks for extraneous whitespace after an operator. /// /// ## Why is this bad? -/// Per PEP 8, operators should be surrounded by at most a single space on either +/// According to [PEP 8], operators should be surrounded by at most a single space on either /// side. /// /// ## Example @@ -112,8 +109,7 @@ impl Violation for TabAfterOperator { /// a = 12 + 3 /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#whitespace-in-expressions-and-statements) +/// [PEP 8]: https://peps.python.org/pep-0008/#whitespace-in-expressions-and-statements #[violation] pub struct MultipleSpacesAfterOperator; diff --git a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_around_keywords.rs b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_around_keywords.rs index d65d1a1693..5c87d7df30 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_around_keywords.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_around_keywords.rs @@ -41,7 +41,6 @@ impl Violation for MultipleSpacesAfterKeyword { /// ## Example /// ```python /// True and False -/// /// ``` /// /// Use instead: @@ -67,7 +66,6 @@ impl Violation for MultipleSpacesBeforeKeyword { /// ## Example /// ```python /// True and\tFalse -/// /// ``` /// /// Use instead: @@ -93,7 +91,6 @@ impl Violation for TabAfterKeyword { /// ## Example /// ```python /// True\tand False -/// /// ``` /// /// Use instead: diff --git a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_before_comment.rs b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_before_comment.rs index e9ee52f52d..dd2e4e4271 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_before_comment.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_before_comment.rs @@ -59,8 +59,7 @@ impl Violation for TooFewSpacesBeforeInlineComment { /// x = x + 1 # Increment x /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#comments) +/// [PEP 8]: https://peps.python.org/pep-0008/#comments #[violation] pub struct NoSpaceAfterInlineComment; @@ -92,8 +91,7 @@ impl Violation for NoSpaceAfterInlineComment { /// # \xa0- Block comment list /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#comments) +/// [PEP 8]: https://peps.python.org/pep-0008/#comments #[violation] pub struct NoSpaceAfterBlockComment; @@ -116,7 +114,6 @@ impl Violation for NoSpaceAfterBlockComment { /// ## Example /// ```python /// ### Block comment -/// /// ``` /// /// Use instead: @@ -126,8 +123,7 @@ impl Violation for NoSpaceAfterBlockComment { /// # \xa0- Block comment list /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#comments) +/// [PEP 8]: https://peps.python.org/pep-0008/#comments #[violation] pub struct MultipleLeadingHashesForBlockComment; diff --git a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_before_parameters.rs b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_before_parameters.rs index a65220fa74..3cb6c0694d 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_before_parameters.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/logical_lines/whitespace_before_parameters.rs @@ -65,8 +65,7 @@ pub(crate) fn whitespace_before_parameters( let mut diagnostic = Diagnostic::new(kind, TextRange::new(start, end)); if autofix { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::deletion(start, end))); + diagnostic.set_fix(Fix::automatic(Edit::deletion(start, end))); } context.push_diagnostic(diagnostic); } diff --git a/crates/ruff/src/rules/pycodestyle/rules/missing_newline_at_end_of_file.rs b/crates/ruff/src/rules/pycodestyle/rules/missing_newline_at_end_of_file.rs index 58bb6d12c7..ea876524e4 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/missing_newline_at_end_of_file.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/missing_newline_at_end_of_file.rs @@ -55,8 +55,7 @@ pub(crate) fn no_newline_at_end_of_file( let mut diagnostic = Diagnostic::new(MissingNewlineAtEndOfFile, range); if autofix { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::insertion( + diagnostic.set_fix(Fix::automatic(Edit::insertion( stylist.line_ending().to_string(), range.start(), ))); diff --git a/crates/ruff/src/rules/pycodestyle/rules/mod.rs b/crates/ruff/src/rules/pycodestyle/rules/mod.rs index 2d43de0186..c9fceb87a6 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/mod.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/mod.rs @@ -1,33 +1,23 @@ -pub(crate) use ambiguous_class_name::{ambiguous_class_name, AmbiguousClassName}; -pub(crate) use ambiguous_function_name::{ambiguous_function_name, AmbiguousFunctionName}; -pub(crate) use ambiguous_variable_name::{ambiguous_variable_name, AmbiguousVariableName}; -pub(crate) use bare_except::{bare_except, BareExcept}; -pub(crate) use compound_statements::{ - compound_statements, MultipleStatementsOnOneLineColon, MultipleStatementsOnOneLineSemicolon, - UselessSemicolon, -}; -pub(crate) use doc_line_too_long::{doc_line_too_long, DocLineTooLong}; +pub(crate) use ambiguous_class_name::*; +pub(crate) use ambiguous_function_name::*; +pub(crate) use ambiguous_variable_name::*; +pub(crate) use bare_except::*; +pub(crate) use compound_statements::*; +pub(crate) use doc_line_too_long::*; pub use errors::IOError; -pub(crate) use errors::{syntax_error, SyntaxError}; -pub(crate) use imports::{ - module_import_not_at_top_of_file, multiple_imports_on_one_line, ModuleImportNotAtTopOfFile, - MultipleImportsOnOneLine, -}; +pub(crate) use errors::*; +pub(crate) use imports::*; -pub(crate) use invalid_escape_sequence::{invalid_escape_sequence, InvalidEscapeSequence}; -pub(crate) use lambda_assignment::{lambda_assignment, LambdaAssignment}; -pub(crate) use line_too_long::{line_too_long, LineTooLong}; -pub(crate) use literal_comparisons::{literal_comparisons, NoneComparison, TrueFalseComparison}; -pub(crate) use missing_newline_at_end_of_file::{ - no_newline_at_end_of_file, MissingNewlineAtEndOfFile, -}; -pub(crate) use mixed_spaces_and_tabs::{mixed_spaces_and_tabs, MixedSpacesAndTabs}; -pub(crate) use not_tests::{not_tests, NotInTest, NotIsTest}; -pub(crate) use tab_indentation::{tab_indentation, TabIndentation}; -pub(crate) use trailing_whitespace::{ - trailing_whitespace, BlankLineWithWhitespace, TrailingWhitespace, -}; -pub(crate) use type_comparison::{type_comparison, TypeComparison}; +pub(crate) use invalid_escape_sequence::*; +pub(crate) use lambda_assignment::*; +pub(crate) use line_too_long::*; +pub(crate) use literal_comparisons::*; +pub(crate) use missing_newline_at_end_of_file::*; +pub(crate) use mixed_spaces_and_tabs::*; +pub(crate) use not_tests::*; +pub(crate) use tab_indentation::*; +pub(crate) use trailing_whitespace::*; +pub(crate) use type_comparison::*; mod ambiguous_class_name; mod ambiguous_function_name; diff --git a/crates/ruff/src/rules/pycodestyle/rules/not_tests.rs b/crates/ruff/src/rules/pycodestyle/rules/not_tests.rs index 3c975789b8..60ef31a5c4 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/not_tests.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/not_tests.rs @@ -1,4 +1,4 @@ -use rustpython_parser::ast::{self, Cmpop, Expr, Ranged, Unaryop}; +use rustpython_parser::ast::{self, CmpOp, Expr, Ranged, UnaryOp}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; @@ -77,12 +77,12 @@ impl AlwaysAutofixableViolation for NotIsTest { pub(crate) fn not_tests( checker: &mut Checker, expr: &Expr, - op: Unaryop, + op: UnaryOp, operand: &Expr, check_not_in: bool, check_not_is: bool, ) { - if matches!(op, Unaryop::Not) { + if matches!(op, UnaryOp::Not) { if let Expr::Compare(ast::ExprCompare { left, ops, @@ -90,20 +90,19 @@ pub(crate) fn not_tests( range: _, }) = operand { - if !matches!(&ops[..], [Cmpop::In | Cmpop::Is]) { + if !matches!(&ops[..], [CmpOp::In | CmpOp::Is]) { return; } - for op in ops.iter() { + for op in ops { match op { - Cmpop::In => { + CmpOp::In => { if check_not_in { let mut diagnostic = Diagnostic::new(NotInTest, operand.range()); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( compare( left, - &[Cmpop::NotIn], + &[CmpOp::NotIn], comparators, checker.generator(), ), @@ -113,15 +112,14 @@ pub(crate) fn not_tests( checker.diagnostics.push(diagnostic); } } - Cmpop::Is => { + CmpOp::Is => { if check_not_is { let mut diagnostic = Diagnostic::new(NotIsTest, operand.range()); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( compare( left, - &[Cmpop::IsNot], + &[CmpOp::IsNot], comparators, checker.generator(), ), diff --git a/crates/ruff/src/rules/pycodestyle/rules/trailing_whitespace.rs b/crates/ruff/src/rules/pycodestyle/rules/trailing_whitespace.rs index 836eafe9e9..79452236c4 100644 --- a/crates/ruff/src/rules/pycodestyle/rules/trailing_whitespace.rs +++ b/crates/ruff/src/rules/pycodestyle/rules/trailing_whitespace.rs @@ -2,6 +2,8 @@ use ruff_text_size::{TextLen, TextRange, TextSize}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers; +use ruff_python_ast::source_code::{Indexer, Locator}; use ruff_python_whitespace::Line; use crate::registry::Rule; @@ -11,7 +13,7 @@ use crate::settings::Settings; /// Checks for superfluous trailing whitespace. /// /// ## Why is this bad? -/// Per PEP 8, "avoid trailing whitespace anywhere. Because it’s usually +/// According to [PEP 8], "avoid trailing whitespace anywhere. Because it’s usually /// invisible, it can be confusing" /// /// ## Example @@ -24,8 +26,7 @@ use crate::settings::Settings; /// spam(1)\n# /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#other-recommendations) +/// [PEP 8]: https://peps.python.org/pep-0008/#other-recommendations #[violation] pub struct TrailingWhitespace; @@ -44,13 +45,12 @@ impl AlwaysAutofixableViolation for TrailingWhitespace { /// Checks for superfluous whitespace in blank lines. /// /// ## Why is this bad? -/// Per PEP 8, "avoid trailing whitespace anywhere. Because it’s usually +/// According to [PEP 8], "avoid trailing whitespace anywhere. Because it’s usually /// invisible, it can be confusing" /// /// ## Example /// ```python /// class Foo(object):\n \n bang = 12 -/// /// ``` /// /// Use instead: @@ -58,8 +58,7 @@ impl AlwaysAutofixableViolation for TrailingWhitespace { /// class Foo(object):\n\n bang = 12 /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#other-recommendations) +/// [PEP 8]: https://peps.python.org/pep-0008/#other-recommendations #[violation] pub struct BlankLineWithWhitespace; @@ -75,7 +74,12 @@ impl AlwaysAutofixableViolation for BlankLineWithWhitespace { } /// W291, W293 -pub(crate) fn trailing_whitespace(line: &Line, settings: &Settings) -> Option { +pub(crate) fn trailing_whitespace( + line: &Line, + locator: &Locator, + indexer: &Indexer, + settings: &Settings, +) -> Option { let whitespace_len: TextSize = line .chars() .rev() @@ -89,16 +93,20 @@ pub(crate) fn trailing_whitespace(line: &Line, settings: &Settings) -> Option { if let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() { // Ex) `type(False)` - if id == "type" && checker.semantic_model().is_builtin("type") { + if id == "type" && checker.semantic().is_builtin("type") { if let Some(arg) = args.first() { // Allow comparison for types which are not obvious. if !matches!( @@ -76,12 +76,12 @@ pub(crate) fn type_comparison( if let Expr::Name(ast::ExprName { id, .. }) = value.as_ref() { // Ex) `types.NoneType` if id == "types" - && checker.semantic_model().resolve_call_path(value).map_or( - false, - |call_path| { + && checker + .semantic() + .resolve_call_path(value) + .map_or(false, |call_path| { call_path.first().map_or(false, |module| *module == "types") - }, - ) + }) { checker .diagnostics diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E211_E21.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E211_E21.py.snap index bd71617569..594efa073b 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E211_E21.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E211_E21.py.snap @@ -11,7 +11,7 @@ E21.py:2:5: E211 [*] Whitespace before '(' | = help: Removed whitespace before '(' -ℹ Suggested fix +ℹ Fix 1 1 | #: E211 2 |-spam (1) 2 |+spam(1) @@ -30,7 +30,7 @@ E21.py:4:5: E211 [*] Whitespace before '[' | = help: Removed whitespace before '[' -ℹ Suggested fix +ℹ Fix 1 1 | #: E211 2 2 | spam (1) 3 3 | #: E211 E211 @@ -51,7 +51,7 @@ E21.py:4:20: E211 [*] Whitespace before '[' | = help: Removed whitespace before '[' -ℹ Suggested fix +ℹ Fix 1 1 | #: E211 2 2 | spam (1) 3 3 | #: E211 E211 @@ -72,7 +72,7 @@ E21.py:6:12: E211 [*] Whitespace before '[' | = help: Removed whitespace before '[' -ℹ Suggested fix +ℹ Fix 3 3 | #: E211 E211 4 4 | dict ['key'] = list [index] 5 5 | #: E211 diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E231_E23.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E231_E23.py.snap index a985571437..34bd1a6274 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E231_E23.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E231_E23.py.snap @@ -11,7 +11,7 @@ E23.py:2:7: E231 [*] Missing whitespace after ',' | = help: Added missing whitespace after ',' -ℹ Suggested fix +ℹ Fix 1 1 | #: E231 2 |-a = (1,2) 2 |+a = (1, 2) @@ -30,7 +30,7 @@ E23.py:4:5: E231 [*] Missing whitespace after ',' | = help: Added missing whitespace after ',' -ℹ Suggested fix +ℹ Fix 1 1 | #: E231 2 2 | a = (1,2) 3 3 | #: E231 @@ -51,7 +51,7 @@ E23.py:6:10: E231 [*] Missing whitespace after ':' | = help: Added missing whitespace after ':' -ℹ Suggested fix +ℹ Fix 3 3 | #: E231 4 4 | a[b1,:] 5 5 | #: E231 @@ -71,7 +71,7 @@ E23.py:19:10: E231 [*] Missing whitespace after ',' | = help: Added missing whitespace after ',' -ℹ Suggested fix +ℹ Fix 16 16 | 17 17 | def foo() -> None: 18 18 | #: E231 @@ -91,7 +91,7 @@ E23.py:29:20: E231 [*] Missing whitespace after ':' | = help: Added missing whitespace after ':' -ℹ Suggested fix +ℹ Fix 26 26 | #: E231:2:20 27 27 | mdtypes_template = { 28 28 | 'tag_full': [('mdtype', 'u4'), ('byte_count', 'u4')], diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E702_E70.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E702_E70.py.snap index 5bca9b1e53..a0e09543c1 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E702_E70.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E702_E70.py.snap @@ -61,4 +61,14 @@ E70.py:56:13: E702 Multiple statements on one line (semicolon) 58 | match *0, 1, *2: | +E70.py:65:4: E702 Multiple statements on one line (semicolon) + | +63 | #: E702:2:4 +64 | while 1: +65 | 1;... + | ^ E702 +66 | #: E703:2:1 +67 | 0\ + | + diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E703_E70.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E703_E70.py.snap index 6e5b240773..a612503351 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E703_E70.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E703_E70.py.snap @@ -12,7 +12,7 @@ E70.py:10:13: E703 [*] Statement ends with an unnecessary semicolon | = help: Remove unnecessary semicolon -ℹ Suggested fix +ℹ Fix 7 7 | #: E702:1:17 8 8 | import bdist_egg; bdist_egg.write_safety_flag(cmd.egg_info, safe) 9 9 | #: E703:1:13 @@ -33,7 +33,7 @@ E70.py:12:23: E703 [*] Statement ends with an unnecessary semicolon | = help: Remove unnecessary semicolon -ℹ Suggested fix +ℹ Fix 9 9 | #: E703:1:13 10 10 | import shlex; 11 11 | #: E702:1:9 E703:1:23 @@ -54,7 +54,7 @@ E70.py:25:14: E703 [*] Statement ends with an unnecessary semicolon | = help: Remove unnecessary semicolon -ℹ Suggested fix +ℹ Fix 22 22 | while all is round: 23 23 | def f(x): return 2*x 24 24 | #: E704:1:8 E702:1:11 E703:1:14 @@ -64,4 +64,21 @@ E70.py:25:14: E703 [*] Statement ends with an unnecessary semicolon 27 27 | if True: lambda a: b 28 28 | #: E701:1:10 +E70.py:68:1: E703 [*] Statement ends with an unnecessary semicolon + | +66 | #: E703:2:1 +67 | 0\ +68 | ; + | ^ E703 + | + = help: Remove unnecessary semicolon + +ℹ Fix +64 64 | while 1: +65 65 | 1;... +66 66 | #: E703:2:1 +67 |-0\ +68 |-; + 67 |+0 + diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E713_E713.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E713_E713.py.snap index 5a4152e320..a77c7313fc 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E713_E713.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E713_E713.py.snap @@ -11,7 +11,7 @@ E713.py:2:8: E713 [*] Test for membership should be `not in` | = help: Convert to `not in` -ℹ Suggested fix +ℹ Fix 1 1 | #: E713 2 |-if not X in Y: 2 |+if X not in Y: @@ -30,7 +30,7 @@ E713.py:5:8: E713 [*] Test for membership should be `not in` | = help: Convert to `not in` -ℹ Suggested fix +ℹ Fix 2 2 | if not X in Y: 3 3 | pass 4 4 | #: E713 @@ -51,7 +51,7 @@ E713.py:8:8: E713 [*] Test for membership should be `not in` | = help: Convert to `not in` -ℹ Suggested fix +ℹ Fix 5 5 | if not X.B in Y: 6 6 | pass 7 7 | #: E713 @@ -72,7 +72,7 @@ E713.py:11:23: E713 [*] Test for membership should be `not in` | = help: Convert to `not in` -ℹ Suggested fix +ℹ Fix 8 8 | if not X in Y and Z == "zero": 9 9 | pass 10 10 | #: E713 @@ -92,7 +92,7 @@ E713.py:14:9: E713 [*] Test for membership should be `not in` | = help: Convert to `not in` -ℹ Suggested fix +ℹ Fix 11 11 | if X == "zero" or not Y in Z: 12 12 | pass 13 13 | #: E713 diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E714_E714.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E714_E714.py.snap index 444d001733..8286975b5e 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E714_E714.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E714_E714.py.snap @@ -11,7 +11,7 @@ E714.py:2:8: E714 [*] Test for object identity should be `is not` | = help: Convert to `is not` -ℹ Suggested fix +ℹ Fix 1 1 | #: E714 2 |-if not X is Y: 2 |+if X is not Y: @@ -29,7 +29,7 @@ E714.py:5:8: E714 [*] Test for object identity should be `is not` | = help: Convert to `is not` -ℹ Suggested fix +ℹ Fix 2 2 | if not X is Y: 3 3 | pass 4 4 | #: E714 diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E731_E731.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E731_E731.py.snap index 1657c1069e..440cc87ebd 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E731_E731.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E731_E731.py.snap @@ -1,284 +1,322 @@ --- source: crates/ruff/src/rules/pycodestyle/mod.rs --- -E731.py:2:1: E731 [*] Do not assign a `lambda` expression, use a `def` +E731.py:3:5: E731 [*] Do not assign a `lambda` expression, use a `def` | -1 | #: E731 -2 | f = lambda x: 2 * x - | ^^^^^^^^^^^^^^^^^^^ E731 -3 | #: E731 -4 | f = lambda x: 2 * x +1 | def scope(): +2 | # E731 +3 | f = lambda x: 2 * x + | ^^^^^^^^^^^^^^^^^^^ E731 | = help: Rewrite `f` as a `def` ℹ Suggested fix -1 1 | #: E731 -2 |-f = lambda x: 2 * x - 2 |+def f(x): - 3 |+ return 2 * x -3 4 | #: E731 -4 5 | f = lambda x: 2 * x -5 6 | #: E731 +1 1 | def scope(): +2 2 | # E731 +3 |- f = lambda x: 2 * x + 3 |+ def f(x): + 4 |+ return 2 * x +4 5 | +5 6 | +6 7 | def scope(): -E731.py:4:1: E731 [*] Do not assign a `lambda` expression, use a `def` +E731.py:8:5: E731 [*] Do not assign a `lambda` expression, use a `def` | -2 | f = lambda x: 2 * x -3 | #: E731 -4 | f = lambda x: 2 * x - | ^^^^^^^^^^^^^^^^^^^ E731 -5 | #: E731 -6 | while False: +6 | def scope(): +7 | # E731 +8 | f = lambda x: 2 * x + | ^^^^^^^^^^^^^^^^^^^ E731 | = help: Rewrite `f` as a `def` ℹ Suggested fix -1 1 | #: E731 -2 2 | f = lambda x: 2 * x -3 3 | #: E731 -4 |-f = lambda x: 2 * x - 4 |+def f(x): - 5 |+ return 2 * x -5 6 | #: E731 -6 7 | while False: -7 8 | this = lambda y, z: 2 * x +5 5 | +6 6 | def scope(): +7 7 | # E731 +8 |- f = lambda x: 2 * x + 8 |+ def f(x): + 9 |+ return 2 * x +9 10 | +10 11 | +11 12 | def scope(): -E731.py:7:5: E731 [*] Do not assign a `lambda` expression, use a `def` - | -5 | #: E731 -6 | while False: -7 | this = lambda y, z: 2 * x - | ^^^^^^^^^^^^^^^^^^^^^^^^^ E731 -8 | #: E731 -9 | f = lambda: (yield 1) - | - = help: Rewrite `this` as a `def` +E731.py:14:9: E731 [*] Do not assign a `lambda` expression, use a `def` + | +12 | # E731 +13 | while False: +14 | this = lambda y, z: 2 * x + | ^^^^^^^^^^^^^^^^^^^^^^^^^ E731 + | + = help: Rewrite `this` as a `def` ℹ Suggested fix -4 4 | f = lambda x: 2 * x -5 5 | #: E731 -6 6 | while False: -7 |- this = lambda y, z: 2 * x - 7 |+ def this(y, z): - 8 |+ return 2 * x -8 9 | #: E731 -9 10 | f = lambda: (yield 1) -10 11 | #: E731 +11 11 | def scope(): +12 12 | # E731 +13 13 | while False: +14 |- this = lambda y, z: 2 * x + 14 |+ def this(y, z): + 15 |+ return 2 * x +15 16 | +16 17 | +17 18 | def scope(): -E731.py:9:1: E731 [*] Do not assign a `lambda` expression, use a `def` +E731.py:19:5: E731 [*] Do not assign a `lambda` expression, use a `def` | - 7 | this = lambda y, z: 2 * x - 8 | #: E731 - 9 | f = lambda: (yield 1) - | ^^^^^^^^^^^^^^^^^^^^^ E731 -10 | #: E731 -11 | f = lambda: (yield from g()) +17 | def scope(): +18 | # E731 +19 | f = lambda: (yield 1) + | ^^^^^^^^^^^^^^^^^^^^^ E731 | = help: Rewrite `f` as a `def` ℹ Suggested fix -6 6 | while False: -7 7 | this = lambda y, z: 2 * x -8 8 | #: E731 -9 |-f = lambda: (yield 1) - 9 |+def f(): - 10 |+ return (yield 1) -10 11 | #: E731 -11 12 | f = lambda: (yield from g()) -12 13 | #: E731 +16 16 | +17 17 | def scope(): +18 18 | # E731 +19 |- f = lambda: (yield 1) + 19 |+ def f(): + 20 |+ return (yield 1) +20 21 | +21 22 | +22 23 | def scope(): -E731.py:11:1: E731 [*] Do not assign a `lambda` expression, use a `def` +E731.py:24:5: E731 [*] Do not assign a `lambda` expression, use a `def` | - 9 | f = lambda: (yield 1) -10 | #: E731 -11 | f = lambda: (yield from g()) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E731 -12 | #: E731 -13 | class F: +22 | def scope(): +23 | # E731 +24 | f = lambda: (yield from g()) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E731 | = help: Rewrite `f` as a `def` ℹ Suggested fix -8 8 | #: E731 -9 9 | f = lambda: (yield 1) -10 10 | #: E731 -11 |-f = lambda: (yield from g()) - 11 |+def f(): - 12 |+ return (yield from g()) -12 13 | #: E731 -13 14 | class F: -14 15 | f = lambda x: 2 * x +21 21 | +22 22 | def scope(): +23 23 | # E731 +24 |- f = lambda: (yield from g()) + 24 |+ def f(): + 25 |+ return (yield from g()) +25 26 | +26 27 | +27 28 | def scope(): -E731.py:14:5: E731 Do not assign a `lambda` expression, use a `def` +E731.py:57:5: E731 [*] Do not assign a `lambda` expression, use a `def` | -12 | #: E731 -13 | class F: -14 | f = lambda x: 2 * x +55 | class Scope: +56 | # E731 +57 | f = lambda x: 2 * x | ^^^^^^^^^^^^^^^^^^^ E731 | = help: Rewrite `f` as a `def` -E731.py:32:1: E731 [*] Do not assign a `lambda` expression, use a `def` +ℹ Suggested fix +54 54 | +55 55 | class Scope: +56 56 | # E731 +57 |- f = lambda x: 2 * x + 57 |+ def f(x): + 58 |+ return 2 * x +58 59 | +59 60 | +60 61 | class Scope: + +E731.py:64:5: E731 [*] Do not assign a `lambda` expression, use a `def` | -31 | # ParamSpec cannot be used in this context, so do not preserve the annotation. -32 | f: Callable[P, int] = lambda *args: len(args) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E731 -33 | f: Callable[[], None] = lambda: None -34 | f: Callable[..., None] = lambda a, b: None +63 | # E731 +64 | f: Callable[[int], int] = lambda x: 2 * x + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E731 + | + = help: Rewrite `f` as a `def` + +ℹ Possible fix +61 61 | from typing import Callable +62 62 | +63 63 | # E731 +64 |- f: Callable[[int], int] = lambda x: 2 * x + 64 |+ def f(x: int) -> int: + 65 |+ return 2 * x +65 66 | +66 67 | +67 68 | def scope(): + +E731.py:73:9: E731 [*] Do not assign a `lambda` expression, use a `def` + | +71 | x: Callable[[int], int] +72 | if True: +73 | x = lambda: 1 + | ^^^^^^^^^^^^^ E731 +74 | else: +75 | x = lambda: 2 + | + = help: Rewrite `x` as a `def` + +ℹ Possible fix +70 70 | +71 71 | x: Callable[[int], int] +72 72 | if True: +73 |- x = lambda: 1 + 73 |+ def x(): + 74 |+ return 1 +74 75 | else: +75 76 | x = lambda: 2 +76 77 | return x + +E731.py:75:9: E731 [*] Do not assign a `lambda` expression, use a `def` + | +73 | x = lambda: 1 +74 | else: +75 | x = lambda: 2 + | ^^^^^^^^^^^^^ E731 +76 | return x + | + = help: Rewrite `x` as a `def` + +ℹ Possible fix +72 72 | if True: +73 73 | x = lambda: 1 +74 74 | else: +75 |- x = lambda: 2 + 75 |+ def x(): + 76 |+ return 2 +76 77 | return x +77 78 | +78 79 | + +E731.py:86:5: E731 [*] Do not assign a `lambda` expression, use a `def` + | +84 | # ParamSpec cannot be used in this context, so do not preserve the annotation. +85 | P = ParamSpec("P") +86 | f: Callable[P, int] = lambda *args: len(args) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E731 | = help: Rewrite `f` as a `def` ℹ Suggested fix -29 29 | P = ParamSpec("P") -30 30 | -31 31 | # ParamSpec cannot be used in this context, so do not preserve the annotation. -32 |-f: Callable[P, int] = lambda *args: len(args) - 32 |+def f(*args): - 33 |+ return len(args) -33 34 | f: Callable[[], None] = lambda: None -34 35 | f: Callable[..., None] = lambda a, b: None -35 36 | f: Callable[[int], int] = lambda x: 2 * x +83 83 | +84 84 | # ParamSpec cannot be used in this context, so do not preserve the annotation. +85 85 | P = ParamSpec("P") +86 |- f: Callable[P, int] = lambda *args: len(args) + 86 |+ def f(*args): + 87 |+ return len(args) +87 88 | +88 89 | +89 90 | def scope(): -E731.py:33:1: E731 [*] Do not assign a `lambda` expression, use a `def` +E731.py:94:5: E731 [*] Do not assign a `lambda` expression, use a `def` | -31 | # ParamSpec cannot be used in this context, so do not preserve the annotation. -32 | f: Callable[P, int] = lambda *args: len(args) -33 | f: Callable[[], None] = lambda: None - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E731 -34 | f: Callable[..., None] = lambda a, b: None -35 | f: Callable[[int], int] = lambda x: 2 * x +92 | from typing import Callable +93 | +94 | f: Callable[[], None] = lambda: None + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E731 | = help: Rewrite `f` as a `def` ℹ Suggested fix -30 30 | -31 31 | # ParamSpec cannot be used in this context, so do not preserve the annotation. -32 32 | f: Callable[P, int] = lambda *args: len(args) -33 |-f: Callable[[], None] = lambda: None - 33 |+def f() -> None: - 34 |+ return None -34 35 | f: Callable[..., None] = lambda a, b: None -35 36 | f: Callable[[int], int] = lambda x: 2 * x -36 37 | +91 91 | +92 92 | from typing import Callable +93 93 | +94 |- f: Callable[[], None] = lambda: None + 94 |+ def f() -> None: + 95 |+ return None +95 96 | +96 97 | +97 98 | def scope(): -E731.py:34:1: E731 [*] Do not assign a `lambda` expression, use a `def` - | -32 | f: Callable[P, int] = lambda *args: len(args) -33 | f: Callable[[], None] = lambda: None -34 | f: Callable[..., None] = lambda a, b: None - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E731 -35 | f: Callable[[int], int] = lambda x: 2 * x - | - = help: Rewrite `f` as a `def` +E731.py:102:5: E731 [*] Do not assign a `lambda` expression, use a `def` + | +100 | from typing import Callable +101 | +102 | f: Callable[..., None] = lambda a, b: None + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E731 + | + = help: Rewrite `f` as a `def` ℹ Suggested fix -31 31 | # ParamSpec cannot be used in this context, so do not preserve the annotation. -32 32 | f: Callable[P, int] = lambda *args: len(args) -33 33 | f: Callable[[], None] = lambda: None -34 |-f: Callable[..., None] = lambda a, b: None - 34 |+def f(a, b) -> None: - 35 |+ return None -35 36 | f: Callable[[int], int] = lambda x: 2 * x -36 37 | -37 38 | # Let's use the `Callable` type from `collections.abc` instead. +99 99 | +100 100 | from typing import Callable +101 101 | +102 |- f: Callable[..., None] = lambda a, b: None + 102 |+ def f(a, b) -> None: + 103 |+ return None +103 104 | +104 105 | +105 106 | def scope(): -E731.py:35:1: E731 [*] Do not assign a `lambda` expression, use a `def` - | -33 | f: Callable[[], None] = lambda: None -34 | f: Callable[..., None] = lambda a, b: None -35 | f: Callable[[int], int] = lambda x: 2 * x - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E731 -36 | -37 | # Let's use the `Callable` type from `collections.abc` instead. - | - = help: Rewrite `f` as a `def` +E731.py:110:5: E731 [*] Do not assign a `lambda` expression, use a `def` + | +108 | from typing import Callable +109 | +110 | f: Callable[[int], int] = lambda x: 2 * x + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E731 + | + = help: Rewrite `f` as a `def` ℹ Suggested fix -32 32 | f: Callable[P, int] = lambda *args: len(args) -33 33 | f: Callable[[], None] = lambda: None -34 34 | f: Callable[..., None] = lambda a, b: None -35 |-f: Callable[[int], int] = lambda x: 2 * x - 35 |+def f(x: int) -> int: - 36 |+ return 2 * x -36 37 | -37 38 | # Let's use the `Callable` type from `collections.abc` instead. -38 39 | from collections.abc import Callable +107 107 | +108 108 | from typing import Callable +109 109 | +110 |- f: Callable[[int], int] = lambda x: 2 * x + 110 |+ def f(x: int) -> int: + 111 |+ return 2 * x +111 112 | +112 113 | +113 114 | # Let's use the `Callable` type from `collections.abc` instead. -E731.py:40:1: E731 [*] Do not assign a `lambda` expression, use a `def` - | -38 | from collections.abc import Callable -39 | -40 | f: Callable[[str, int], str] = lambda a, b: a * b - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E731 -41 | f: Callable[[str, int], tuple[str, int]] = lambda a, b: (a, b) -42 | f: Callable[[str, int, list[str]], list[str]] = lambda a, b, /, c: [*c, a * b] - | - = help: Rewrite `f` as a `def` +E731.py:119:5: E731 [*] Do not assign a `lambda` expression, use a `def` + | +117 | from collections.abc import Callable +118 | +119 | f: Callable[[str, int], str] = lambda a, b: a * b + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E731 + | + = help: Rewrite `f` as a `def` ℹ Suggested fix -37 37 | # Let's use the `Callable` type from `collections.abc` instead. -38 38 | from collections.abc import Callable -39 39 | -40 |-f: Callable[[str, int], str] = lambda a, b: a * b - 40 |+def f(a: str, b: int) -> str: - 41 |+ return a * b -41 42 | f: Callable[[str, int], tuple[str, int]] = lambda a, b: (a, b) -42 43 | f: Callable[[str, int, list[str]], list[str]] = lambda a, b, /, c: [*c, a * b] -43 44 | +116 116 | +117 117 | from collections.abc import Callable +118 118 | +119 |- f: Callable[[str, int], str] = lambda a, b: a * b + 119 |+ def f(a: str, b: int) -> str: + 120 |+ return a * b +120 121 | +121 122 | +122 123 | def scope(): -E731.py:41:1: E731 [*] Do not assign a `lambda` expression, use a `def` - | -40 | f: Callable[[str, int], str] = lambda a, b: a * b -41 | f: Callable[[str, int], tuple[str, int]] = lambda a, b: (a, b) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E731 -42 | f: Callable[[str, int, list[str]], list[str]] = lambda a, b, /, c: [*c, a * b] - | - = help: Rewrite `f` as a `def` +E731.py:127:5: E731 [*] Do not assign a `lambda` expression, use a `def` + | +125 | from collections.abc import Callable +126 | +127 | f: Callable[[str, int], tuple[str, int]] = lambda a, b: (a, b) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E731 + | + = help: Rewrite `f` as a `def` ℹ Suggested fix -38 38 | from collections.abc import Callable -39 39 | -40 40 | f: Callable[[str, int], str] = lambda a, b: a * b -41 |-f: Callable[[str, int], tuple[str, int]] = lambda a, b: (a, b) - 41 |+def f(a: str, b: int) -> tuple[str, int]: - 42 |+ return a, b -42 43 | f: Callable[[str, int, list[str]], list[str]] = lambda a, b, /, c: [*c, a * b] -43 44 | -44 45 | +124 124 | +125 125 | from collections.abc import Callable +126 126 | +127 |- f: Callable[[str, int], tuple[str, int]] = lambda a, b: (a, b) + 127 |+ def f(a: str, b: int) -> tuple[str, int]: + 128 |+ return a, b +128 129 | +129 130 | +130 131 | def scope(): -E731.py:42:1: E731 [*] Do not assign a `lambda` expression, use a `def` - | -40 | f: Callable[[str, int], str] = lambda a, b: a * b -41 | f: Callable[[str, int], tuple[str, int]] = lambda a, b: (a, b) -42 | f: Callable[[str, int, list[str]], list[str]] = lambda a, b, /, c: [*c, a * b] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E731 - | - = help: Rewrite `f` as a `def` +E731.py:135:5: E731 [*] Do not assign a `lambda` expression, use a `def` + | +133 | from collections.abc import Callable +134 | +135 | f: Callable[[str, int, list[str]], list[str]] = lambda a, b, /, c: [*c, a * b] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E731 + | + = help: Rewrite `f` as a `def` ℹ Suggested fix -39 39 | -40 40 | f: Callable[[str, int], str] = lambda a, b: a * b -41 41 | f: Callable[[str, int], tuple[str, int]] = lambda a, b: (a, b) -42 |-f: Callable[[str, int, list[str]], list[str]] = lambda a, b, /, c: [*c, a * b] - 42 |+def f(a: str, b: int, /, c: list[str]) -> list[str]: - 43 |+ return [*c, a * b] -43 44 | -44 45 | -45 46 | # Override `Callable` - -E731.py:51:1: E731 [*] Do not assign a `lambda` expression, use a `def` - | -50 | # Do not copy the annotation from here on out. -51 | f: Callable[[str, int], str] = lambda a, b: a * b - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E731 - | - = help: Rewrite `f` as a `def` - -ℹ Suggested fix -48 48 | -49 49 | -50 50 | # Do not copy the annotation from here on out. -51 |-f: Callable[[str, int], str] = lambda a, b: a * b - 51 |+def f(a, b): - 52 |+ return a * b +132 132 | +133 133 | from collections.abc import Callable +134 134 | +135 |- f: Callable[[str, int, list[str]], list[str]] = lambda a, b, /, c: [*c, a * b] + 135 |+ def f(a: str, b: int, /, c: list[str]) -> list[str]: + 136 |+ return [*c, a * b] diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W291_W29.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W291_W29.py.snap index 356d7ebaf7..e6e4ed42aa 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W291_W29.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W291_W29.py.snap @@ -12,7 +12,7 @@ W29.py:4:6: W291 [*] Trailing whitespace | = help: Remove trailing whitespace -ℹ Suggested fix +ℹ Fix 1 1 | #: Okay 2 2 | # 情 3 3 | #: W291:1:6 @@ -33,7 +33,7 @@ W29.py:11:35: W291 [*] Trailing whitespace | = help: Remove trailing whitespace -ℹ Suggested fix +ℹ Fix 8 8 | bang = 12 9 9 | #: W291:2:35 10 10 | '''multiline @@ -54,7 +54,7 @@ W29.py:13:6: W291 [*] Trailing whitespace | = help: Remove trailing whitespace -ℹ Suggested fix +ℹ Fix 10 10 | '''multiline 11 11 | string with trailing whitespace''' 12 12 | #: W291 W292 noeol diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W292_W292_0.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W292_W292_0.py.snap index 90f1c0015d..7b67a4f6a1 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W292_W292_0.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W292_W292_0.py.snap @@ -9,7 +9,7 @@ W292_0.py:2:9: W292 [*] No newline at end of file | = help: Add trailing newline -ℹ Suggested fix +ℹ Fix 1 1 | def fn() -> None: 2 |- pass 2 |+ pass diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W293_W29.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W293_W29.py.snap index 55fd59bee8..14f67ce27f 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W293_W29.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W293_W29.py.snap @@ -12,7 +12,7 @@ W29.py:7:1: W293 [*] Blank line contains whitespace | = help: Remove whitespace from blank line -ℹ Suggested fix +ℹ Fix 4 4 | print 5 5 | #: W293:2:1 6 6 | class Foo(object): diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W605_W605_0.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W605_W605_0.py.snap index 60999dc01a..7a4317fb36 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W605_W605_0.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W605_W605_0.py.snap @@ -11,10 +11,10 @@ W605_0.py:2:10: W605 [*] Invalid escape sequence: `\.` | = help: Add backslash to escape sequence -ℹ Suggested fix +ℹ Fix 1 1 | #: W605:1:10 2 |-regex = '\.png$' - 2 |+regex = '\\.png$' + 2 |+regex = r'\.png$' 3 3 | 4 4 | #: W605:2:1 5 5 | regex = ''' @@ -29,15 +29,15 @@ W605_0.py:6:1: W605 [*] Invalid escape sequence: `\.` | = help: Add backslash to escape sequence -ℹ Suggested fix +ℹ Fix +2 2 | regex = '\.png$' 3 3 | 4 4 | #: W605:2:1 -5 5 | regex = ''' -6 |-\.png$ - 6 |+\\.png$ +5 |-regex = ''' + 5 |+regex = r''' +6 6 | \.png$ 7 7 | ''' 8 8 | -9 9 | #: W605:2:6 W605_0.py:11:6: W605 [*] Invalid escape sequence: `\_` | @@ -49,12 +49,12 @@ W605_0.py:11:6: W605 [*] Invalid escape sequence: `\_` | = help: Add backslash to escape sequence -ℹ Suggested fix +ℹ Fix 8 8 | 9 9 | #: W605:2:6 10 10 | f( 11 |- '\_' - 11 |+ '\\_' + 11 |+ r'\_' 12 12 | ) 13 13 | 14 14 | #: W605:4:6 @@ -70,14 +70,53 @@ W605_0.py:18:6: W605 [*] Invalid escape sequence: `\_` | = help: Add backslash to escape sequence -ℹ Suggested fix -15 15 | """ +ℹ Fix +12 12 | ) +13 13 | +14 14 | #: W605:4:6 +15 |-""" + 15 |+r""" 16 16 | multi-line 17 17 | literal -18 |-with \_ somewhere - 18 |+with \\_ somewhere -19 19 | in the middle +18 18 | with \_ somewhere + +W605_0.py:23:39: W605 [*] Invalid escape sequence: `\_` + | +22 | #: W605:1:38 +23 | value = 'new line\nand invalid escape \_ here' + | ^^ W605 + | + = help: Add backslash to escape sequence + +ℹ Fix 20 20 | """ 21 21 | +22 22 | #: W605:1:38 +23 |-value = 'new line\nand invalid escape \_ here' + 23 |+value = 'new line\nand invalid escape \\_ here' +24 24 | +25 25 | +26 26 | def f(): + +W605_0.py:28:12: W605 [*] Invalid escape sequence: `\.` + | +26 | def f(): +27 | #: W605:1:11 +28 | return'\.png$' + | ^^ W605 +29 | +30 | #: Okay + | + = help: Add backslash to escape sequence + +ℹ Fix +25 25 | +26 26 | def f(): +27 27 | #: W605:1:11 +28 |- return'\.png$' + 28 |+ return r'\.png$' +29 29 | +30 30 | #: Okay +31 31 | regex = r'\.png$' diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W605_W605_1.py.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W605_W605_1.py.snap index e0e9a9371c..6b6f5939fb 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W605_W605_1.py.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__W605_W605_1.py.snap @@ -11,10 +11,10 @@ W605_1.py:2:10: W605 [*] Invalid escape sequence: `\.` | = help: Add backslash to escape sequence -ℹ Suggested fix +ℹ Fix 1 1 | #: W605:1:10 2 |-regex = '\.png$' - 2 |+regex = '\\.png$' + 2 |+regex = r'\.png$' 3 3 | 4 4 | #: W605:2:1 5 5 | regex = ''' @@ -29,15 +29,15 @@ W605_1.py:6:1: W605 [*] Invalid escape sequence: `\.` | = help: Add backslash to escape sequence -ℹ Suggested fix +ℹ Fix +2 2 | regex = '\.png$' 3 3 | 4 4 | #: W605:2:1 -5 5 | regex = ''' -6 |-\.png$ - 6 |+\\.png$ +5 |-regex = ''' + 5 |+regex = r''' +6 6 | \.png$ 7 7 | ''' 8 8 | -9 9 | #: W605:2:6 W605_1.py:11:6: W605 [*] Invalid escape sequence: `\_` | @@ -49,12 +49,12 @@ W605_1.py:11:6: W605 [*] Invalid escape sequence: `\_` | = help: Add backslash to escape sequence -ℹ Suggested fix +ℹ Fix 8 8 | 9 9 | #: W605:2:6 10 10 | f( 11 |- '\_' - 11 |+ '\\_' + 11 |+ r'\_' 12 12 | ) 13 13 | 14 14 | #: W605:4:6 @@ -70,14 +70,35 @@ W605_1.py:18:6: W605 [*] Invalid escape sequence: `\_` | = help: Add backslash to escape sequence -ℹ Suggested fix -15 15 | """ +ℹ Fix +12 12 | ) +13 13 | +14 14 | #: W605:4:6 +15 |-""" + 15 |+r""" 16 16 | multi-line 17 17 | literal -18 |-with \_ somewhere - 18 |+with \\_ somewhere -19 19 | in the middle -20 20 | """ -21 21 | +18 18 | with \_ somewhere + +W605_1.py:25:12: W605 [*] Invalid escape sequence: `\.` + | +23 | def f(): +24 | #: W605:1:11 +25 | return'\.png$' + | ^^ W605 +26 | +27 | #: Okay + | + = help: Add backslash to escape sequence + +ℹ Fix +22 22 | +23 23 | def f(): +24 24 | #: W605:1:11 +25 |- return'\.png$' + 25 |+ return r'\.png$' +26 26 | +27 27 | #: Okay +28 28 | regex = r'\.png$' diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__constant_literals.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__constant_literals.snap index c085685d06..b6ae2e3549 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__constant_literals.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__constant_literals.snap @@ -12,7 +12,7 @@ constant_literals.py:4:4: F632 [*] Use `==` to compare constant literals | = help: Replace `is` with `==` -ℹ Suggested fix +ℹ Fix 1 1 | ### 2 2 | # Errors 3 3 | ### @@ -33,7 +33,7 @@ constant_literals.py:6:4: F632 [*] Use `==` to compare constant literals | = help: Replace `is` with `==` -ℹ Suggested fix +ℹ Fix 3 3 | ### 4 4 | if "abc" is "def": # F632 (fix) 5 5 | pass @@ -54,7 +54,7 @@ constant_literals.py:8:4: F632 [*] Use `==` to compare constant literals | = help: Replace `is` with `==` -ℹ Suggested fix +ℹ Fix 5 5 | pass 6 6 | if "abc" is None: # F632 (fix, but leaves behind unfixable E711) 7 7 | pass @@ -75,7 +75,7 @@ constant_literals.py:10:4: F632 [*] Use `==` to compare constant literals | = help: Replace `is` with `==` -ℹ Suggested fix +ℹ Fix 7 7 | pass 8 8 | if None is "abc": # F632 (fix, but leaves behind unfixable E711) 9 9 | pass @@ -96,7 +96,7 @@ constant_literals.py:12:4: F632 [*] Use `==` to compare constant literals | = help: Replace `is` with `==` -ℹ Suggested fix +ℹ Fix 9 9 | pass 10 10 | if "abc" is False: # F632 (fix, but leaves behind unfixable E712) 11 11 | pass diff --git a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__w292_4.snap b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__w292_4.snap index 2b15945ea1..68d23e984d 100644 --- a/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__w292_4.snap +++ b/crates/ruff/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__w292_4.snap @@ -8,7 +8,7 @@ W292_4.py:1:2: W292 [*] No newline at end of file | = help: Add trailing newline -ℹ Suggested fix +ℹ Fix 1 |- 1 |+ diff --git a/crates/ruff/src/rules/pydocstyle/helpers.rs b/crates/ruff/src/rules/pydocstyle/helpers.rs index 774fd1ff57..f0632f86f8 100644 --- a/crates/ruff/src/rules/pydocstyle/helpers.rs +++ b/crates/ruff/src/rules/pydocstyle/helpers.rs @@ -4,8 +4,7 @@ use ruff_python_ast::call_path::from_qualified_name; use ruff_python_ast::cast; use ruff_python_ast::helpers::map_callable; use ruff_python_ast::str::is_implicit_concatenation; -use ruff_python_semantic::definition::{Definition, Member, MemberKind}; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::{Definition, Member, MemberKind, SemanticModel}; use ruff_python_whitespace::UniversalNewlines; /// Return the index of the first logical line in a string. @@ -41,9 +40,9 @@ pub(super) fn ends_with_backslash(line: &str) -> bool { /// Check decorator list to see if function should be ignored. pub(crate) fn should_ignore_definition( - model: &SemanticModel, definition: &Definition, ignore_decorators: &BTreeSet, + semantic: &SemanticModel, ) -> bool { if ignore_decorators.is_empty() { return false; @@ -56,7 +55,8 @@ pub(crate) fn should_ignore_definition( }) = definition { for decorator in cast::decorator_list(stmt) { - if let Some(call_path) = model.resolve_call_path(map_callable(&decorator.expression)) { + if let Some(call_path) = semantic.resolve_call_path(map_callable(&decorator.expression)) + { if ignore_decorators .iter() .any(|decorator| from_qualified_name(decorator) == call_path) diff --git a/crates/ruff/src/rules/pydocstyle/mod.rs b/crates/ruff/src/rules/pydocstyle/mod.rs index 9dc0e45fa5..41c6a1fc6f 100644 --- a/crates/ruff/src/rules/pydocstyle/mod.rs +++ b/crates/ruff/src/rules/pydocstyle/mod.rs @@ -51,6 +51,7 @@ mod tests { #[test_case(Rule::EmptyDocstring, Path::new("D.py"))] #[test_case(Rule::EmptyDocstringSection, Path::new("sections.py"))] #[test_case(Rule::NonImperativeMood, Path::new("D401.py"))] + #[test_case(Rule::NoBlankLineAfterSection, Path::new("D410.py"))] #[test_case(Rule::OneBlankLineAfterClass, Path::new("D.py"))] #[test_case(Rule::OneBlankLineBeforeClass, Path::new("D.py"))] #[test_case(Rule::UndocumentedPublicClass, Path::new("D.py"))] @@ -82,6 +83,7 @@ mod tests { #[test_case(Rule::SectionUnderlineNotOverIndented, Path::new("sections.py"))] #[test_case(Rule::OverloadWithDocstring, Path::new("D.py"))] #[test_case(Rule::EscapeSequenceInDocstring, Path::new("D.py"))] + #[test_case(Rule::EscapeSequenceInDocstring, Path::new("D301.py"))] #[test_case(Rule::TripleSingleQuotes, Path::new("D.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); diff --git a/crates/ruff/src/rules/pydocstyle/rules/backslashes.rs b/crates/ruff/src/rules/pydocstyle/rules/backslashes.rs index 7bd2b326fd..88cbdf2b58 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/backslashes.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/backslashes.rs @@ -1,5 +1,4 @@ -use once_cell::sync::Lazy; -use regex::Regex; +use memchr::memchr_iter; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -7,6 +6,42 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; use crate::docstrings::Docstring; +/// ## What it does +/// Checks for docstrings that include backslashes, but are not defined as +/// raw string literals. +/// +/// ## Why is this bad? +/// In Python, backslashes are typically used to escape characters in strings. +/// In raw strings (those prefixed with an `r`), however, backslashes are +/// treated as literal characters. +/// +/// [PEP 257](https://peps.python.org/pep-0257/#what-is-a-docstring) recommends +/// the use of raw strings (i.e., `r"""raw triple double quotes"""`) for +/// docstrings that include backslashes. The use of a raw string ensures that +/// any backslashes are treated as literal characters, and not as escape +/// sequences, which avoids confusion. +/// +/// ## Example +/// ```python +/// def foobar(): +/// """Docstring for foo\bar.""" +/// +/// +/// foobar.__doc__ # "Docstring for foar." +/// ``` +/// +/// Use instead: +/// ```python +/// def foobar(): +/// r"""Docstring for foo\bar.""" +/// +/// +/// foobar.__doc__ # "Docstring for foo\bar." +/// ``` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [Python documentation: String and Bytes literals](https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals) #[violation] pub struct EscapeSequenceInDocstring; @@ -17,18 +52,22 @@ impl Violation for EscapeSequenceInDocstring { } } -static BACKSLASH_REGEX: Lazy = Lazy::new(|| Regex::new(r"\\[^(\r\n|\n)uN]").unwrap()); - /// D301 pub(crate) fn backslashes(checker: &mut Checker, docstring: &Docstring) { - let contents = docstring.contents; - // Docstring is already raw. + let contents = docstring.contents; if contents.starts_with('r') || contents.starts_with("ur") { return; } - if BACKSLASH_REGEX.is_match(contents) { + // Docstring contains at least one backslash. + let body = docstring.body(); + let bytes = body.as_bytes(); + if memchr_iter(b'\\', bytes).any(|position| { + let escaped_char = bytes.get(position.saturating_add(1)); + // Allow continuations (backslashes followed by newlines) and Unicode escapes. + !matches!(escaped_char, Some(b'\r' | b'\n' | b'u' | b'N')) + }) { checker.diagnostics.push(Diagnostic::new( EscapeSequenceInDocstring, docstring.range(), diff --git a/crates/ruff/src/rules/pydocstyle/rules/blank_after_summary.rs b/crates/ruff/src/rules/pydocstyle/rules/blank_after_summary.rs index 423b77f78d..8e939060dc 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/blank_after_summary.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/blank_after_summary.rs @@ -6,6 +6,40 @@ use crate::checkers::ast::Checker; use crate::docstrings::Docstring; use crate::registry::AsRule; +/// ## What it does +/// Checks for docstring summary lines that are not separated from the docstring +/// description by one blank line. +/// +/// ## Why is this bad? +/// [PEP 257] recommends that multi-line docstrings consist of "a summary line +/// just like a one-line docstring, followed by a blank line, followed by a +/// more elaborate description." +/// +/// ## Example +/// ```python +/// def sort_list(l: list[int]) -> list[int]: +/// """Return a sorted copy of the list. +/// Sort the list in ascending order and return a copy of the +/// result using the bubble sort algorithm. +/// """ +/// ``` +/// +/// Use instead: +/// ```python +/// def sort_list(l: list[int]) -> list[int]: +/// """Return a sorted copy of the list. +/// +/// Sort the list in ascending order and return a copy of the +/// result using the bubble sort algorithm. +/// """ +/// ``` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) +/// +/// [PEP 257]: https://peps.python.org/pep-0257/ #[violation] pub struct BlankLineAfterSummary { num_lines: usize, @@ -75,8 +109,7 @@ pub(crate) fn blank_after_summary(checker: &mut Checker, docstring: &Docstring) } // Insert one blank line after the summary (replacing any existing lines). - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::replacement( + diagnostic.set_fix(Fix::automatic(Edit::replacement( checker.stylist.line_ending().to_string(), summary_end, blank_end, diff --git a/crates/ruff/src/rules/pydocstyle/rules/blank_before_after_class.rs b/crates/ruff/src/rules/pydocstyle/rules/blank_before_after_class.rs index 17d106d550..d13f094e24 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/blank_before_after_class.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/blank_before_after_class.rs @@ -3,13 +3,44 @@ use rustpython_parser::ast::Ranged; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::definition::{Definition, Member, MemberKind}; +use ruff_python_semantic::{Definition, Member, MemberKind}; use ruff_python_whitespace::{PythonWhitespace, UniversalNewlineIterator, UniversalNewlines}; use crate::checkers::ast::Checker; use crate::docstrings::Docstring; use crate::registry::{AsRule, Rule}; +/// ## What it does +/// Checks for docstrings on class definitions that are not preceded by a +/// blank line. +/// +/// ## Why is this bad? +/// Use a blank line to separate the docstring from the class definition, for +/// consistency. +/// +/// This rule may not apply to all projects; its applicability is a matter of +/// convention. By default, this rule is disabled when using the `google`, +/// `numpy`, and `pep257` conventions. +/// +/// For an alternative, see [D211]. +/// +/// ## Example +/// ```python +/// class PhotoMetadata: +/// """Metadata about a photo.""" +/// ``` +/// +/// Use instead: +/// ```python +/// class PhotoMetadata: +/// +/// """Metadata about a photo.""" +/// ``` +/// +/// ## Options +/// - `pydocstyle.convention` +/// +/// [D211]: https://beta.ruff.rs/docs/rules/blank-line-before-class #[violation] pub struct OneBlankLineBeforeClass { lines: usize, @@ -26,6 +57,44 @@ impl AlwaysAutofixableViolation for OneBlankLineBeforeClass { } } +/// ## What it does +/// Checks for class methods that are not separated from the class's docstring +/// by a blank line. +/// +/// ## Why is this bad? +/// [PEP 257] recommends the use of a blank line to separate a class's +/// docstring its methods. +/// +/// This rule may not apply to all projects; its applicability is a matter of +/// convention. By default, this rule is enabled when using the `google` +/// convention, and disabled when using the `numpy` and `pep257` conventions. +/// +/// ## Example +/// ```python +/// class PhotoMetadata: +/// """Metadata about a photo.""" +/// def __init__(self, file: Path): +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// class PhotoMetadata: +/// """Metadata about a photo.""" +/// +/// def __init__(self, file: Path): +/// ... +/// ``` +/// +/// ## Options +/// - `pydocstyle.convention` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) +/// +/// [PEP 257]: https://peps.python.org/pep-0257/ #[violation] pub struct OneBlankLineAfterClass { lines: usize, @@ -42,6 +111,37 @@ impl AlwaysAutofixableViolation for OneBlankLineAfterClass { } } +/// ## What it does +/// Checks for docstrings on class definitions that are preceded by a blank +/// line. +/// +/// ## Why is this bad? +/// Avoid introducing any blank lines between a class definition and its +/// docstring, for consistency. +/// +/// This rule may not apply to all projects; its applicability is a matter of +/// convention. By default, this rule is enabled when using the `google`, +/// `numpy`, and `pep257` conventions. +/// +/// For an alternative, see [D203]. +/// +/// ## Example +/// ```python +/// class PhotoMetadata: +/// """Metadata about a photo.""" +/// ``` +/// +/// Use instead: +/// ```python +/// class PhotoMetadata: +/// +/// """Metadata about a photo.""" +/// ``` +/// +/// ## Options +/// - `pydocstyle.convention` +/// +/// [D203]: https://beta.ruff.rs/docs/rules/one-blank-line-before-class #[violation] pub struct BlankLineBeforeClass { lines: usize, @@ -61,10 +161,11 @@ impl AlwaysAutofixableViolation for BlankLineBeforeClass { /// D203, D204, D211 pub(crate) fn blank_before_after_class(checker: &mut Checker, docstring: &Docstring) { let Definition::Member(Member { - kind: MemberKind::Class | MemberKind::NestedClass , + kind: MemberKind::Class | MemberKind::NestedClass, stmt, .. - }) = docstring.definition else { + }) = docstring.definition + else { return; }; @@ -97,8 +198,7 @@ pub(crate) fn blank_before_after_class(checker: &mut Checker, docstring: &Docstr ); if checker.patch(diagnostic.kind.rule()) { // Delete the blank line before the class. - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::deletion( + diagnostic.set_fix(Fix::automatic(Edit::deletion( blank_lines_start, docstring.start() - docstring.indentation.text_len(), ))); @@ -116,8 +216,7 @@ pub(crate) fn blank_before_after_class(checker: &mut Checker, docstring: &Docstr ); if checker.patch(diagnostic.kind.rule()) { // Insert one blank line before the class. - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::replacement( + diagnostic.set_fix(Fix::automatic(Edit::replacement( checker.stylist.line_ending().to_string(), blank_lines_start, docstring.start() - docstring.indentation.text_len(), @@ -163,8 +262,7 @@ pub(crate) fn blank_before_after_class(checker: &mut Checker, docstring: &Docstr ); if checker.patch(diagnostic.kind.rule()) { // Insert a blank line before the class (replacing any existing lines). - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::replacement( + diagnostic.set_fix(Fix::automatic(Edit::replacement( checker.stylist.line_ending().to_string(), first_line_start, blank_lines_end, diff --git a/crates/ruff/src/rules/pydocstyle/rules/blank_before_after_function.rs b/crates/ruff/src/rules/pydocstyle/rules/blank_before_after_function.rs index d8c52c5b66..ccdeed72b7 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/blank_before_after_function.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/blank_before_after_function.rs @@ -5,13 +5,38 @@ use rustpython_parser::ast::Ranged; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::definition::{Definition, Member, MemberKind}; +use ruff_python_semantic::{Definition, Member, MemberKind}; use ruff_python_whitespace::{PythonWhitespace, UniversalNewlineIterator, UniversalNewlines}; use crate::checkers::ast::Checker; use crate::docstrings::Docstring; use crate::registry::{AsRule, Rule}; +/// ## What it does +/// Checks for docstrings on functions that are separated by one or more blank +/// lines from the function definition. +/// +/// ## Why is this bad? +/// Remove any blank lines between the function definition and its docstring, +/// for consistency. +/// +/// ## Example +/// ```python +/// def average(values: list[float]) -> float: +/// +/// """Return the mean of the given values.""" +/// ``` +/// +/// Use instead: +/// ```python +/// def average(values: list[float]) -> float: +/// """Return the mean of the given values.""" +/// ``` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[violation] pub struct NoBlankLineBeforeFunction { num_lines: usize, @@ -29,6 +54,33 @@ impl AlwaysAutofixableViolation for NoBlankLineBeforeFunction { } } +/// ## What it does +/// Checks for docstrings on functions that are separated by one or more blank +/// lines from the function body. +/// +/// ## Why is this bad? +/// Remove any blank lines between the function body and the function +/// docstring, for consistency. +/// +/// ## Example +/// ```python +/// def average(values: list[float]) -> float: +/// """Return the mean of the given values.""" +/// +/// return sum(values) / len(values) +/// ``` +/// +/// Use instead: +/// ```python +/// def average(values: list[float]) -> float: +/// """Return the mean of the given values.""" +/// return sum(values) / len(values) +/// ``` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[violation] pub struct NoBlankLineAfterFunction { num_lines: usize, @@ -55,7 +107,8 @@ pub(crate) fn blank_before_after_function(checker: &mut Checker, docstring: &Doc kind: MemberKind::Function | MemberKind::NestedFunction | MemberKind::Method, stmt, .. - }) = docstring.definition else { + }) = docstring.definition + else { return; }; @@ -86,8 +139,7 @@ pub(crate) fn blank_before_after_function(checker: &mut Checker, docstring: &Doc ); if checker.patch(diagnostic.kind.rule()) { // Delete the blank line before the docstring. - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::deletion( + diagnostic.set_fix(Fix::automatic(Edit::deletion( blank_lines_start, docstring.start() - docstring.indentation.text_len(), ))); @@ -143,8 +195,7 @@ pub(crate) fn blank_before_after_function(checker: &mut Checker, docstring: &Doc ); if checker.patch(diagnostic.kind.rule()) { // Delete the blank line after the docstring. - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::deletion( + diagnostic.set_fix(Fix::automatic(Edit::deletion( first_line_end, blank_lines_end, ))); diff --git a/crates/ruff/src/rules/pydocstyle/rules/capitalized.rs b/crates/ruff/src/rules/pydocstyle/rules/capitalized.rs index 7a77ddcd7f..b17ef0b3cf 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/capitalized.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/capitalized.rs @@ -3,12 +3,35 @@ use rustpython_parser::ast::Ranged; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::definition::{Definition, Member, MemberKind}; +use ruff_python_semantic::{Definition, Member, MemberKind}; use crate::checkers::ast::Checker; use crate::docstrings::Docstring; use crate::registry::AsRule; +/// ## What it does +/// Checks for docstrings that do not start with a capital letter. +/// +/// ## Why is this bad? +/// The first character in a docstring should be capitalized for, grammatical +/// correctness and consistency. +/// +/// ## Example +/// ```python +/// def average(values: list[float]) -> float: +/// """return the mean of the given values.""" +/// ``` +/// +/// Use instead: +/// ```python +/// def average(values: list[float]) -> float: +/// """Return the mean of the given values.""" +/// ``` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[violation] pub struct FirstLineCapitalized { first_word: String, @@ -46,7 +69,7 @@ pub(crate) fn capitalized(checker: &mut Checker, docstring: &Docstring) { let body = docstring.body(); let Some(first_word) = body.split(' ').next() else { - return + return; }; // Like pydocstyle, we only support ASCII for now. @@ -76,8 +99,7 @@ pub(crate) fn capitalized(checker: &mut Checker, docstring: &Docstring) { ); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( capitalized_word, TextRange::at(body.start(), first_word.text_len()), ))); diff --git a/crates/ruff/src/rules/pydocstyle/rules/ends_with_period.rs b/crates/ruff/src/rules/pydocstyle/rules/ends_with_period.rs index 85045134a8..ba53386fee 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/ends_with_period.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/ends_with_period.rs @@ -11,6 +11,38 @@ use crate::docstrings::Docstring; use crate::registry::AsRule; use crate::rules::pydocstyle::helpers::logical_line; +/// ## What it does +/// Checks for docstrings in which the first line does not end in a period. +/// +/// ## Why is this bad? +/// [PEP 257] recommends that the first line of a docstring is written in the +/// form of a command, ending in a period. +/// +/// This rule may not apply to all projects; its applicability is a matter of +/// convention. By default, this rule is enabled when using the `numpy` and +/// `pep257` conventions, and disabled when using the `google` convention. +/// +/// ## Example +/// ```python +/// def average(values: list[float]) -> float: +/// """Return the mean of the given values""" +/// ``` +/// +/// Use instead: +/// ```python +/// def average(values: list[float]) -> float: +/// """Return the mean of the given values.""" +/// ``` +/// +/// ## Options +/// - `pydocstyle.convention` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) +/// +/// [PEP 257]: https://peps.python.org/pep-0257/ #[violation] pub struct EndsInPeriod; @@ -64,8 +96,7 @@ pub(crate) fn ends_with_period(checker: &mut Checker, docstring: &Docstring) { && !trimmed.ends_with(':') && !trimmed.ends_with(';') { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::insertion( + diagnostic.set_fix(Fix::suggested(Edit::insertion( ".".to_string(), line.start() + trimmed.text_len(), ))); diff --git a/crates/ruff/src/rules/pydocstyle/rules/ends_with_punctuation.rs b/crates/ruff/src/rules/pydocstyle/rules/ends_with_punctuation.rs index 8341266f01..9e11eb1c8e 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/ends_with_punctuation.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/ends_with_punctuation.rs @@ -11,6 +11,37 @@ use crate::docstrings::Docstring; use crate::registry::AsRule; use crate::rules::pydocstyle::helpers::logical_line; +/// ## What it does +/// Checks for docstrings in which the first line does not end in a punctuation +/// mark, such as a period, question mark, or exclamation point. +/// +/// ## Why is this bad? +/// The first line of a docstring should end with a period, question mark, or +/// exclamation point, for grammatical correctness and consistency. +/// +/// This rule may not apply to all projects; its applicability is a matter of +/// convention. By default, this rule is enabled when using the `google` +/// convention, and disabled when using the `numpy` and `pep257` conventions. +/// +/// ## Example +/// ```python +/// def average(values: list[float]) -> float: +/// """Return the mean of the given values""" +/// ``` +/// +/// Use instead: +/// ```python +/// def average(values: list[float]) -> float: +/// """Return the mean of the given values.""" +/// ``` +/// +/// ## Options +/// - `pydocstyle.convention` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[violation] pub struct EndsInPunctuation; @@ -61,8 +92,7 @@ pub(crate) fn ends_with_punctuation(checker: &mut Checker, docstring: &Docstring let mut diagnostic = Diagnostic::new(EndsInPunctuation, docstring.range()); // Best-effort autofix: avoid adding a period after other punctuation marks. if checker.patch(diagnostic.kind.rule()) && !trimmed.ends_with([':', ';']) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::insertion( + diagnostic.set_fix(Fix::suggested(Edit::insertion( ".".to_string(), line.start() + trimmed.text_len(), ))); diff --git a/crates/ruff/src/rules/pydocstyle/rules/if_needed.rs b/crates/ruff/src/rules/pydocstyle/rules/if_needed.rs index 1ef2016d5a..ee9e2364ea 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/if_needed.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/if_needed.rs @@ -1,13 +1,73 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::cast; -use ruff_python_ast::helpers::identifier_range; +use ruff_python_ast::identifier::Identifier; use ruff_python_semantic::analyze::visibility::is_overload; -use ruff_python_semantic::definition::{Definition, Member, MemberKind}; +use ruff_python_semantic::{Definition, Member, MemberKind}; use crate::checkers::ast::Checker; use crate::docstrings::Docstring; +/// ## What it does +/// Checks for `@overload` function definitions that contain a docstring. +/// +/// ## Why is this bad? +/// The `@overload` decorator is used to define multiple compatible signatures +/// for a given function, to support type-checking. A series of `@overload` +/// definitions should be followed by a single non-decorated definition that +/// contains the implementation of the function. +/// +/// `@overload` function definitions should not contain a docstring; instead, +/// the docstring should be placed on the non-decorated definition that contains +/// the implementation. +/// +/// ## Example +/// ```python +/// from typing import overload +/// +/// +/// @overload +/// def factorial(n: int) -> int: +/// """Return the factorial of n.""" +/// +/// +/// @overload +/// def factorial(n: float) -> float: +/// """Return the factorial of n.""" +/// +/// +/// def factorial(n): +/// """Return the factorial of n.""" +/// +/// +/// factorial.__doc__ # "Return the factorial of n." +/// ``` +/// +/// Use instead: +/// ```python +/// from typing import overload +/// +/// +/// @overload +/// def factorial(n: int) -> int: +/// ... +/// +/// +/// @overload +/// def factorial(n: float) -> float: +/// ... +/// +/// +/// def factorial(n): +/// """Return the factorial of n.""" +/// +/// +/// factorial.__doc__ # "Return the factorial of n." +/// ``` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [Python documentation: `typing.overload`](https://docs.python.org/3/library/typing.html#typing.overload) #[violation] pub struct OverloadWithDocstring; @@ -24,14 +84,14 @@ pub(crate) fn if_needed(checker: &mut Checker, docstring: &Docstring) { kind: MemberKind::Function | MemberKind::NestedFunction | MemberKind::Method, stmt, .. - }) = docstring.definition else { + }) = docstring.definition + else { return; }; - if !is_overload(checker.semantic_model(), cast::decorator_list(stmt)) { + if !is_overload(cast::decorator_list(stmt), checker.semantic()) { return; } - checker.diagnostics.push(Diagnostic::new( - OverloadWithDocstring, - identifier_range(stmt, checker.locator), - )); + checker + .diagnostics + .push(Diagnostic::new(OverloadWithDocstring, stmt.identifier())); } diff --git a/crates/ruff/src/rules/pydocstyle/rules/indent.rs b/crates/ruff/src/rules/pydocstyle/rules/indent.rs index 3be1583909..1f8762cae5 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/indent.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/indent.rs @@ -10,6 +10,38 @@ use crate::checkers::ast::Checker; use crate::docstrings::Docstring; use crate::registry::{AsRule, Rule}; +/// ## What it does +/// Checks for docstrings that are indented with tabs. +/// +/// ## Why is this bad? +/// [PEP 8](https://peps.python.org/pep-0008/#tabs-or-spaces) recommends using +/// spaces over tabs for indentation. +/// +/// +/// ## Example +/// ```python +/// def sort_list(l: list[int]) -> list[int]: +/// """Return a sorted copy of the list. +/// +/// Sort the list in ascending order and return a copy of the result using the bubble +/// sort algorithm. +/// """ +/// ``` +/// +/// Use instead: +/// ```python +/// def sort_list(l: list[int]) -> list[int]: +/// """Return a sorted copy of the list. +/// +/// Sort the list in ascending order and return a copy of the result using the bubble +/// sort algorithm. +/// """ +/// ``` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[violation] pub struct IndentWithSpaces; @@ -20,6 +52,39 @@ impl Violation for IndentWithSpaces { } } +/// ## What it does +/// Checks for under-indented docstrings. +/// +/// ## Why is this bad? +/// [PEP 257] recommends that docstrings be indented to the same level as their +/// opening quotes. Avoid under-indenting docstrings, for consistency. +/// +/// ## Example +/// ```python +/// def sort_list(l: list[int]) -> list[int]: +/// """Return a sorted copy of the list. +/// +/// Sort the list in ascending order and return a copy of the result using the bubble sort +/// algorithm. +/// """ +/// ``` +/// +/// Use instead: +/// ```python +/// def sort_list(l: list[int]) -> list[int]: +/// """Return a sorted copy of the list. +/// +/// Sort the list in ascending order and return a copy of the result using the bubble +/// sort algorithm. +/// """ +/// ``` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) +/// +/// [PEP 257]: https://peps.python.org/pep-0257/ #[violation] pub struct UnderIndentation; @@ -34,6 +99,39 @@ impl AlwaysAutofixableViolation for UnderIndentation { } } +/// ## What it does +/// Checks for over-indented docstrings. +/// +/// ## Why is this bad? +/// [PEP 257] recommends that docstrings be indented to the same level as their +/// opening quotes. Avoid over-indenting docstrings, for consistency. +/// +/// ## Example +/// ```python +/// def sort_list(l: list[int]) -> list[int]: +/// """Return a sorted copy of the list. +/// +/// Sort the list in ascending order and return a copy of the result using the +/// bubble sort algorithm. +/// """ +/// ``` +/// +/// Use instead: +/// ```python +/// def sort_list(l: list[int]) -> list[int]: +/// """Return a sorted copy of the list. +/// +/// Sort the list in ascending order and return a copy of the result using the bubble +/// sort algorithm. +/// """ +/// ``` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) +/// +/// [PEP 257]: https://peps.python.org/pep-0257/ #[violation] pub struct OverIndentation; @@ -91,8 +189,7 @@ pub(crate) fn indent(checker: &mut Checker, docstring: &Docstring) { let mut diagnostic = Diagnostic::new(UnderIndentation, TextRange::empty(line.start())); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( clean_space(docstring.indentation), TextRange::at(line.start(), line_indent.text_len()), ))); @@ -139,8 +236,7 @@ pub(crate) fn indent(checker: &mut Checker, docstring: &Docstring) { } else { Edit::range_replacement(indent, over_indented) }; - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(edit)); + diagnostic.set_fix(Fix::automatic(edit)); } checker.diagnostics.push(diagnostic); } @@ -160,8 +256,7 @@ pub(crate) fn indent(checker: &mut Checker, docstring: &Docstring) { } else { Edit::range_replacement(indent, range) }; - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(edit)); + diagnostic.set_fix(Fix::automatic(edit)); } checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/pydocstyle/rules/mod.rs b/crates/ruff/src/rules/pydocstyle/rules/mod.rs index 2cb62ffe0a..e71d7d08fc 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/mod.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/mod.rs @@ -1,41 +1,23 @@ -pub(crate) use backslashes::{backslashes, EscapeSequenceInDocstring}; -pub(crate) use blank_after_summary::{blank_after_summary, BlankLineAfterSummary}; -pub(crate) use blank_before_after_class::{ - blank_before_after_class, BlankLineBeforeClass, OneBlankLineAfterClass, OneBlankLineBeforeClass, -}; -pub(crate) use blank_before_after_function::{ - blank_before_after_function, NoBlankLineAfterFunction, NoBlankLineBeforeFunction, -}; -pub(crate) use capitalized::{capitalized, FirstLineCapitalized}; -pub(crate) use ends_with_period::{ends_with_period, EndsInPeriod}; -pub(crate) use ends_with_punctuation::{ends_with_punctuation, EndsInPunctuation}; -pub(crate) use if_needed::{if_needed, OverloadWithDocstring}; -pub(crate) use indent::{indent, IndentWithSpaces, OverIndentation, UnderIndentation}; -pub(crate) use multi_line_summary_start::{ - multi_line_summary_start, MultiLineSummaryFirstLine, MultiLineSummarySecondLine, -}; -pub(crate) use newline_after_last_paragraph::{ - newline_after_last_paragraph, NewLineAfterLastParagraph, -}; -pub(crate) use no_signature::{no_signature, NoSignature}; -pub(crate) use no_surrounding_whitespace::{no_surrounding_whitespace, SurroundingWhitespace}; -pub(crate) use non_imperative_mood::{non_imperative_mood, NonImperativeMood}; -pub(crate) use not_empty::{not_empty, EmptyDocstring}; -pub(crate) use not_missing::{ - not_missing, UndocumentedMagicMethod, UndocumentedPublicClass, UndocumentedPublicFunction, - UndocumentedPublicInit, UndocumentedPublicMethod, UndocumentedPublicModule, - UndocumentedPublicNestedClass, UndocumentedPublicPackage, -}; -pub(crate) use one_liner::{one_liner, FitsOnOneLine}; -pub(crate) use sections::{ - sections, BlankLineAfterLastSection, BlankLinesBetweenHeaderAndContent, CapitalizeSectionName, - DashedUnderlineAfterSection, EmptyDocstringSection, NewLineAfterSectionName, - NoBlankLineAfterSection, NoBlankLineBeforeSection, SectionNameEndsInColon, - SectionNotOverIndented, SectionUnderlineAfterName, SectionUnderlineMatchesSectionLength, - SectionUnderlineNotOverIndented, UndocumentedParam, -}; -pub(crate) use starts_with_this::{starts_with_this, DocstringStartsWithThis}; -pub(crate) use triple_quotes::{triple_quotes, TripleSingleQuotes}; +pub(crate) use backslashes::*; +pub(crate) use blank_after_summary::*; +pub(crate) use blank_before_after_class::*; +pub(crate) use blank_before_after_function::*; +pub(crate) use capitalized::*; +pub(crate) use ends_with_period::*; +pub(crate) use ends_with_punctuation::*; +pub(crate) use if_needed::*; +pub(crate) use indent::*; +pub(crate) use multi_line_summary_start::*; +pub(crate) use newline_after_last_paragraph::*; +pub(crate) use no_signature::*; +pub(crate) use no_surrounding_whitespace::*; +pub(crate) use non_imperative_mood::*; +pub(crate) use not_empty::*; +pub(crate) use not_missing::*; +pub(crate) use one_liner::*; +pub(crate) use sections::*; +pub(crate) use starts_with_this::*; +pub(crate) use triple_quotes::*; mod backslashes; mod blank_after_summary; diff --git a/crates/ruff/src/rules/pydocstyle/rules/multi_line_summary_start.rs b/crates/ruff/src/rules/pydocstyle/rules/multi_line_summary_start.rs index 8453973125..7660553e96 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/multi_line_summary_start.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/multi_line_summary_start.rs @@ -4,13 +4,53 @@ use rustpython_parser::ast::Ranged; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::str::{is_triple_quote, leading_quote}; -use ruff_python_semantic::definition::{Definition, Member}; +use ruff_python_semantic::{Definition, Member}; use ruff_python_whitespace::{NewlineWithTrailingNewline, UniversalNewlineIterator}; use crate::checkers::ast::Checker; use crate::docstrings::Docstring; use crate::registry::{AsRule, Rule}; +/// ## What it does +/// Checks for docstring summary lines that are not positioned on the first +/// physical line of the docstring. +/// +/// ## Why is this bad? +/// [PEP 257] recommends that multi-line docstrings consist of "a summary line +/// just like a one-line docstring, followed by a blank line, followed by a +/// more elaborate description." +/// +/// The summary line should be located on the first physical line of the +/// docstring, immediately after the opening quotes. +/// +/// This rule may not apply to all projects; its applicability is a matter of +/// convention. By default, this rule is enabled when using the `google` +/// convention, and disabled when using the `numpy` and `pep257` conventions. +/// +/// For an alternative, see [D213]. +/// +/// ## Example +/// ```python +/// def sort_list(l: list[int]) -> list[int]: +/// """ +/// Return a sorted copy of the list. +/// +/// Sort the list in ascending order and return a copy of the result using the +/// bubble sort algorithm. +/// """ +/// ``` +/// +/// Use instead: +/// ```python +/// def sort_list(l: list[int]) -> list[int]: +/// """Return a sorted copy of the list. +/// +/// Sort the list in ascending order and return a copy of the result using the bubble +/// sort algorithm. +/// """ +/// ``` +/// +/// [D213]: https://beta.ruff.rs/docs/rules/multi-line-summary-second-line #[violation] pub struct MultiLineSummaryFirstLine; @@ -25,6 +65,46 @@ impl AlwaysAutofixableViolation for MultiLineSummaryFirstLine { } } +/// ## What it does +/// Checks for docstring summary lines that are not positioned on the second +/// physical line of the docstring. +/// +/// ## Why is this bad? +/// [PEP 257] recommends that multi-line docstrings consist of "a summary line +/// just like a one-line docstring, followed by a blank line, followed by a +/// more elaborate description." +/// +/// The summary line should be located on the second physical line of the +/// docstring, immediately after the opening quotes and the blank line. +/// +/// This rule may not apply to all projects; its applicability is a matter of +/// convention. By default, this rule is disabled when using the `google`, +/// `numpy`, and `pep257` conventions. +/// +/// For an alternative, see [D212]. +/// +/// ## Example +/// ```python +/// def sort_list(l: list[int]) -> list[int]: +/// """Return a sorted copy of the list. +/// +/// Sort the list in ascending order and return a copy of the result using the +/// bubble sort algorithm. +/// """ +/// ``` +/// +/// Use instead: +/// ```python +/// def sort_list(l: list[int]) -> list[int]: +/// """ +/// Return a sorted copy of the list. +/// +/// Sort the list in ascending order and return a copy of the result using the bubble +/// sort algorithm. +/// """ +/// ``` +/// +/// [D212]: https://beta.ruff.rs/docs/rules/multi-line-summary-first-line #[violation] pub struct MultiLineSummarySecondLine; @@ -52,10 +132,7 @@ pub(crate) fn multi_line_summary_start(checker: &mut Checker, docstring: &Docstr }; let mut content_lines = UniversalNewlineIterator::with_offset(contents, docstring.start()); - let Some(first_line) = content_lines - .next() - else - { + let Some(first_line) = content_lines.next() else { return; }; @@ -66,8 +143,7 @@ pub(crate) fn multi_line_summary_start(checker: &mut Checker, docstring: &Docstr // Delete until first non-whitespace char. for line in content_lines { if let Some(end_column) = line.find(|c: char| !c.is_whitespace()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::deletion( + diagnostic.set_fix(Fix::automatic(Edit::deletion( first_line.end(), line.start() + TextSize::try_from(end_column).unwrap(), ))); @@ -114,8 +190,7 @@ pub(crate) fn multi_line_summary_start(checker: &mut Checker, docstring: &Docstr first_line.strip_prefix(prefix).unwrap().trim_start() ); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::replacement( + diagnostic.set_fix(Fix::automatic(Edit::replacement( repl, body.start(), first_line.end(), diff --git a/crates/ruff/src/rules/pydocstyle/rules/newline_after_last_paragraph.rs b/crates/ruff/src/rules/pydocstyle/rules/newline_after_last_paragraph.rs index 499cfb4bae..384635ea61 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/newline_after_last_paragraph.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/newline_after_last_paragraph.rs @@ -10,6 +10,40 @@ use crate::checkers::ast::Checker; use crate::docstrings::Docstring; use crate::registry::AsRule; +/// ## What it does +/// Checks for multi-line docstrings whose closing quotes are not on their +/// own line. +/// +/// ## Why is this bad? +/// [PEP 257] recommends that the closing quotes of a multi-line docstring be +/// on their own line, for consistency and compatibility with documentation +/// tools that may need to parse the docstring. +/// +/// ## Example +/// ```python +/// def sort_list(l: List[int]) -> List[int]: +/// """Return a sorted copy of the list. +/// +/// Sort the list in ascending order and return a copy of the result using the +/// bubble sort algorithm.""" +/// ``` +/// +/// Use instead: +/// ```python +/// def sort_list(l: List[int]) -> List[int]: +/// """Return a sorted copy of the list. +/// +/// Sort the list in ascending order and return a copy of the result using the bubble +/// sort algorithm. +/// """ +/// ``` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) +/// +/// [PEP 257]: https://peps.python.org/pep-0257/ #[violation] pub struct NewLineAfterLastParagraph; @@ -58,8 +92,7 @@ pub(crate) fn newline_after_last_paragraph(checker: &mut Checker, docstring: &Do checker.stylist.line_ending().as_str(), clean_space(docstring.indentation) ); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::replacement( + diagnostic.set_fix(Fix::automatic(Edit::replacement( content, docstring.expr.end() - num_trailing_quotes - num_trailing_spaces, docstring.expr.end() - num_trailing_quotes, diff --git a/crates/ruff/src/rules/pydocstyle/rules/no_signature.rs b/crates/ruff/src/rules/pydocstyle/rules/no_signature.rs index bbca269d99..11054ab016 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/no_signature.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/no_signature.rs @@ -2,12 +2,46 @@ use rustpython_parser::ast::{self, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::definition::{Definition, Member, MemberKind}; +use ruff_python_semantic::{Definition, Member, MemberKind}; use ruff_python_whitespace::UniversalNewlines; use crate::checkers::ast::Checker; use crate::docstrings::Docstring; +/// ## What it does +/// Checks for function docstrings that include the function's signature in +/// the summary line. +/// +/// ## Why is this bad? +/// [PEP 257] recommends against including a function's signature in its +/// docstring. Instead, consider using type annotations as a form of +/// documentation for the function's parameters and return value. +/// +/// This rule may not apply to all projects; its applicability is a matter of +/// convention. By default, this rule is enabled when using the `google` and +/// `pep257` conventions, and disabled when using the `numpy` convention. +/// +/// ## Example +/// ```python +/// def foo(a, b): +/// """foo(a: int, b: int) -> list[int]""" +/// ``` +/// +/// Use instead: +/// ```python +/// def foo(a: int, b: int) -> list[int]: +/// """Return a list of a and b.""" +/// ``` +/// +/// ## Options +/// - `pydocstyle.convention` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) +/// +/// [PEP 257]: https://peps.python.org/pep-0257/ #[violation] pub struct NoSignature; @@ -24,7 +58,8 @@ pub(crate) fn no_signature(checker: &mut Checker, docstring: &Docstring) { kind: MemberKind::Function | MemberKind::NestedFunction | MemberKind::Method, stmt, .. - }) = docstring.definition else { + }) = docstring.definition + else { return; }; let Stmt::FunctionDef(ast::StmtFunctionDef { name, .. }) = stmt else { diff --git a/crates/ruff/src/rules/pydocstyle/rules/no_surrounding_whitespace.rs b/crates/ruff/src/rules/pydocstyle/rules/no_surrounding_whitespace.rs index 105df09759..cd1d4ff3da 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/no_surrounding_whitespace.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/no_surrounding_whitespace.rs @@ -9,6 +9,28 @@ use crate::docstrings::Docstring; use crate::registry::AsRule; use crate::rules::pydocstyle::helpers::ends_with_backslash; +/// ## What it does +/// Checks for surrounding whitespace in docstrings. +/// +/// ## Why is this bad? +/// Remove surrounding whitespace from the docstring, for consistency. +/// +/// ## Example +/// ```python +/// def factorial(n: int) -> int: +/// """ Return the factorial of n. """ +/// ``` +/// +/// Use instead: +/// ```python +/// def factorial(n: int) -> int: +/// """Return the factorial of n.""" +/// ``` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[violation] pub struct SurroundingWhitespace; @@ -47,8 +69,7 @@ pub(crate) fn no_surrounding_whitespace(checker: &mut Checker, docstring: &Docst // characters, avoid applying the fix. if !trimmed.ends_with(quote) && !trimmed.starts_with(quote) && !ends_with_backslash(trimmed) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( trimmed.to_string(), TextRange::at(body.start(), line.text_len()), ))); diff --git a/crates/ruff/src/rules/pydocstyle/rules/non_imperative_mood.rs b/crates/ruff/src/rules/pydocstyle/rules/non_imperative_mood.rs index 5459f09512..3969452fce 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/non_imperative_mood.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/non_imperative_mood.rs @@ -8,7 +8,7 @@ use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::call_path::{from_qualified_name, CallPath}; use ruff_python_ast::cast; use ruff_python_semantic::analyze::visibility::{is_property, is_test}; -use ruff_python_semantic::definition::{Definition, Member, MemberKind}; +use ruff_python_semantic::{Definition, Member, MemberKind}; use ruff_python_whitespace::UniversalNewlines; use crate::checkers::ast::Checker; @@ -17,6 +17,52 @@ use crate::rules::pydocstyle::helpers::normalize_word; static MOOD: Lazy = Lazy::new(Mood::new); +/// ## What it does +/// Checks for docstring first lines that are not in an imperative mood. +/// +/// ## Why is this bad? +/// [PEP 257] recommends that the first line of a docstring be written in the +/// imperative mood, for consistency. +/// +/// Hint: to rewrite the docstring in the imperative, phrase the first line as +/// if it were a command. +/// +/// This rule may not apply to all projects; its applicability is a matter of +/// convention. By default, this rule is enabled when using the `numpy` and +/// `pep257` conventions, and disabled when using the `google` conventions. +/// +/// ## Example +/// ```python +/// def average(values: list[float]) -> float: +/// """Returns the mean of the given values.""" +/// ``` +/// +/// Use instead: +/// ```python +/// def average(values: list[float]) -> float: +/// """Return the mean of the given values.""" +/// ``` +/// +/// ## Options +/// - `pydocstyle.convention` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// +/// [PEP 257]: https://peps.python.org/pep-0257/ +#[violation] +pub struct NonImperativeMood { + first_line: String, +} + +impl Violation for NonImperativeMood { + #[derive_message_formats] + fn message(&self) -> String { + let NonImperativeMood { first_line } = self; + format!("First line of docstring should be in imperative mood: \"{first_line}\"") + } +} + /// D401 pub(crate) fn non_imperative_mood( checker: &mut Checker, @@ -41,9 +87,9 @@ pub(crate) fn non_imperative_mood( if is_test(cast::name(stmt)) || is_property( - checker.semantic_model(), cast::decorator_list(stmt), &property_decorators, + checker.semantic(), ) { return; @@ -52,31 +98,26 @@ pub(crate) fn non_imperative_mood( let body = docstring.body(); // Find first line, disregarding whitespace. - let line = match body.trim().universal_newlines().next() { + let first_line = match body.trim().universal_newlines().next() { Some(line) => line.as_str().trim(), None => return, }; + // Find the first word on that line and normalize it to lower-case. - let first_word_norm = match line.split_whitespace().next() { + let first_word_norm = match first_line.split_whitespace().next() { Some(word) => normalize_word(word), None => return, }; if first_word_norm.is_empty() { return; } - if let Some(false) = MOOD.is_imperative(&first_word_norm) { - let diagnostic = Diagnostic::new(NonImperativeMood(line.to_string()), docstring.range()); - checker.diagnostics.push(diagnostic); - } -} - -#[violation] -pub struct NonImperativeMood(pub String); - -impl Violation for NonImperativeMood { - #[derive_message_formats] - fn message(&self) -> String { - let NonImperativeMood(first_line) = self; - format!("First line of docstring should be in imperative mood: \"{first_line}\"") + + if matches!(MOOD.is_imperative(&first_word_norm), Some(false)) { + checker.diagnostics.push(Diagnostic::new( + NonImperativeMood { + first_line: first_line.to_string(), + }, + docstring.range(), + )); } } diff --git a/crates/ruff/src/rules/pydocstyle/rules/not_empty.rs b/crates/ruff/src/rules/pydocstyle/rules/not_empty.rs index c349336e7c..3c89974b37 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/not_empty.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/not_empty.rs @@ -5,6 +5,29 @@ use crate::checkers::ast::Checker; use crate::docstrings::Docstring; use crate::registry::Rule; +/// ## What it does +/// Checks for empty docstrings. +/// +/// ## Why is this bad? +/// An empty docstring is indicative of incomplete documentation. It should either +/// be removed or replaced with a meaningful docstring. +/// +/// ## Example +/// ```python +/// def average(values: list[float]) -> float: +/// """""" +/// ``` +/// +/// Use instead: +/// ```python +/// def average(values: list[float]) -> float: +/// """Return the mean of the given values.""" +/// ``` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[violation] pub struct EmptyDocstring; diff --git a/crates/ruff/src/rules/pydocstyle/rules/not_missing.rs b/crates/ruff/src/rules/pydocstyle/rules/not_missing.rs index a4ecde3e00..cefee351cb 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/not_missing.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/not_missing.rs @@ -3,15 +3,65 @@ use ruff_text_size::TextRange; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::cast; -use ruff_python_ast::helpers::identifier_range; +use ruff_python_ast::identifier::Identifier; use ruff_python_semantic::analyze::visibility::{ is_call, is_init, is_magic, is_new, is_overload, is_override, Visibility, }; -use ruff_python_semantic::definition::{Definition, Member, MemberKind, Module, ModuleKind}; +use ruff_python_semantic::{Definition, Member, MemberKind, Module, ModuleKind}; use crate::checkers::ast::Checker; use crate::registry::Rule; +/// ## What it does +/// Checks for undocumented public module definitions. +/// +/// ## Why is this bad? +/// Public modules should be documented via docstrings to outline their purpose +/// and contents. +/// +/// Generally, module docstrings should describe the purpose of the module and +/// list the classes, exceptions, functions, and other objects that are exported +/// by the module, alongside a one-line summary of each. +/// +/// If the module is a script, the docstring should be usable as its "usage" +/// message. +/// +/// If the codebase adheres to a standard format for module docstrings, follow +/// that format for consistency. +/// +/// ## Example +/// ```python +/// class FasterThanLightError(ZeroDivisionError): +/// ... +/// +/// +/// def calculate_speed(distance: float, time: float) -> float: +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// """Utility functions and classes for calculating speed. +/// +/// This module provides: +/// - FasterThanLightError: exception when FTL speed is calculated; +/// - calculate_speed: calculate speed given distance and time. +/// """ +/// +/// +/// class FasterThanLightError(ZeroDivisionError): +/// ... +/// +/// +/// def calculate_speed(distance: float, time: float) -> float: +/// ... +/// ``` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[violation] pub struct UndocumentedPublicModule; @@ -22,6 +72,79 @@ impl Violation for UndocumentedPublicModule { } } +/// ## What it does +/// Checks for undocumented public class definitions. +/// +/// ## Why is this bad? +/// Public classes should be documented via docstrings to outline their purpose +/// and behavior. +/// +/// Generally, a class docstring should describe the class's purpose and list +/// its public attributes and methods. +/// +/// If the codebase adheres to a standard format for class docstrings, follow +/// that format for consistency. +/// +/// ## Example +/// ```python +/// class Player: +/// def __init__(self, name: str, points: int = 0) -> None: +/// self.name: str = name +/// self.points: int = points +/// +/// def add_points(self, points: int) -> None: +/// self.points += points +/// ``` +/// +/// Use instead (in the NumPy docstring format): +/// ```python +/// class Player: +/// """A player in the game. +/// +/// Attributes +/// ---------- +/// name : str +/// The name of the player. +/// points : int +/// The number of points the player has. +/// +/// Methods +/// ------- +/// add_points(points: int) -> None +/// Add points to the player's score. +/// """ +/// +/// def __init__(self, name: str, points: int = 0) -> None: +/// self.name: str = name +/// self.points: int = points +/// +/// def add_points(self, points: int) -> None: +/// self.points += points +/// ``` +/// +/// Or (in the Google docstring format): +/// ```python +/// class Player: +/// """A player in the game. +/// +/// Attributes: +/// name: The name of the player. +/// points: The number of points the player has. +/// """ +/// +/// def __init__(self, name: str, points: int = 0) -> None: +/// self.name: str = name +/// self.points: int = points +/// +/// def add_points(self, points: int) -> None: +/// self.points += points +/// ``` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[violation] pub struct UndocumentedPublicClass; @@ -32,6 +155,75 @@ impl Violation for UndocumentedPublicClass { } } +/// ## What it does +/// Checks for undocumented public method definitions. +/// +/// ## Why is this bad? +/// Public methods should be documented via docstrings to outline their purpose +/// and behavior. +/// +/// Generally, a method docstring should describe the method's behavior, +/// arguments, side effects, exceptions, return values, and any other +/// information that may be relevant to the user. +/// +/// If the codebase adheres to a standard format for method docstrings, follow +/// that format for consistency. +/// +/// ## Example +/// ```python +/// class Cat(Animal): +/// def greet(self, happy: bool = True): +/// if happy: +/// print("Meow!") +/// else: +/// raise ValueError("Tried to greet an unhappy cat.") +/// ``` +/// +/// Use instead (in the NumPy docstring format): +/// ```python +/// class Cat(Animal): +/// def greet(self, happy: bool = True): +/// """Print a greeting from the cat. +/// +/// Parameters +/// ---------- +/// happy : bool, optional +/// Whether the cat is happy, is True by default. +/// +/// Raises +/// ------ +/// ValueError +/// If the cat is not happy. +/// """ +/// if happy: +/// print("Meow!") +/// else: +/// raise ValueError("Tried to greet an unhappy cat.") +/// ``` +/// +/// Or (in the Google docstring format): +/// ```python +/// class Cat(Animal): +/// def greet(self, happy: bool = True): +/// """Print a greeting from the cat. +/// +/// Args: +/// happy: Whether the cat is happy, is True by default. +/// +/// Raises: +/// ValueError: If the cat is not happy. +/// """ +/// if happy: +/// print("Meow!") +/// else: +/// raise ValueError("Tried to greet an unhappy cat.") +/// ``` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[violation] pub struct UndocumentedPublicMethod; @@ -42,6 +234,83 @@ impl Violation for UndocumentedPublicMethod { } } +/// ## What it does +/// Checks for undocumented public function definitions. +/// +/// ## Why is this bad? +/// Public functions should be documented via docstrings to outline their +/// purpose and behavior. +/// +/// Generally, a function docstring should describe the function's behavior, +/// arguments, side effects, exceptions, return values, and any other +/// information that may be relevant to the user. +/// +/// If the codebase adheres to a standard format for function docstrings, follow +/// that format for consistency. +/// +/// ## Example +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// Use instead (using the NumPy docstring format): +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Parameters +/// ---------- +/// distance : float +/// Distance traveled. +/// time : float +/// Time spent traveling. +/// +/// Returns +/// ------- +/// float +/// Speed as distance divided by time. +/// +/// Raises +/// ------ +/// FasterThanLightError +/// If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// Or, using the Google docstring format: +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Args: +/// distance: Distance traveled. +/// time: Time spent traveling. +/// +/// Returns: +/// Speed as distance divided by time. +/// +/// Raises: +/// FasterThanLightError: If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Style Python Docstrings](https://google.github.io/styleguide/pyguide.html#s3.8-comments-and-docstrings) #[violation] pub struct UndocumentedPublicFunction; @@ -52,6 +321,39 @@ impl Violation for UndocumentedPublicFunction { } } +/// ## What it does +/// Checks for undocumented public package definitions. +/// +/// ## Why is this bad? +/// Public packages should be documented via docstrings to outline their +/// purpose and contents. +/// +/// Generally, package docstrings should list the modules and subpackages that +/// are exported by the package. +/// +/// If the codebase adheres to a standard format for package docstrings, follow +/// that format for consistency. +/// +/// ## Example +/// ```python +/// __all__ = ["Player", "Game"] +/// ``` +/// +/// Use instead: +/// ```python +/// """Game and player management package. +/// +/// This package provides classes for managing players and games. +/// """ +/// +/// __all__ = ["player", "game"] +/// ``` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Style Python Docstrings](https://google.github.io/styleguide/pyguide.html#s3.8-comments-and-docstrings) #[violation] pub struct UndocumentedPublicPackage; @@ -62,6 +364,50 @@ impl Violation for UndocumentedPublicPackage { } } +/// ## What it does +/// Checks for undocumented magic method definitions. +/// +/// ## Why is this bad? +/// Magic methods (methods with names that start and end with double +/// underscores) are used to implement operator overloading and other special +/// behavior. Such methods should should be documented via docstrings to +/// outline their behavior. +/// +/// Generally, magic method docstrings should describe the method's behavior, +/// arguments, side effects, exceptions, return values, and any other +/// information that may be relevant to the user. +/// +/// If the codebase adheres to a standard format for method docstrings, follow +/// that format for consistency. +/// +/// ## Example +/// ```python +/// class Cat(Animal): +/// def __str__(self) -> str: +/// return f"Cat: {self.name}" +/// +/// +/// cat = Cat("Dusty") +/// print(cat) # "Cat: Dusty" +/// ``` +/// +/// Use instead: +/// ```python +/// class Cat(Animal): +/// def __str__(self) -> str: +/// """Return a string representation of the cat.""" +/// return f"Cat: {self.name}" +/// +/// +/// cat = Cat("Dusty") +/// print(cat) # "Cat: Dusty" +/// ``` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Style Python Docstrings](https://google.github.io/styleguide/pyguide.html#s3.8-comments-and-docstrings) #[violation] pub struct UndocumentedMagicMethod; @@ -72,6 +418,50 @@ impl Violation for UndocumentedMagicMethod { } } +/// ## What it does +/// Checks for undocumented public class definitions, for nested classes. +/// +/// ## Why is this bad? +/// Public classes should be documented via docstrings to outline their +/// purpose and behavior. +/// +/// Nested classes do not inherit the docstring of their enclosing class, so +/// they should have their own docstrings. +/// +/// If the codebase adheres to a standard format for class docstrings, follow +/// that format for consistency. +/// +/// ## Example +/// ```python +/// class Foo: +/// """Class Foo.""" +/// +/// class Bar: +/// ... +/// +/// +/// bar = Foo.Bar() +/// bar.__doc__ # None +/// ``` +/// +/// Use instead: +/// ```python +/// class Foo: +/// """Class Foo.""" +/// +/// class Bar: +/// """Class Bar.""" +/// +/// +/// bar = Foo.Bar() +/// bar.__doc__ # "Class Bar." +/// ``` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Style Python Docstrings](https://google.github.io/styleguide/pyguide.html#s3.8-comments-and-docstrings) #[violation] pub struct UndocumentedPublicNestedClass; @@ -82,6 +472,41 @@ impl Violation for UndocumentedPublicNestedClass { } } +/// ## What it does +/// Checks for public `__init__` method definitions that are missing +/// docstrings. +/// +/// ## Why is this bad? +/// Public `__init__` methods are used to initialize objects. `__init__` +/// methods should be documented via docstrings to describe the method's +/// behavior, arguments, side effects, exceptions, and any other information +/// that may be relevant to the user. +/// +/// If the codebase adheres to a standard format for `__init__` method docstrings, +/// follow that format for consistency. +/// +/// ## Example +/// ```python +/// class City: +/// def __init__(self, name: str, population: int) -> None: +/// self.name: str = name +/// self.population: int = population +/// ``` +/// +/// Use instead: +/// ```python +/// class City: +/// def __init__(self, name: str, population: int) -> None: +/// """Initialize a city with a name and population.""" +/// self.name: str = name +/// self.population: int = population +/// ``` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Style Python Docstrings](https://google.github.io/styleguide/pyguide.html#s3.8-comments-and-docstrings) #[violation] pub struct UndocumentedPublicInit; @@ -133,10 +558,9 @@ pub(crate) fn not_missing( .. }) => { if checker.enabled(Rule::UndocumentedPublicClass) { - checker.diagnostics.push(Diagnostic::new( - UndocumentedPublicClass, - identifier_range(stmt, checker.locator), - )); + checker + .diagnostics + .push(Diagnostic::new(UndocumentedPublicClass, stmt.identifier())); } false } @@ -148,7 +572,7 @@ pub(crate) fn not_missing( if checker.enabled(Rule::UndocumentedPublicNestedClass) { checker.diagnostics.push(Diagnostic::new( UndocumentedPublicNestedClass, - identifier_range(stmt, checker.locator), + stmt.identifier(), )); } false @@ -158,13 +582,13 @@ pub(crate) fn not_missing( stmt, .. }) => { - if is_overload(checker.semantic_model(), cast::decorator_list(stmt)) { + if is_overload(cast::decorator_list(stmt), checker.semantic()) { true } else { if checker.enabled(Rule::UndocumentedPublicFunction) { checker.diagnostics.push(Diagnostic::new( UndocumentedPublicFunction, - identifier_range(stmt, checker.locator), + stmt.identifier(), )); } false @@ -175,40 +599,36 @@ pub(crate) fn not_missing( stmt, .. }) => { - if is_overload(checker.semantic_model(), cast::decorator_list(stmt)) - || is_override(checker.semantic_model(), cast::decorator_list(stmt)) + if is_overload(cast::decorator_list(stmt), checker.semantic()) + || is_override(cast::decorator_list(stmt), checker.semantic()) { true } else if is_init(cast::name(stmt)) { if checker.enabled(Rule::UndocumentedPublicInit) { - checker.diagnostics.push(Diagnostic::new( - UndocumentedPublicInit, - identifier_range(stmt, checker.locator), - )); + checker + .diagnostics + .push(Diagnostic::new(UndocumentedPublicInit, stmt.identifier())); } true } else if is_new(cast::name(stmt)) || is_call(cast::name(stmt)) { if checker.enabled(Rule::UndocumentedPublicMethod) { - checker.diagnostics.push(Diagnostic::new( - UndocumentedPublicMethod, - identifier_range(stmt, checker.locator), - )); + checker + .diagnostics + .push(Diagnostic::new(UndocumentedPublicMethod, stmt.identifier())); } true } else if is_magic(cast::name(stmt)) { if checker.enabled(Rule::UndocumentedMagicMethod) { - checker.diagnostics.push(Diagnostic::new( - UndocumentedMagicMethod, - identifier_range(stmt, checker.locator), - )); + checker + .diagnostics + .push(Diagnostic::new(UndocumentedMagicMethod, stmt.identifier())); } true } else { if checker.enabled(Rule::UndocumentedPublicMethod) { - checker.diagnostics.push(Diagnostic::new( - UndocumentedPublicMethod, - identifier_range(stmt, checker.locator), - )); + checker + .diagnostics + .push(Diagnostic::new(UndocumentedPublicMethod, stmt.identifier())); } true } diff --git a/crates/ruff/src/rules/pydocstyle/rules/one_liner.rs b/crates/ruff/src/rules/pydocstyle/rules/one_liner.rs index 616083fc15..7a3ad6980b 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/one_liner.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/one_liner.rs @@ -7,6 +7,31 @@ use crate::checkers::ast::Checker; use crate::docstrings::Docstring; use crate::registry::AsRule; +/// ## What it does +/// Checks for single-line docstrings that are broken across multiple lines. +/// +/// ## Why is this bad? +/// [PEP 257] recommends that docstrings that _can_ fit on one line should be +/// formatted on a single line, for consistency and readability. +/// +/// ## Example +/// ```python +/// def average(values: list[float]) -> float: +/// """ +/// Return the mean of the given values. +/// """ +/// ``` +/// +/// Use instead: +/// ```python +/// def average(values: list[float]) -> float: +/// """Return the mean of the given values.""" +/// ``` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// +/// [PEP 257]: https://peps.python.org/pep-0257/ #[violation] pub struct FitsOnOneLine; @@ -51,8 +76,7 @@ pub(crate) fn one_liner(checker: &mut Checker, docstring: &Docstring) { if !trimmed.ends_with(trailing.chars().last().unwrap()) && !trimmed.starts_with(leading.chars().last().unwrap()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( format!("{leading}{trimmed}{trailing}"), docstring.range(), ))); diff --git a/crates/ruff/src/rules/pydocstyle/rules/sections.rs b/crates/ruff/src/rules/pydocstyle/rules/sections.rs index f101a18246..4dd426f4b5 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/sections.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/sections.rs @@ -3,17 +3,17 @@ use once_cell::sync::Lazy; use regex::Regex; use ruff_text_size::{TextLen, TextRange, TextSize}; use rustc_hash::FxHashSet; -use rustpython_parser::ast::{self, Stmt}; +use rustpython_parser::ast::{self, ArgWithDefault, Stmt}; use ruff_diagnostics::{AlwaysAutofixableViolation, Violation}; use ruff_diagnostics::{Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::cast; use ruff_python_ast::docstrings::{clean_space, leading_space}; -use ruff_python_ast::helpers::identifier_range; +use ruff_python_ast::identifier::Identifier; use ruff_python_semantic::analyze::visibility::is_staticmethod; -use ruff_python_semantic::definition::{Definition, Member, MemberKind}; -use ruff_python_whitespace::NewlineWithTrailingNewline; +use ruff_python_semantic::{Definition, Member, MemberKind}; +use ruff_python_whitespace::{NewlineWithTrailingNewline, PythonWhitespace}; use ruff_textwrap::dedent; use crate::checkers::ast::Checker; @@ -23,6 +23,68 @@ use crate::docstrings::Docstring; use crate::registry::{AsRule, Rule}; use crate::rules::pydocstyle::settings::Convention; +/// ## What it does +/// Checks for over-indented sections in docstrings. +/// +/// ## Why is this bad? +/// Multi-line docstrings are typically composed of a summary line, followed by +/// a blank line, followed by a series of sections, each with a section header +/// and a section body. +/// +/// Each section should use consistent indentation, with the section headers +/// matching the indentation of the docstring's opening quotes, and the +/// section bodies being indented one level further. +/// +/// ## Example +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Args: +/// distance: Distance traveled. +/// time: Time spent traveling. +/// +/// Returns: +/// Speed as distance divided by time. +/// +/// Raises: +/// FasterThanLightError: If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// Use instead: +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Args: +/// distance: Distance traveled. +/// time: Time spent traveling. +/// +/// Returns: +/// Speed as distance divided by time. +/// +/// Raises: +/// FasterThanLightError: If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// ## Options +/// - `pydocstyle.convention` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[violation] pub struct SectionNotOverIndented { name: String, @@ -41,6 +103,86 @@ impl AlwaysAutofixableViolation for SectionNotOverIndented { } } +/// ## What it does +/// Checks for over-indented section underlines in docstrings. +/// +/// ## Why is this bad? +/// Multi-line docstrings are typically composed of a summary line, followed by +/// a blank line, followed by a series of sections, each with a section header +/// and a section body. +/// +/// Some docstring formats (like reStructuredText) use underlines to separate +/// section bodies from section headers. +/// +/// Avoid over-indenting the section underlines, as this can cause syntax +/// errors in reStructuredText. +/// +/// By default, this rule is enabled when using the `numpy` convention, and +/// disabled when using the `google` or `pep257` conventions. +/// +/// ## Example +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Parameters +/// ---------- +/// distance : float +/// Distance traveled. +/// time : float +/// Time spent traveling. +/// +/// Returns +/// ------- +/// float +/// Speed as distance divided by time. +/// +/// Raises +/// ------ +/// FasterThanLightError +/// If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// Use instead: +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Parameters +/// ---------- +/// distance : float +/// Distance traveled. +/// time : float +/// Time spent traveling. +/// +/// Returns +/// ------- +/// float +/// Speed as distance divided by time. +/// +/// Raises +/// ------ +/// FasterThanLightError +/// If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// ## Options +/// - `pydocstyle.convention` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) #[violation] pub struct SectionUnderlineNotOverIndented { name: String, @@ -59,6 +201,67 @@ impl AlwaysAutofixableViolation for SectionUnderlineNotOverIndented { } } +/// ## What it does +/// Checks for section headers in docstrings that do not begin with capital +/// letters. +/// +/// ## Why is this bad? +/// Multi-line docstrings are typically composed of a summary line, followed by +/// a blank line, followed by a series of sections, each with a section header +/// and a section body. +/// +/// Section headers should be capitalized, for consistency. +/// +/// ## Example +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// args: +/// distance: Distance traveled. +/// time: Time spent traveling. +/// +/// returns: +/// Speed as distance divided by time. +/// +/// raises: +/// FasterThanLightError: If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// Use instead: +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Args: +/// distance: Distance traveled. +/// time: Time spent traveling. +/// +/// Returns: +/// Speed as distance divided by time. +/// +/// Raises: +/// FasterThanLightError: If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// ## Options +/// - `pydocstyle.convention` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[violation] pub struct CapitalizeSectionName { name: String, @@ -77,6 +280,84 @@ impl AlwaysAutofixableViolation for CapitalizeSectionName { } } +/// ## What it does +/// Checks that section headers in docstrings that are not followed by a +/// newline. +/// +/// ## Why is this bad? +/// Multi-line docstrings are typically composed of a summary line, followed by +/// a blank line, followed by a series of sections, each with a section header +/// and a section body. +/// +/// Section headers should be followed by a newline, and not by another +/// character (like a colon), for consistency. +/// +/// This rule is enabled when using the `numpy` convention, and disabled +/// when using the `google` or `pep257` conventions. +/// +/// ## Example +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Parameters: +/// ----------- +/// distance : float +/// Distance traveled. +/// time : float +/// Time spent traveling. +/// +/// Returns: +/// -------- +/// float +/// Speed as distance divided by time. +/// +/// Raises: +/// ------- +/// FasterThanLightError +/// If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// Use instead: +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Parameters +/// ---------- +/// distance : float +/// Distance traveled. +/// time : float +/// Time spent traveling. +/// +/// Returns +/// ------- +/// float +/// Speed as distance divided by time. +/// +/// Raises +/// ------ +/// FasterThanLightError +/// If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// ## Options +/// - `pydocstyle.convention` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) #[violation] pub struct NewLineAfterSectionName { name: String, @@ -95,6 +376,84 @@ impl AlwaysAutofixableViolation for NewLineAfterSectionName { } } +/// ## What it does +/// Checks for section headers in docstrings that are not followed by +/// underlines. +/// +/// ## Why is this bad? +/// Multi-line docstrings are typically composed of a summary line, followed by +/// a blank line, followed by a series of sections, each with a section header +/// and a section body. +/// +/// Some docstring formats (like reStructuredText) use underlines to separate +/// section bodies from section headers. +/// +/// This rule is enabled when using the `numpy` convention, and disabled +/// when using the `google` or `pep257` conventions. +/// +/// ## Example +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Parameters +/// +/// distance : float +/// Distance traveled. +/// time : float +/// Time spent traveling. +/// +/// Returns +/// +/// float +/// Speed as distance divided by time. +/// +/// Raises +/// +/// FasterThanLightError +/// If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// Use instead: +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Parameters +/// ---------- +/// distance : float +/// Distance traveled. +/// time : float +/// Time spent traveling. +/// +/// Returns +/// ------- +/// float +/// Speed as distance divided by time. +/// +/// Raises +/// ------ +/// FasterThanLightError +/// If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// ## Options +/// - `pydocstyle.convention` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) #[violation] pub struct DashedUnderlineAfterSection { name: String, @@ -113,6 +472,90 @@ impl AlwaysAutofixableViolation for DashedUnderlineAfterSection { } } +/// ## What it does +/// Checks for section underlines in docstrings that are not on the line +/// immediately following the section name. +/// +/// ## Why is this bad? +/// Multi-line docstrings are typically composed of a summary line, followed by +/// a blank line, followed by a series of sections, each with a section header +/// and a section body. +/// +/// Some docstring formats (like reStructuredText) use underlines to separate +/// section bodies from section headers. +/// +/// When present, section underlines should be positioned on the line +/// immediately following the section header. +/// +/// This rule is enabled when using the `numpy` convention, and disabled +/// when using the `google` or `pep257` conventions. +/// +/// ## Example +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Parameters +/// +/// ---------- +/// distance : float +/// Distance traveled. +/// time : float +/// Time spent traveling. +/// +/// Returns +/// +/// ------- +/// float +/// Speed as distance divided by time. +/// +/// Raises +/// +/// ------ +/// FasterThanLightError +/// If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// Use instead: +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Parameters +/// ---------- +/// distance : float +/// Distance traveled. +/// time : float +/// Time spent traveling. +/// +/// Returns +/// ------- +/// float +/// Speed as distance divided by time. +/// +/// Raises +/// ------ +/// FasterThanLightError +/// If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// ## Options +/// - `pydocstyle.convention` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) #[violation] pub struct SectionUnderlineAfterName { name: String, @@ -131,6 +574,87 @@ impl AlwaysAutofixableViolation for SectionUnderlineAfterName { } } +/// ## What it does +/// Checks for section underlines in docstrings that do not match the length of +/// the corresponding section header. +/// +/// ## Why is this bad? +/// Multi-line docstrings are typically composed of a summary line, followed by +/// a blank line, followed by a series of sections, each with a section header +/// and a section body. +/// +/// Some docstring formats (like reStructuredText) use underlines to separate +/// section bodies from section headers. +/// +/// When present, section underlines should match the length of the +/// corresponding section header. +/// +/// This rule is enabled when using the `numpy` convention, and disabled +/// when using the `google` or `pep257` conventions. +/// +/// ## Example +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Parameters +/// --- +/// distance : float +/// Distance traveled. +/// time : float +/// Time spent traveling. +/// +/// Returns +/// --- +/// float +/// Speed as distance divided by time. +/// +/// Raises +/// --- +/// FasterThanLightError +/// If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// Use instead: +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Parameters +/// ---------- +/// distance : float +/// Distance traveled. +/// time : float +/// Time spent traveling. +/// +/// Returns +/// ------- +/// float +/// Speed as distance divided by time. +/// +/// Raises +/// ------ +/// FasterThanLightError +/// If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// ## Options +/// - `pydocstyle.convention` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) #[violation] pub struct SectionUnderlineMatchesSectionLength { name: String, @@ -149,6 +673,83 @@ impl AlwaysAutofixableViolation for SectionUnderlineMatchesSectionLength { } } +/// ## What it does +/// Checks for docstring sections that are not separated by a single blank +/// line. +/// +/// ## Why is this bad? +/// Multi-line docstrings are typically composed of a summary line, followed by +/// a blank line, followed by a series of sections, each with a section header +/// and a section body. +/// +/// Docstring sections should be separated by a blank line, for consistency and +/// compatibility with documentation tooling. +/// +/// This rule is enabled when using the `numpy` and `google` conventions, and +/// disabled when using the `pep257` convention. +/// +/// ## Example +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Parameters +/// ---------- +/// distance : float +/// Distance traveled. +/// time : float +/// Time spent traveling. +/// Returns +/// ------- +/// float +/// Speed as distance divided by time. +/// Raises +/// ------ +/// FasterThanLightError +/// If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// Use instead: +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Parameters +/// ---------- +/// distance : float +/// Distance traveled. +/// time : float +/// Time spent traveling. +/// +/// Returns +/// ------- +/// float +/// Speed as distance divided by time. +/// +/// Raises +/// ------ +/// FasterThanLightError +/// If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// ## Options +/// - `pydocstyle.convention` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Style Guide](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[violation] pub struct NoBlankLineAfterSection { name: String, @@ -167,6 +768,81 @@ impl AlwaysAutofixableViolation for NoBlankLineAfterSection { } } +/// ## What it does +/// Checks for docstring sections that are separated by a blank line. +/// +/// ## Why is this bad? +/// Multi-line docstrings are typically composed of a summary line, followed by +/// a blank line, followed by a series of sections, each with a section header +/// and a section body. +/// +/// Docstring sections should be separated by a blank line, for consistency and +/// compatibility with documentation tooling. +/// +/// This rule is enabled when using the `numpy` and `google` conventions, and +/// disabled when using the `pep257` convention. +/// +/// ## Example +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Parameters +/// ---------- +/// distance : float +/// Distance traveled. +/// time : float +/// Time spent traveling. +/// Returns +/// ------- +/// float +/// Speed as distance divided by time. +/// Raises +/// ------ +/// FasterThanLightError +/// If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// Use instead: +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Parameters +/// ---------- +/// distance : float +/// Distance traveled. +/// time : float +/// Time spent traveling. +/// +/// Returns +/// ------- +/// float +/// Speed as distance divided by time. +/// +/// Raises +/// ------ +/// FasterThanLightError +/// If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// ## Options +/// - `pydocstyle.convention` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) #[violation] pub struct NoBlankLineBeforeSection { name: String, @@ -185,6 +861,81 @@ impl AlwaysAutofixableViolation for NoBlankLineBeforeSection { } } +/// ## What it does +/// Checks for missing blank lines after the last section of a multi-line +/// docstring. +/// +/// ## Why is this bad? +/// Multi-line docstrings are typically composed of a summary line, followed by +/// a blank line, followed by a series of sections, each with a section header +/// and a section body. +/// +/// The last section in a docstring should be separated by a blank line, for +/// consistency and compatibility with documentation tooling. +/// +/// This rule is enabled when using the `numpy` convention, and disabled when +/// using the `pep257` and `google` conventions. +/// +/// ## Example +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Parameters +/// ---------- +/// distance : float +/// Distance traveled. +/// time : float +/// Time spent traveling. +/// Returns +/// ------- +/// float +/// Speed as distance divided by time. +/// Raises +/// ------ +/// FasterThanLightError +/// If speed is greater than the speed of light.""" +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// Use instead: +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Parameters +/// ---------- +/// distance : float +/// Distance traveled. +/// time : float +/// Time spent traveling. +/// +/// Returns +/// ------- +/// float +/// Speed as distance divided by time. +/// +/// Raises +/// ------ +/// FasterThanLightError +/// If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// ## Options +/// - `pydocstyle.convention` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) #[violation] pub struct BlankLineAfterLastSection { name: String, @@ -203,6 +954,79 @@ impl AlwaysAutofixableViolation for BlankLineAfterLastSection { } } +/// ## What it does +/// Checks for docstrings that contain empty sections. +/// +/// ## Why is this bad? +/// Multi-line docstrings are typically composed of a summary line, followed by +/// a blank line, followed by a series of sections, each with a section header +/// and a section body. +/// +/// Empty docstring sections are indicative of missing documentation. Empty +/// sections should either be removed or filled in with relevant documentation. +/// +/// ## Example +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Parameters +/// ---------- +/// distance : float +/// Distance traveled. +/// time : float +/// Time spent traveling. +/// +/// Returns +/// ------- +/// float +/// Speed as distance divided by time. +/// +/// Raises +/// ------ +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// Use instead: +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Parameters +/// ---------- +/// distance : float +/// Distance traveled. +/// time : float +/// Time spent traveling. +/// +/// Returns +/// ------- +/// float +/// Speed as distance divided by time. +/// +/// Raises +/// ------ +/// FasterThanLightError +/// If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// ## Options +/// - `pydocstyle.convention` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Style Guide](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[violation] pub struct EmptyDocstringSection { name: String, @@ -216,6 +1040,69 @@ impl Violation for EmptyDocstringSection { } } +/// ## What it does +/// Checks for docstring section headers that do not end with a colon. +/// +/// ## Why is this bad? +/// Multi-line docstrings are typically composed of a summary line, followed by +/// a blank line, followed by a series of sections, each with a section header +/// and a section body. +/// +/// In a docstring, each section header should end with a colon, for +/// consistency. +/// +/// This rule is enabled when using the `google` convention, and disabled when +/// using the `pep257` and `numpy` conventions. +/// +/// ## Example +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Args +/// distance: Distance traveled. +/// time: Time spent traveling. +/// +/// Returns +/// Speed as distance divided by time. +/// +/// Raises +/// FasterThanLightError: If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// Use instead: +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Args: +/// distance: Distance traveled. +/// time: Time spent traveling. +/// +/// Returns: +/// Speed as distance divided by time. +/// +/// Raises: +/// FasterThanLightError: If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// ## Options +/// - `pydocstyle.convention` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) +/// - [Google Style Guide](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[violation] pub struct SectionNameEndsInColon { name: String, @@ -234,6 +1121,70 @@ impl AlwaysAutofixableViolation for SectionNameEndsInColon { } } +/// ## What it does +/// Checks for function docstrings that do not include documentation for all +/// parameters in the function. +/// +/// ## Why is this bad? +/// Multi-line docstrings are typically composed of a summary line, followed by +/// a blank line, followed by a series of sections, each with a section header +/// and a section body. +/// +/// Function docstrings often include a section for function arguments, which +/// should include documentation for every argument. Undocumented arguments are +/// indicative of missing documentation. +/// +/// This rule is enabled when using the `google` convention, and disabled when +/// using the `pep257` and `numpy` conventions. +/// +/// ## Example +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Args: +/// distance: Distance traveled. +/// +/// Returns: +/// Speed as distance divided by time. +/// +/// Raises: +/// FasterThanLightError: If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// Use instead: +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Args: +/// distance: Distance traveled. +/// time: Time spent traveling. +/// +/// Returns: +/// Speed as distance divided by time. +/// +/// Raises: +/// FasterThanLightError: If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// ## Options +/// - `pydocstyle.convention` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) +/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[violation] pub struct UndocumentedParam { pub names: Vec, @@ -253,6 +1204,69 @@ impl Violation for UndocumentedParam { } } +/// ## What it does +/// Checks for docstring sections that contain blank lines between the section +/// header and the section body. +/// +/// ## Why is this bad? +/// Multi-line docstrings are typically composed of a summary line, followed by +/// a blank line, followed by a series of sections, each with a section header +/// and a section body. +/// +/// Docstring sections should not contain blank lines between the section header +/// and the section body, for consistency. +/// +/// ## Example +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Args: +/// +/// distance: Distance traveled. +/// time: Time spent traveling. +/// +/// Returns: +/// Speed as distance divided by time. +/// +/// Raises: +/// FasterThanLightError: If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// Use instead: +/// ```python +/// def calculate_speed(distance: float, time: float) -> float: +/// """Calculate speed as distance divided by time. +/// +/// Args: +/// distance: Distance traveled. +/// time: Time spent traveling. +/// +/// Returns: +/// Speed as distance divided by time. +/// +/// Raises: +/// FasterThanLightError: If speed is greater than the speed of light. +/// """ +/// try: +/// return distance / time +/// except ZeroDivisionError as exc: +/// raise FasterThanLightError from exc +/// ``` +/// +/// ## Options +/// - `pydocstyle.convention` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[violation] pub struct BlankLinesBetweenHeaderAndContent { name: String, @@ -376,8 +1390,7 @@ fn blanks_and_section_underline( let range = TextRange::new(context.following_range().start(), blank_lines_end); // Delete any blank lines between the header and the underline. - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_deletion(range))); + diagnostic.set_fix(Fix::automatic(Edit::range_deletion(range))); } checker.diagnostics.push(diagnostic); } @@ -405,8 +1418,7 @@ fn blanks_and_section_underline( "-".repeat(context.section_name().len()), checker.stylist.line_ending().as_str() ); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::replacement( + diagnostic.set_fix(Fix::automatic(Edit::replacement( content, blank_lines_end, non_blank_line.full_end(), @@ -432,8 +1444,7 @@ fn blanks_and_section_underline( ); // Replace the existing indentation with whitespace of the appropriate length. - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( clean_space(docstring.indentation), range, ))); @@ -472,8 +1483,7 @@ fn blanks_and_section_underline( ); if checker.patch(diagnostic.kind.rule()) { // Delete any blank lines between the header and content. - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::deletion( + diagnostic.set_fix(Fix::automatic(Edit::deletion( line_after_dashes.start(), blank_lines_after_dashes_end, ))); @@ -492,6 +1502,10 @@ fn blanks_and_section_underline( } } } else { + let equal_line_found = non_blank_line + .chars() + .all(|char| char.is_whitespace() || char == '='); + if checker.enabled(Rule::DashedUnderlineAfterSection) { let mut diagnostic = Diagnostic::new( DashedUnderlineAfterSection { @@ -507,11 +1521,23 @@ fn blanks_and_section_underline( clean_space(docstring.indentation), "-".repeat(context.section_name().len()), ); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::insertion( - content, - context.summary_range().end(), - ))); + if equal_line_found + && non_blank_line.trim_whitespace().len() == context.section_name().len() + { + // If an existing underline is an equal sign line of the appropriate length, + // replace it with a dashed line. + diagnostic.set_fix(Fix::automatic(Edit::replacement( + content, + context.summary_range().end(), + non_blank_line.end(), + ))); + } else { + // Otherwise, insert a dashed line after the section header. + diagnostic.set_fix(Fix::automatic(Edit::insertion( + content, + context.summary_range().end(), + ))); + } } checker.diagnostics.push(diagnostic); } @@ -527,8 +1553,7 @@ fn blanks_and_section_underline( let range = TextRange::new(context.following_range().start(), blank_lines_end); // Delete any blank lines between the header and content. - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_deletion(range))); + diagnostic.set_fix(Fix::automatic(Edit::range_deletion(range))); } checker.diagnostics.push(diagnostic); } @@ -553,8 +1578,7 @@ fn blanks_and_section_underline( "-".repeat(context.section_name().len()), ); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::insertion( + diagnostic.set_fix(Fix::automatic(Edit::insertion( content, context.summary_range().end(), ))); @@ -591,8 +1615,7 @@ fn common_section( // Replace the section title with the capitalized variant. This requires // locating the start and end of the section name. let section_range = context.section_name_range(); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( capitalized_section_name.to_string(), section_range, ))); @@ -615,8 +1638,7 @@ fn common_section( let content = clean_space(docstring.indentation); let fix_range = TextRange::at(context.range().start(), leading_space.text_len()); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(if content.is_empty() { + diagnostic.set_fix(Fix::automatic(if content.is_empty() { Edit::range_deletion(fix_range) } else { Edit::range_replacement(content, fix_range) @@ -639,8 +1661,7 @@ fn common_section( ); if checker.patch(diagnostic.kind.rule()) { // Add a newline at the beginning of the next section. - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::insertion( + diagnostic.set_fix(Fix::automatic(Edit::insertion( line_end.to_string(), next.range().start(), ))); @@ -657,8 +1678,7 @@ fn common_section( ); if checker.patch(diagnostic.kind.rule()) { // Add a newline after the section. - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::insertion( + diagnostic.set_fix(Fix::automatic(Edit::insertion( format!("{}{}", line_end, docstring.indentation), context.range().end(), ))); @@ -678,8 +1698,7 @@ fn common_section( ); if checker.patch(diagnostic.kind.rule()) { // Add a blank line before the section. - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::insertion( + diagnostic.set_fix(Fix::automatic(Edit::insertion( line_end.to_string(), context.range().start(), ))); @@ -696,37 +1715,41 @@ fn missing_args(checker: &mut Checker, docstring: &Docstring, docstrings_args: & kind: MemberKind::Function | MemberKind::NestedFunction | MemberKind::Method, stmt, .. - }) = docstring.definition else { + }) = docstring.definition + else { return; }; - let ( - Stmt::FunctionDef(ast::StmtFunctionDef { - args: arguments, .. - }) - | Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { - args: arguments, .. - }) - ) = stmt else { + let (Stmt::FunctionDef(ast::StmtFunctionDef { + args: arguments, .. + }) + | Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { + args: arguments, .. + })) = stmt + else { return; }; // Look for arguments that weren't included in the docstring. let mut missing_arg_names: FxHashSet = FxHashSet::default(); - for arg in arguments + for ArgWithDefault { + def, + default: _, + range: _, + } in arguments .posonlyargs .iter() - .chain(arguments.args.iter()) - .chain(arguments.kwonlyargs.iter()) + .chain(&arguments.args) + .chain(&arguments.kwonlyargs) .skip( // If this is a non-static method, skip `cls` or `self`. usize::from( docstring.definition.is_method() - && !is_staticmethod(checker.semantic_model(), cast::decorator_list(stmt)), + && !is_staticmethod(cast::decorator_list(stmt), checker.semantic()), ), ) { - let arg_name = arg.arg.as_str(); + let arg_name = def.arg.as_str(); if !arg_name.starts_with('_') && !docstrings_args.contains(arg_name) { missing_arg_names.insert(arg_name.to_string()); } @@ -759,7 +1782,7 @@ fn missing_args(checker: &mut Checker, docstring: &Docstring, docstrings_args: & let names = missing_arg_names.into_iter().sorted().collect(); checker.diagnostics.push(Diagnostic::new( UndocumentedParam { names }, - identifier_range(stmt, checker.locator), + stmt.identifier(), )); } } @@ -878,8 +1901,7 @@ fn numpy_section( ); if checker.patch(diagnostic.kind.rule()) { let section_range = context.section_name_range(); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_deletion(TextRange::at( + diagnostic.set_fix(Fix::automatic(Edit::range_deletion(TextRange::at( section_range.end(), suffix.text_len(), )))); @@ -916,8 +1938,7 @@ fn google_section( if checker.patch(diagnostic.kind.rule()) { // Replace the suffix. let section_name_range = context.section_name_range(); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( ":".to_string(), TextRange::at(section_name_range.end(), suffix.text_len()), ))); diff --git a/crates/ruff/src/rules/pydocstyle/rules/starts_with_this.rs b/crates/ruff/src/rules/pydocstyle/rules/starts_with_this.rs index efe015b802..4e73aa0ed3 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/starts_with_this.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/starts_with_this.rs @@ -5,6 +5,39 @@ use crate::checkers::ast::Checker; use crate::docstrings::Docstring; use crate::rules::pydocstyle::helpers::normalize_word; +/// ## What it does +/// Checks for docstrings that start with `This`. +/// +/// ## Why is this bad? +/// [PEP 257] recommends that the first line of a docstring be written in the +/// imperative mood, for consistency. +/// +/// Hint: to rewrite the docstring in the imperative, phrase the first line as +/// if it were a command. +/// +/// This rule may not apply to all projects; its applicability is a matter of +/// convention. By default, this rule is enabled when using the `numpy` +/// convention,, and disabled when using the `google` and `pep257` conventions. +/// +/// ## Example +/// ```python +/// def average(values: list[float]) -> float: +/// """This function returns the mean of the given values.""" +/// ``` +/// +/// Use instead: +/// ```python +/// def average(values: list[float]) -> float: +/// """Return the mean of the given values.""" +/// ``` +/// +/// ## Options +/// - `pydocstyle.convention` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// +/// [PEP 257]: https://peps.python.org/pep-0257/ #[violation] pub struct DocstringStartsWithThis; @@ -25,7 +58,7 @@ pub(crate) fn starts_with_this(checker: &mut Checker, docstring: &Docstring) { } let Some(first_word) = trimmed.split(' ').next() else { - return + return; }; if normalize_word(first_word) != "this" { return; diff --git a/crates/ruff/src/rules/pydocstyle/rules/triple_quotes.rs b/crates/ruff/src/rules/pydocstyle/rules/triple_quotes.rs index 5cf020ce38..caa7d59872 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/triple_quotes.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/triple_quotes.rs @@ -1,36 +1,77 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::source_code::Quote; use crate::checkers::ast::Checker; use crate::docstrings::Docstring; +/// ## What it does +/// Checks for docstrings that use `'''triple single quotes'''` instead of +/// `"""triple double quotes"""`. +/// +/// ## Why is this bad? +/// [PEP 257](https://peps.python.org/pep-0257/#what-is-a-docstring) recommends +/// the use of `"""triple double quotes"""` for docstrings, to ensure +/// consistency. +/// +/// ## Example +/// ```python +/// def kos_root(): +/// '''Return the pathname of the KOS root directory.''' +/// ``` +/// +/// Use instead: +/// ```python +/// def kos_root(): +/// """Return the pathname of the KOS root directory.""" +/// ``` +/// +/// ## References +/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) +/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) +/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) #[violation] -pub struct TripleSingleQuotes; +pub struct TripleSingleQuotes { + expected_quote: Quote, +} impl Violation for TripleSingleQuotes { #[derive_message_formats] fn message(&self) -> String { - format!(r#"Use triple double quotes `"""`"#) + let TripleSingleQuotes { expected_quote } = self; + match expected_quote { + Quote::Double => format!(r#"Use triple double quotes `"""`"#), + Quote::Single => format!(r#"Use triple single quotes `'''`"#), + } } } /// D300 pub(crate) fn triple_quotes(checker: &mut Checker, docstring: &Docstring) { - let body = docstring.body(); + let leading_quote = docstring.leading_quote(); - let leading_quote = docstring.leading_quote().to_ascii_lowercase(); - - let starts_with_triple = if body.contains("\"\"\"") { - matches!(leading_quote.as_str(), "'''" | "u'''" | "r'''" | "ur'''") + let expected_quote = if docstring.body().contains("\"\"\"") { + Quote::Single } else { - matches!( - leading_quote.as_str(), - "\"\"\"" | "u\"\"\"" | "r\"\"\"" | "ur\"\"\"" - ) + Quote::Double }; - if !starts_with_triple { - checker - .diagnostics - .push(Diagnostic::new(TripleSingleQuotes, docstring.range())); + + match expected_quote { + Quote::Single => { + if !leading_quote.ends_with("'''") { + checker.diagnostics.push(Diagnostic::new( + TripleSingleQuotes { expected_quote }, + docstring.range(), + )); + } + } + Quote::Double => { + if !leading_quote.ends_with("\"\"\"") { + checker.diagnostics.push(Diagnostic::new( + TripleSingleQuotes { expected_quote }, + docstring.range(), + )); + } + } } } diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D201_D.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D201_D.py.snap index 48fb3a6a40..9f8287aa46 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D201_D.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D201_D.py.snap @@ -10,7 +10,7 @@ D.py:137:5: D201 [*] No blank lines allowed before function docstring (found 1) | = help: Remove blank line(s) before function docstring -ℹ Suggested fix +ℹ Fix 133 133 | 134 134 | @expect('D201: No blank lines allowed before function docstring (found 1)') 135 135 | def leading_space(): @@ -30,7 +30,7 @@ D.py:151:5: D201 [*] No blank lines allowed before function docstring (found 1) | = help: Remove blank line(s) before function docstring -ℹ Suggested fix +ℹ Fix 147 147 | @expect('D201: No blank lines allowed before function docstring (found 1)') 148 148 | @expect('D202: No blank lines allowed after function docstring (found 1)') 149 149 | def trailing_and_leading_space(): @@ -52,7 +52,7 @@ D.py:546:5: D201 [*] No blank lines allowed before function docstring (found 1) | = help: Remove blank line(s) before function docstring -ℹ Suggested fix +ℹ Fix 542 542 | @expect('D201: No blank lines allowed before function docstring (found 1)') 543 543 | @expect('D213: Multi-line docstring summary should start at the second line') 544 544 | def multiline_leading_space(): @@ -76,7 +76,7 @@ D.py:568:5: D201 [*] No blank lines allowed before function docstring (found 1) | = help: Remove blank line(s) before function docstring -ℹ Suggested fix +ℹ Fix 564 564 | @expect('D202: No blank lines allowed after function docstring (found 1)') 565 565 | @expect('D213: Multi-line docstring summary should start at the second line') 566 566 | def multiline_trailing_and_leading_space(): diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D202_D.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D202_D.py.snap index 94d9380103..e620ddc640 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D202_D.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D202_D.py.snap @@ -12,7 +12,7 @@ D.py:142:5: D202 [*] No blank lines allowed after function docstring (found 1) | = help: Remove blank line(s) after function docstring -ℹ Suggested fix +ℹ Fix 140 140 | @expect('D202: No blank lines allowed after function docstring (found 1)') 141 141 | def trailing_space(): 142 142 | """Leading space.""" @@ -32,7 +32,7 @@ D.py:151:5: D202 [*] No blank lines allowed after function docstring (found 1) | = help: Remove blank line(s) after function docstring -ℹ Suggested fix +ℹ Fix 149 149 | def trailing_and_leading_space(): 150 150 | 151 151 | """Trailing and leading space.""" @@ -56,7 +56,7 @@ D.py:555:5: D202 [*] No blank lines allowed after function docstring (found 1) | = help: Remove blank line(s) after function docstring -ℹ Suggested fix +ℹ Fix 556 556 | 557 557 | More content. 558 558 | """ @@ -80,7 +80,7 @@ D.py:568:5: D202 [*] No blank lines allowed after function docstring (found 1) | = help: Remove blank line(s) after function docstring -ℹ Suggested fix +ℹ Fix 569 569 | 570 570 | More content. 571 571 | """ diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D202_D202.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D202_D202.py.snap index 543bc5e331..b05850ca5c 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D202_D202.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D202_D202.py.snap @@ -10,7 +10,7 @@ D202.py:57:5: D202 [*] No blank lines allowed after function docstring (found 2) | = help: Remove blank line(s) after function docstring -ℹ Suggested fix +ℹ Fix 55 55 | # D202 56 56 | def outer(): 57 57 | """This is a docstring.""" @@ -29,7 +29,7 @@ D202.py:68:5: D202 [*] No blank lines allowed after function docstring (found 2) | = help: Remove blank line(s) after function docstring -ℹ Suggested fix +ℹ Fix 66 66 | # D202 67 67 | def outer(): 68 68 | """This is a docstring.""" @@ -50,7 +50,7 @@ D202.py:80:5: D202 [*] No blank lines allowed after function docstring (found 1) | = help: Remove blank line(s) after function docstring -ℹ Suggested fix +ℹ Fix 78 78 | # D202 79 79 | def outer(): 80 80 | """This is a docstring.""" diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D203_D.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D203_D.py.snap index 62629b10ed..154bcd0e52 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D203_D.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D203_D.py.snap @@ -9,7 +9,7 @@ D.py:161:5: D203 [*] 1 blank line required before class docstring | = help: Insert 1 blank line before class docstring -ℹ Suggested fix +ℹ Fix 158 158 | 159 159 | 160 160 | class LeadingSpaceMissing: @@ -27,7 +27,7 @@ D.py:192:5: D203 [*] 1 blank line required before class docstring | = help: Insert 1 blank line before class docstring -ℹ Suggested fix +ℹ Fix 189 189 | 190 190 | 191 191 | class LeadingAndTrailingSpaceMissing: @@ -54,7 +54,7 @@ D.py:526:5: D203 [*] 1 blank line required before class docstring | = help: Insert 1 blank line before class docstring -ℹ Suggested fix +ℹ Fix 523 523 | # This is reproducing a bug where AttributeError is raised when parsing class 524 524 | # parameters as functions for Google / Numpy conventions. 525 525 | class Blah: # noqa: D203,D213 diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D204_D.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D204_D.py.snap index 9e7a6ecd8c..3d7025563d 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D204_D.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D204_D.py.snap @@ -11,7 +11,7 @@ D.py:181:5: D204 [*] 1 blank line required after class docstring | = help: Insert 1 blank line after class docstring -ℹ Suggested fix +ℹ Fix 179 179 | class TrailingSpace: 180 180 | 181 181 | """TrailingSpace.""" @@ -29,7 +29,7 @@ D.py:192:5: D204 [*] 1 blank line required after class docstring | = help: Insert 1 blank line after class docstring -ℹ Suggested fix +ℹ Fix 190 190 | 191 191 | class LeadingAndTrailingSpaceMissing: 192 192 | """Leading and trailing space missing.""" diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D205_D.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D205_D.py.snap index df41898d6d..266342bb41 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D205_D.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D205_D.py.snap @@ -29,7 +29,7 @@ D.py:210:5: D205 [*] 1 blank line required between summary line and description | = help: Insert single blank line -ℹ Suggested fix +ℹ Fix 209 209 | def multi_line_two_separating_blanks(): 210 210 | """Summary. 211 211 | diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D207_D.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D207_D.py.snap index 443c759fb9..9c86c51d99 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D207_D.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D207_D.py.snap @@ -12,7 +12,7 @@ D.py:232:1: D207 [*] Docstring is under-indented | = help: Increase indentation -ℹ Suggested fix +ℹ Fix 229 229 | def asdfsdf(): 230 230 | """Summary. 231 231 | @@ -31,7 +31,7 @@ D.py:244:1: D207 [*] Docstring is under-indented | = help: Increase indentation -ℹ Suggested fix +ℹ Fix 241 241 | 242 242 | Description. 243 243 | @@ -51,7 +51,7 @@ D.py:440:1: D207 [*] Docstring is under-indented | = help: Increase indentation -ℹ Suggested fix +ℹ Fix 437 437 | @expect('D213: Multi-line docstring summary should start at the second line') 438 438 | def docstring_start_in_same_line(): """First Line. 439 439 | @@ -69,7 +69,7 @@ D.py:441:1: D207 [*] Docstring is under-indented | = help: Increase indentation -ℹ Suggested fix +ℹ Fix 438 438 | def docstring_start_in_same_line(): """First Line. 439 439 | 440 440 | Second Line diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D208_D.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D208_D.py.snap index 66f91b47bb..7fe7b24f84 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D208_D.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D208_D.py.snap @@ -12,7 +12,7 @@ D.py:252:1: D208 [*] Docstring is over-indented | = help: Remove over-indentation -ℹ Suggested fix +ℹ Fix 249 249 | def asdfsdsdf24(): 250 250 | """Summary. 251 251 | @@ -31,7 +31,7 @@ D.py:264:1: D208 [*] Docstring is over-indented | = help: Remove over-indentation -ℹ Suggested fix +ℹ Fix 261 261 | 262 262 | Description. 263 263 | @@ -52,7 +52,7 @@ D.py:272:1: D208 [*] Docstring is over-indented | = help: Remove over-indentation -ℹ Suggested fix +ℹ Fix 269 269 | def asdfsdfsdsdsdfsdf24(): 270 270 | """Summary. 271 271 | diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D209_D.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D209_D.py.snap index 7861e2dbc6..01e3838be0 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D209_D.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D209_D.py.snap @@ -13,7 +13,7 @@ D.py:281:5: D209 [*] Multi-line docstring closing quotes should be on a separate | = help: Move closing quotes to new line -ℹ Suggested fix +ℹ Fix 280 280 | def asdfljdf24(): 281 281 | """Summary. 282 282 | @@ -36,7 +36,7 @@ D.py:588:5: D209 [*] Multi-line docstring closing quotes should be on a separate | = help: Move closing quotes to new line -ℹ Suggested fix +ℹ Fix 587 587 | def asdfljdjgf24(): 588 588 | """Summary. 589 589 | diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D210_D.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D210_D.py.snap index ca75b0221e..8e3a805a4d 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D210_D.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D210_D.py.snap @@ -10,7 +10,7 @@ D.py:288:5: D210 [*] No whitespaces allowed surrounding docstring text | = help: Trim surrounding whitespace -ℹ Suggested fix +ℹ Fix 285 285 | 286 286 | @expect('D210: No whitespaces allowed surrounding docstring text') 287 287 | def endswith(): @@ -29,7 +29,7 @@ D.py:293:5: D210 [*] No whitespaces allowed surrounding docstring text | = help: Trim surrounding whitespace -ℹ Suggested fix +ℹ Fix 290 290 | 291 291 | @expect('D210: No whitespaces allowed surrounding docstring text') 292 292 | def around(): @@ -52,7 +52,7 @@ D.py:299:5: D210 [*] No whitespaces allowed surrounding docstring text | = help: Trim surrounding whitespace -ℹ Suggested fix +ℹ Fix 296 296 | @expect('D210: No whitespaces allowed surrounding docstring text') 297 297 | @expect('D213: Multi-line docstring summary should start at the second line') 298 298 | def multiline(): diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D211_D.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D211_D.py.snap index a8e21c2805..729ea91914 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D211_D.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D211_D.py.snap @@ -10,7 +10,7 @@ D.py:170:5: D211 [*] No blank lines allowed before class docstring | = help: Remove blank line(s) before class docstring -ℹ Suggested fix +ℹ Fix 166 166 | 167 167 | 168 168 | class WithLeadingSpace: @@ -29,7 +29,7 @@ D.py:181:5: D211 [*] No blank lines allowed before class docstring | = help: Remove blank line(s) before class docstring -ℹ Suggested fix +ℹ Fix 177 177 | 178 178 | 179 179 | class TrailingSpace: diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D212_D.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D212_D.py.snap index 1177bbd802..5d2ea74fd8 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D212_D.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D212_D.py.snap @@ -13,7 +13,7 @@ D.py:129:5: D212 [*] Multi-line docstring summary should start at the first line | = help: Remove whitespace after opening quotes -ℹ Suggested fix +ℹ Fix 126 126 | '(found 3)') 127 127 | @expect('D212: Multi-line docstring summary should start at the first line') 128 128 | def asdlkfasd(): @@ -36,7 +36,7 @@ D.py:597:5: D212 [*] Multi-line docstring summary should start at the first line | = help: Remove whitespace after opening quotes -ℹ Suggested fix +ℹ Fix 594 594 | '(found 3)') 595 595 | @expect('D212: Multi-line docstring summary should start at the first line') 596 596 | def one_liner(): @@ -60,7 +60,7 @@ D.py:624:5: D212 [*] Multi-line docstring summary should start at the first line | = help: Remove whitespace after opening quotes -ℹ Suggested fix +ℹ Fix 621 621 | '(found 3)') 622 622 | @expect('D212: Multi-line docstring summary should start at the first line') 623 623 | def one_liner(): diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D213_D.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D213_D.py.snap index 6651cc1aec..070f70b2c8 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D213_D.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D213_D.py.snap @@ -14,7 +14,7 @@ D.py:200:5: D213 [*] Multi-line docstring summary should start at the second lin | = help: Insert line break and indentation after opening quotes -ℹ Suggested fix +ℹ Fix 197 197 | '(found 0)') 198 198 | @expect('D213: Multi-line docstring summary should start at the second line') 199 199 | def multi_line_zero_separating_blanks(): @@ -40,7 +40,7 @@ D.py:210:5: D213 [*] Multi-line docstring summary should start at the second lin | = help: Insert line break and indentation after opening quotes -ℹ Suggested fix +ℹ Fix 207 207 | '(found 2)') 208 208 | @expect('D213: Multi-line docstring summary should start at the second line') 209 209 | def multi_line_two_separating_blanks(): @@ -65,7 +65,7 @@ D.py:220:5: D213 [*] Multi-line docstring summary should start at the second lin | = help: Insert line break and indentation after opening quotes -ℹ Suggested fix +ℹ Fix 217 217 | 218 218 | @expect('D213: Multi-line docstring summary should start at the second line') 219 219 | def multi_line_one_separating_blanks(): @@ -90,7 +90,7 @@ D.py:230:5: D213 [*] Multi-line docstring summary should start at the second lin | = help: Insert line break and indentation after opening quotes -ℹ Suggested fix +ℹ Fix 227 227 | @expect('D207: Docstring is under-indented') 228 228 | @expect('D213: Multi-line docstring summary should start at the second line') 229 229 | def asdfsdf(): @@ -115,7 +115,7 @@ D.py:240:5: D213 [*] Multi-line docstring summary should start at the second lin | = help: Insert line break and indentation after opening quotes -ℹ Suggested fix +ℹ Fix 237 237 | @expect('D207: Docstring is under-indented') 238 238 | @expect('D213: Multi-line docstring summary should start at the second line') 239 239 | def asdsdfsdffsdf(): @@ -140,7 +140,7 @@ D.py:250:5: D213 [*] Multi-line docstring summary should start at the second lin | = help: Insert line break and indentation after opening quotes -ℹ Suggested fix +ℹ Fix 247 247 | @expect('D208: Docstring is over-indented') 248 248 | @expect('D213: Multi-line docstring summary should start at the second line') 249 249 | def asdfsdsdf24(): @@ -165,7 +165,7 @@ D.py:260:5: D213 [*] Multi-line docstring summary should start at the second lin | = help: Insert line break and indentation after opening quotes -ℹ Suggested fix +ℹ Fix 257 257 | @expect('D208: Docstring is over-indented') 258 258 | @expect('D213: Multi-line docstring summary should start at the second line') 259 259 | def asdfsdsdfsdf24(): @@ -190,7 +190,7 @@ D.py:270:5: D213 [*] Multi-line docstring summary should start at the second lin | = help: Insert line break and indentation after opening quotes -ℹ Suggested fix +ℹ Fix 267 267 | @expect('D208: Docstring is over-indented') 268 268 | @expect('D213: Multi-line docstring summary should start at the second line') 269 269 | def asdfsdfsdsdsdfsdf24(): @@ -213,7 +213,7 @@ D.py:281:5: D213 [*] Multi-line docstring summary should start at the second lin | = help: Insert line break and indentation after opening quotes -ℹ Suggested fix +ℹ Fix 278 278 | 'line') 279 279 | @expect('D213: Multi-line docstring summary should start at the second line') 280 280 | def asdfljdf24(): @@ -237,7 +237,7 @@ D.py:299:5: D213 [*] Multi-line docstring summary should start at the second lin | = help: Insert line break and indentation after opening quotes -ℹ Suggested fix +ℹ Fix 296 296 | @expect('D210: No whitespaces allowed surrounding docstring text') 297 297 | @expect('D213: Multi-line docstring summary should start at the second line') 298 298 | def multiline(): @@ -263,7 +263,7 @@ D.py:343:5: D213 [*] Multi-line docstring summary should start at the second lin | = help: Insert line break and indentation after opening quotes -ℹ Suggested fix +ℹ Fix 340 340 | 341 341 | @expect('D213: Multi-line docstring summary should start at the second line') 342 342 | def exceptions_of_D301(): @@ -288,7 +288,7 @@ D.py:383:5: D213 [*] Multi-line docstring summary should start at the second lin | = help: Insert line break and indentation after opening quotes -ℹ Suggested fix +ℹ Fix 380 380 | 381 381 | @expect('D213: Multi-line docstring summary should start at the second line') 382 382 | def new_209(): @@ -313,7 +313,7 @@ D.py:392:5: D213 [*] Multi-line docstring summary should start at the second lin | = help: Insert line break and indentation after opening quotes -ℹ Suggested fix +ℹ Fix 389 389 | 390 390 | @expect('D213: Multi-line docstring summary should start at the second line') 391 391 | def old_209(): @@ -337,7 +337,7 @@ D.py:438:37: D213 [*] Multi-line docstring summary should start at the second li | = help: Insert line break and indentation after opening quotes -ℹ Suggested fix +ℹ Fix 435 435 | 436 436 | @expect("D207: Docstring is under-indented") 437 437 | @expect('D213: Multi-line docstring summary should start at the second line') @@ -362,7 +362,7 @@ D.py:450:5: D213 [*] Multi-line docstring summary should start at the second lin | = help: Insert line break and indentation after opening quotes -ℹ Suggested fix +ℹ Fix 447 447 | 448 448 | @expect('D213: Multi-line docstring summary should start at the second line') 449 449 | def a_following_valid_function(x=None): @@ -391,7 +391,7 @@ D.py:526:5: D213 [*] Multi-line docstring summary should start at the second lin | = help: Insert line break and indentation after opening quotes -ℹ Suggested fix +ℹ Fix 523 523 | # This is reproducing a bug where AttributeError is raised when parsing class 524 524 | # parameters as functions for Google / Numpy conventions. 525 525 | class Blah: # noqa: D203,D213 @@ -415,7 +415,7 @@ D.py:546:5: D213 [*] Multi-line docstring summary should start at the second lin | = help: Insert line break and indentation after opening quotes -ℹ Suggested fix +ℹ Fix 543 543 | @expect('D213: Multi-line docstring summary should start at the second line') 544 544 | def multiline_leading_space(): 545 545 | @@ -441,7 +441,7 @@ D.py:555:5: D213 [*] Multi-line docstring summary should start at the second lin | = help: Insert line break and indentation after opening quotes -ℹ Suggested fix +ℹ Fix 552 552 | @expect('D202: No blank lines allowed after function docstring (found 1)') 553 553 | @expect('D213: Multi-line docstring summary should start at the second line') 554 554 | def multiline_trailing_space(): @@ -467,7 +467,7 @@ D.py:568:5: D213 [*] Multi-line docstring summary should start at the second lin | = help: Insert line break and indentation after opening quotes -ℹ Suggested fix +ℹ Fix 565 565 | @expect('D213: Multi-line docstring summary should start at the second line') 566 566 | def multiline_trailing_and_leading_space(): 567 567 | @@ -490,7 +490,7 @@ D.py:588:5: D213 [*] Multi-line docstring summary should start at the second lin | = help: Insert line break and indentation after opening quotes -ℹ Suggested fix +ℹ Fix 585 585 | 'line') 586 586 | @expect('D213: Multi-line docstring summary should start at the second line') 587 587 | def asdfljdjgf24(): @@ -513,7 +513,7 @@ D.py:606:5: D213 [*] Multi-line docstring summary should start at the second lin | = help: Insert line break and indentation after opening quotes -ℹ Suggested fix +ℹ Fix 603 603 | '(found 3)') 604 604 | @expect('D212: Multi-line docstring summary should start at the first line') 605 605 | def one_liner(): @@ -536,7 +536,7 @@ D.py:615:5: D213 [*] Multi-line docstring summary should start at the second lin | = help: Insert line break and indentation after opening quotes -ℹ Suggested fix +ℹ Fix 612 612 | '(found 3)') 613 613 | @expect('D212: Multi-line docstring summary should start at the first line') 614 614 | def one_liner(): diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D214_D214_module.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D214_D214_module.py.snap index bc822b0bb6..be8ef02628 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D214_D214_module.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D214_D214_module.py.snap @@ -19,7 +19,7 @@ D214_module.py:1:1: D214 [*] Section is over-indented ("Returns") | = help: Remove over-indentation from "Returns" -ℹ Suggested fix +ℹ Fix 1 1 | """A module docstring with D214 violations 2 2 | 3 |- Returns @@ -46,7 +46,7 @@ D214_module.py:1:1: D214 [*] Section is over-indented ("Args") | = help: Remove over-indentation from "Args" -ℹ Suggested fix +ℹ Fix 4 4 | ----- 5 5 | valid returns 6 6 | diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D214_sections.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D214_sections.py.snap index b4e381024b..a1395220a6 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D214_sections.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D214_sections.py.snap @@ -17,7 +17,7 @@ sections.py:144:5: D214 [*] Section is over-indented ("Returns") | = help: Remove over-indentation from "Returns" -ℹ Suggested fix +ℹ Fix 143 143 | def section_overindented(): # noqa: D416 144 144 | """Toggle the gizmo. 145 145 | diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D215_sections.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D215_sections.py.snap index 02edf6f0dd..4c99c69a03 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D215_sections.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D215_sections.py.snap @@ -17,7 +17,7 @@ sections.py:156:5: D215 [*] Section underline is over-indented ("Returns") | = help: Remove over-indentation from "Returns" underline -ℹ Suggested fix +ℹ Fix 156 156 | """Toggle the gizmo. 157 157 | 158 158 | Returns @@ -41,7 +41,7 @@ sections.py:170:5: D215 [*] Section underline is over-indented ("Returns") | = help: Remove over-indentation from "Returns" underline -ℹ Suggested fix +ℹ Fix 170 170 | """Toggle the gizmo. 171 171 | 172 172 | Returns diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D301_D301.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D301_D301.py.snap new file mode 100644 index 0000000000..9f8919505c --- /dev/null +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D301_D301.py.snap @@ -0,0 +1,18 @@ +--- +source: crates/ruff/src/rules/pydocstyle/mod.rs +--- +D301.py:2:5: D301 Use `r"""` if any backslashes in a docstring + | +1 | def double_quotes_backslash(): +2 | """Sum\\mary.""" + | ^^^^^^^^^^^^^^^^ D301 + | + +D301.py:10:5: D301 Use `r"""` if any backslashes in a docstring + | + 9 | def double_quotes_backslash_uppercase(): +10 | R"""Sum\\mary.""" + | ^^^^^^^^^^^^^^^^^ D301 + | + + diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D403_D403.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D403_D403.py.snap index 7203275eba..79e1522fd3 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D403_D403.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D403_D403.py.snap @@ -11,7 +11,7 @@ D403.py:2:5: D403 [*] First word of the first line should be capitalized: `this` | = help: Capitalize `this` to `This` -ℹ Suggested fix +ℹ Fix 1 1 | def bad_function(): 2 |- """this docstring is not capitalized""" 2 |+ """This docstring is not capitalized""" diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D405_sections.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D405_sections.py.snap index 26914a769e..3b3ec3f1df 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D405_sections.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D405_sections.py.snap @@ -17,7 +17,7 @@ sections.py:17:5: D405 [*] Section name should be properly capitalized ("returns | = help: Capitalize "returns" -ℹ Suggested fix +ℹ Fix 16 16 | def not_capitalized(): # noqa: D416 17 17 | """Toggle the gizmo. 18 18 | @@ -51,7 +51,7 @@ sections.py:216:5: D405 [*] Section name should be properly capitalized ("Short | = help: Capitalize "Short summary" -ℹ Suggested fix +ℹ Fix 215 215 | def multiple_sections(): # noqa: D416 216 216 | """Toggle the gizmo. 217 217 | diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D406_sections.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D406_sections.py.snap index fb95343722..b87c43b472 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D406_sections.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D406_sections.py.snap @@ -17,7 +17,7 @@ sections.py:30:5: D406 [*] Section name should end with a newline ("Returns") | = help: Add newline after "Returns" -ℹ Suggested fix +ℹ Fix 29 29 | def superfluous_suffix(): # noqa: D416 30 30 | """Toggle the gizmo. 31 31 | @@ -51,7 +51,7 @@ sections.py:216:5: D406 [*] Section name should end with a newline ("Raises") | = help: Add newline after "Raises" -ℹ Suggested fix +ℹ Fix 224 224 | Returns 225 225 | ------ 226 226 | Many many wonderful things. diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D407_sections.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D407_sections.py.snap index 33128a959e..f0eac75133 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D407_sections.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D407_sections.py.snap @@ -16,7 +16,7 @@ sections.py:42:5: D407 [*] Missing dashed underline after section ("Returns") | = help: Add dashed line under "Returns" -ℹ Suggested fix +ℹ Fix 42 42 | """Toggle the gizmo. 43 43 | 44 44 | Returns @@ -39,7 +39,7 @@ sections.py:54:5: D407 [*] Missing dashed underline after section ("Returns") | = help: Add dashed line under "Returns" -ℹ Suggested fix +ℹ Fix 54 54 | """Toggle the gizmo. 55 55 | 56 56 | Returns @@ -60,7 +60,7 @@ sections.py:65:5: D407 [*] Missing dashed underline after section ("Returns") | = help: Add dashed line under "Returns" -ℹ Suggested fix +ℹ Fix 64 64 | def no_underline_and_no_newline(): # noqa: D416 65 65 | """Toggle the gizmo. 66 66 | @@ -95,7 +95,7 @@ sections.py:216:5: D407 [*] Missing dashed underline after section ("Raises") | = help: Add dashed line under "Raises" -ℹ Suggested fix +ℹ Fix 225 225 | ------ 226 226 | Many many wonderful things. 227 227 | Raises: @@ -124,7 +124,7 @@ sections.py:261:5: D407 [*] Missing dashed underline after section ("Args") | = help: Add dashed line under "Args" -ℹ Suggested fix +ℹ Fix 261 261 | """Toggle the gizmo. 262 262 | 263 263 | Args: @@ -153,7 +153,7 @@ sections.py:261:5: D407 [*] Missing dashed underline after section ("Returns") | = help: Add dashed line under "Returns" -ℹ Suggested fix +ℹ Fix 264 264 | note: A random string. 265 265 | 266 266 | Returns: @@ -182,7 +182,7 @@ sections.py:261:5: D407 [*] Missing dashed underline after section ("Raises") | = help: Add dashed line under "Raises" -ℹ Suggested fix +ℹ Fix 266 266 | Returns: 267 267 | 268 268 | Raises: @@ -206,7 +206,7 @@ sections.py:278:5: D407 [*] Missing dashed underline after section ("Args") | = help: Add dashed line under "Args" -ℹ Suggested fix +ℹ Fix 278 278 | """Toggle the gizmo. 279 279 | 280 280 | Args @@ -233,7 +233,7 @@ sections.py:293:9: D407 [*] Missing dashed underline after section ("Args") | = help: Add dashed line under "Args" -ℹ Suggested fix +ℹ Fix 295 295 | Will this work when referencing x? 296 296 | 297 297 | Args: @@ -257,7 +257,7 @@ sections.py:310:5: D407 [*] Missing dashed underline after section ("Args") | = help: Add dashed line under "Args" -ℹ Suggested fix +ℹ Fix 310 310 | """Toggle the gizmo. 311 311 | 312 312 | Args: @@ -283,7 +283,7 @@ sections.py:322:9: D407 [*] Missing dashed underline after section ("Args") | = help: Add dashed line under "Args" -ℹ Suggested fix +ℹ Fix 322 322 | """Test a valid args section. 323 323 | 324 324 | Args: @@ -309,7 +309,7 @@ sections.py:334:9: D407 [*] Missing dashed underline after section ("Args") | = help: Add dashed line under "Args" -ℹ Suggested fix +ℹ Fix 334 334 | """Test a valid args section. 335 335 | 336 336 | Args: @@ -336,7 +336,7 @@ sections.py:346:9: D407 [*] Missing dashed underline after section ("Args") | = help: Add dashed line under "Args" -ℹ Suggested fix +ℹ Fix 346 346 | """Test a valid args section. 347 347 | 348 348 | Args: @@ -362,7 +362,7 @@ sections.py:359:9: D407 [*] Missing dashed underline after section ("Args") | = help: Add dashed line under "Args" -ℹ Suggested fix +ℹ Fix 359 359 | """Test a valid args section. 360 360 | 361 361 | Args: @@ -388,7 +388,7 @@ sections.py:371:9: D407 [*] Missing dashed underline after section ("Args") | = help: Add dashed line under "Args" -ℹ Suggested fix +ℹ Fix 371 371 | """Test a valid args section. 372 372 | 373 373 | Args: @@ -418,7 +418,7 @@ sections.py:380:9: D407 [*] Missing dashed underline after section ("Args") | = help: Add dashed line under "Args" -ℹ Suggested fix +ℹ Fix 380 380 | """Do stuff. 381 381 | 382 382 | Args: @@ -444,7 +444,7 @@ sections.py:499:9: D407 [*] Missing dashed underline after section ("Args") | = help: Add dashed line under "Args" -ℹ Suggested fix +ℹ Fix 501 501 | Testing this incorrectly indented docstring. 502 502 | 503 503 | Args: @@ -453,4 +453,48 @@ sections.py:499:9: D407 [*] Missing dashed underline after section ("Args") 505 506 | 506 507 | """ +sections.py:519:5: D407 [*] Missing dashed underline after section ("Parameters") + | +518 | def replace_equals_with_dash(): +519 | """Equal length equals should be replaced with dashes. + | _____^ +520 | | +521 | | Parameters +522 | | ========== +523 | | """ + | |_______^ D407 + | + = help: Add dashed line under "Parameters" + +ℹ Fix +519 519 | """Equal length equals should be replaced with dashes. +520 520 | +521 521 | Parameters +522 |- ========== + 522 |+ ---------- +523 523 | """ +524 524 | +525 525 | + +sections.py:527:5: D407 [*] Missing dashed underline after section ("Parameters") + | +526 | def replace_equals_with_dash2(): +527 | """Here, the length of equals is not the same. + | _____^ +528 | | +529 | | Parameters +530 | | =========== +531 | | """ + | |_______^ D407 + | + = help: Add dashed line under "Parameters" + +ℹ Fix +527 527 | """Here, the length of equals is not the same. +528 528 | +529 529 | Parameters + 530 |+ ---------- +530 531 | =========== +531 532 | """ + diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D408_sections.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D408_sections.py.snap index ae8af56231..4550efa25b 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D408_sections.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D408_sections.py.snap @@ -18,7 +18,7 @@ sections.py:94:5: D408 [*] Section underline should be in the line following the | = help: Add underline to "Returns" -ℹ Suggested fix +ℹ Fix 94 94 | """Toggle the gizmo. 95 95 | 96 96 | Returns diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D409_sections.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D409_sections.py.snap index cbfd7d0938..db7d4e96b3 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D409_sections.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D409_sections.py.snap @@ -17,7 +17,7 @@ sections.py:108:5: D409 [*] Section underline should match the length of its nam | = help: Adjust underline length to match "Returns" -ℹ Suggested fix +ℹ Fix 108 108 | """Toggle the gizmo. 109 109 | 110 110 | Returns @@ -51,7 +51,7 @@ sections.py:216:5: D409 [*] Section underline should match the length of its nam | = help: Adjust underline length to match "Returns" -ℹ Suggested fix +ℹ Fix 222 222 | returns. 223 223 | 224 224 | Returns diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D410_D410.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D410_D410.py.snap new file mode 100644 index 0000000000..0ce1725147 --- /dev/null +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D410_D410.py.snap @@ -0,0 +1,59 @@ +--- +source: crates/ruff/src/rules/pydocstyle/mod.rs +--- +D410.py:2:5: D410 [*] Missing blank line after section ("Parameters") + | + 1 | def f(a: int, b: int) -> int: + 2 | """Showcase function. + | _____^ + 3 | | + 4 | | Parameters + 5 | | ---------- + 6 | | a : int + 7 | | _description_ + 8 | | b : int + 9 | | _description_ +10 | | Returns +11 | | ------- +12 | | int +13 | | _description +14 | | """ + | |_______^ D410 +15 | return b - a + | + = help: Add blank line after "Parameters" + +ℹ Fix +7 7 | _description_ +8 8 | b : int +9 9 | _description_ + 10 |+ +10 11 | Returns +11 12 | ------- +12 13 | int + +D410.py:19:5: D410 [*] Missing blank line after section ("Parameters") + | +18 | def f() -> int: +19 | """Showcase function. + | _____^ +20 | | +21 | | Parameters +22 | | ---------- +23 | | Returns +24 | | ------- +25 | | """ + | |_______^ D410 + | + = help: Add blank line after "Parameters" + +ℹ Fix +20 20 | +21 21 | Parameters +22 22 | ---------- + 23 |+ +23 24 | Returns +24 25 | ------- +25 26 | """ + + diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D410_sections.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D410_sections.py.snap index 668b7d7915..e2cda946d5 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D410_sections.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D410_sections.py.snap @@ -22,7 +22,7 @@ sections.py:76:5: D410 [*] Missing blank line after section ("Returns") | = help: Add blank line after "Returns" -ℹ Suggested fix +ℹ Fix 77 77 | 78 78 | Returns 79 79 | ------- @@ -55,7 +55,7 @@ sections.py:216:5: D410 [*] Missing blank line after section ("Returns") | = help: Add blank line after "Returns" -ℹ Suggested fix +ℹ Fix 224 224 | Returns 225 225 | ------ 226 226 | Many many wonderful things. diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D411_sections.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D411_sections.py.snap index 8a17fc197d..1e7cad7261 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D411_sections.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D411_sections.py.snap @@ -22,7 +22,7 @@ sections.py:76:5: D411 [*] Missing blank line before section ("Yields") | = help: Add blank line before "Yields" -ℹ Suggested fix +ℹ Fix 77 77 | 78 78 | Returns 79 79 | ------- @@ -48,7 +48,7 @@ sections.py:131:5: D411 [*] Missing blank line before section ("Returns") | = help: Add blank line before "Returns" -ℹ Suggested fix +ℹ Fix 131 131 | """Toggle the gizmo. 132 132 | 133 133 | The function's description. @@ -81,7 +81,7 @@ sections.py:216:5: D411 [*] Missing blank line before section ("Raises") | = help: Add blank line before "Raises" -ℹ Suggested fix +ℹ Fix 224 224 | Returns 225 225 | ------ 226 226 | Many many wonderful things. diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D412_sections.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D412_sections.py.snap index 551d45f43a..2d32ab4dcc 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D412_sections.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D412_sections.py.snap @@ -25,7 +25,7 @@ sections.py:216:5: D412 [*] No blank lines allowed between a section header and | = help: Remove blank line(s) -ℹ Suggested fix +ℹ Fix 217 217 | 218 218 | Short summary 219 219 | ------------- diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D413_sections.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D413_sections.py.snap index 8cb693bee7..05ae181bc0 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D413_sections.py.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D413_sections.py.snap @@ -13,7 +13,7 @@ sections.py:65:5: D413 [*] Missing blank line after last section ("Returns") | = help: Add blank line after "Returns" -ℹ Suggested fix +ℹ Fix 64 64 | def no_underline_and_no_newline(): # noqa: D416 65 65 | """Toggle the gizmo. 66 66 | diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__d209_d400.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__d209_d400.snap index cf6e6e6be6..4b0e26c0d5 100644 --- a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__d209_d400.snap +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__d209_d400.snap @@ -11,7 +11,7 @@ D209_D400.py:2:5: D209 [*] Multi-line docstring closing quotes should be on a se | = help: Move closing quotes to new line -ℹ Suggested fix +ℹ Fix 1 1 | def lorem(): 2 2 | """lorem ipsum dolor sit amet consectetur adipiscing elit 3 |- sed do eiusmod tempor incididunt ut labore et dolore magna aliqua""" diff --git a/crates/ruff/src/rules/pyflakes/cformat.rs b/crates/ruff/src/rules/pyflakes/cformat.rs index 1a7c3f6d9e..1f9279c241 100644 --- a/crates/ruff/src/rules/pyflakes/cformat.rs +++ b/crates/ruff/src/rules/pyflakes/cformat.rs @@ -25,8 +25,8 @@ impl From<&CFormatString> for CFormatSummary { ref min_field_width, ref precision, .. - }) = format_part.1 else - { + }) = format_part.1 + else { continue; }; match mapping_key { diff --git a/crates/ruff/src/rules/pyflakes/fixes.rs b/crates/ruff/src/rules/pyflakes/fixes.rs index 24a7ef82e7..694e03bf87 100644 --- a/crates/ruff/src/rules/pyflakes/fixes.rs +++ b/crates/ruff/src/rules/pyflakes/fixes.rs @@ -1,7 +1,7 @@ use anyhow::{bail, Ok, Result}; use ruff_text_size::TextRange; -use rustpython_parser::ast::{Excepthandler, Expr, Ranged}; -use rustpython_parser::{lexer, Mode, Tok}; +use rustpython_parser::ast::{ExceptHandler, Expr, Ranged}; +use rustpython_parser::{lexer, Mode}; use ruff_diagnostics::Edit; use ruff_python_ast::source_code::{Locator, Stylist}; @@ -10,7 +10,7 @@ use crate::autofix::codemods::CodegenStylist; use crate::cst::matchers::{match_call_mut, match_dict, match_expression}; /// Generate a [`Edit`] to remove unused keys from format dict. -pub(crate) fn remove_unused_format_arguments_from_dict( +pub(super) fn remove_unused_format_arguments_from_dict( unused_arguments: &[usize], stmt: &Expr, locator: &Locator, @@ -35,7 +35,7 @@ pub(crate) fn remove_unused_format_arguments_from_dict( } /// Generate a [`Edit`] to remove unused keyword arguments from a `format` call. -pub(crate) fn remove_unused_keyword_arguments_from_format_call( +pub(super) fn remove_unused_keyword_arguments_from_format_call( unused_arguments: &[usize], location: TextRange, locator: &Locator, @@ -90,22 +90,22 @@ pub(crate) fn remove_unused_positional_arguments_from_format_call( /// Generate a [`Edit`] to remove the binding from an exception handler. pub(crate) fn remove_exception_handler_assignment( - excepthandler: &Excepthandler, + except_handler: &ExceptHandler, locator: &Locator, ) -> Result { - let contents = locator.slice(excepthandler.range()); + let contents = locator.slice(except_handler.range()); let mut fix_start = None; let mut fix_end = None; // End of the token just before the `as` to the semicolon. let mut prev = None; for (tok, range) in - lexer::lex_starts_at(contents, Mode::Module, excepthandler.start()).flatten() + lexer::lex_starts_at(contents, Mode::Module, except_handler.start()).flatten() { - if matches!(tok, Tok::As) { + if tok.is_as() { fix_start = prev; } - if matches!(tok, Tok::Colon) { + if tok.is_colon() { fix_end = Some(range.start()); break; } diff --git a/crates/ruff/src/rules/pyflakes/format.rs b/crates/ruff/src/rules/pyflakes/format.rs index 03d1f30f95..abb52e8c8a 100644 --- a/crates/ruff/src/rules/pyflakes/format.rs +++ b/crates/ruff/src/rules/pyflakes/format.rs @@ -44,7 +44,8 @@ impl TryFrom<&str> for FormatSummary { field_name, format_spec, .. - } = format_part else { + } = format_part + else { continue; }; let parsed = FieldName::parse(field_name)?; diff --git a/crates/ruff/src/rules/pyflakes/mod.rs b/crates/ruff/src/rules/pyflakes/mod.rs index 11d2944aed..4159bf16f9 100644 --- a/crates/ruff/src/rules/pyflakes/mod.rs +++ b/crates/ruff/src/rules/pyflakes/mod.rs @@ -45,6 +45,7 @@ mod tests { #[test_case(Rule::UnusedImport, Path::new("F401_17.py"))] #[test_case(Rule::UnusedImport, Path::new("F401_18.py"))] #[test_case(Rule::UnusedImport, Path::new("F401_19.py"))] + #[test_case(Rule::UnusedImport, Path::new("F401_20.py"))] #[test_case(Rule::ImportShadowedByLoopVar, Path::new("F402.py"))] #[test_case(Rule::UndefinedLocalWithImportStar, Path::new("F403.py"))] #[test_case(Rule::LateFutureImport, Path::new("F404.py"))] @@ -283,7 +284,7 @@ mod tests { import os # Despite this `del`, `import os` in `f` should still be flagged as shadowing an unused - # import. (This is a false negative.) + # import. del os "#, "del_shadowed_global_import_in_local_scope" @@ -313,8 +314,185 @@ mod tests { "#, "del_shadowed_local_import_in_local_scope" )] + #[test_case( + r#" + import os + + def f(): + os = 1 + print(os) + + del os + + def g(): + # `import os` doesn't need to be flagged as shadowing an import. + os = 1 + print(os) + "#, + "del_shadowed_import_shadow_in_local_scope" + )] + #[test_case( + r#" + x = 1 + + def foo(): + x = 2 + del x + # Flake8 treats this as an F823 error, because it removes the binding + # entirely after the `del` statement. However, it should be an F821 + # error, because the name is defined in the scope, but unbound. + x += 1 + "#, + "augmented_assignment_after_del" + )] + #[test_case( + r#" + def f(): + x = 1 + + try: + 1 / 0 + except Exception as x: + pass + + # No error here, though it should arguably be an F821 error. `x` will + # be unbound after the `except` block (assuming an exception is raised + # and caught). + print(x) + "#, + "print_in_body_after_shadowing_except" + )] + #[test_case( + r#" + def f(): + x = 1 + + try: + 1 / 0 + except ValueError as x: + pass + except ImportError as x: + pass + + # No error here, though it should arguably be an F821 error. `x` will + # be unbound after the `except` block (assuming an exception is raised + # and caught). + print(x) + "#, + "print_in_body_after_double_shadowing_except" + )] + #[test_case( + r#" + def f(): + try: + x = 3 + except ImportError as x: + print(x) + else: + print(x) + "#, + "print_in_try_else_after_shadowing_except" + )] + #[test_case( + r#" + def f(): + list = [1, 2, 3] + + for e in list: + if e % 2 == 0: + try: + pass + except Exception as e: + print(e) + else: + print(e) + "#, + "print_in_if_else_after_shadowing_except" + )] + #[test_case( + r#" + def f(): + x = 1 + del x + del x + "#, + "double_del" + )] + #[test_case( + r#" + x = 1 + + def f(): + try: + pass + except ValueError as x: + pass + + # This should resolve to the `x` in `x = 1`. + print(x) + "#, + "load_after_unbind_from_module_scope" + )] + #[test_case( + r#" + x = 1 + + def f(): + try: + pass + except ValueError as x: + pass + + try: + pass + except ValueError as x: + pass + + # This should resolve to the `x` in `x = 1`. + print(x) + "#, + "load_after_multiple_unbinds_from_module_scope" + )] + #[test_case( + r#" + x = 1 + + def f(): + try: + pass + except ValueError as x: + pass + + def g(): + try: + pass + except ValueError as x: + pass + + # This should resolve to the `x` in `x = 1`. + print(x) + "#, + "load_after_unbind_from_nested_module_scope" + )] + #[test_case( + r#" + class C: + x = 1 + + def f(): + try: + pass + except ValueError as x: + pass + + # This should raise an F821 error, rather than resolving to the + # `x` in `x = 1`. + print(x) + "#, + "load_after_unbind_from_class_scope" + )] fn contents(contents: &str, snapshot: &str) { - let diagnostics = test_snippet(contents, &Settings::for_rules(&Linter::Pyflakes)); + let diagnostics = test_snippet(contents, &Settings::for_rules(Linter::Pyflakes.rules())); assert_messages!(snapshot, diagnostics); } @@ -322,7 +500,7 @@ mod tests { /// Note that all tests marked with `#[ignore]` should be considered TODOs. fn flakes(contents: &str, expected: &[Rule]) { let contents = dedent(contents); - let settings = Settings::for_rules(&Linter::Pyflakes); + let settings = Settings::for_rules(Linter::Pyflakes.rules()); let tokens: Vec = ruff_rustpython::tokenize(&contents); let locator = Locator::new(&contents); let stylist = Stylist::from_tokens(&tokens, &locator); @@ -346,6 +524,7 @@ mod tests { &directives, &settings, flags::Noqa::Enabled, + None, ); diagnostics.sort_by_key(Diagnostic::start); let actual = diagnostics @@ -1937,7 +2116,7 @@ mod tests { try: pass except Exception as fu: pass "#, - &[Rule::RedefinedWhileUnused, Rule::UnusedVariable], + &[Rule::UnusedVariable, Rule::RedefinedWhileUnused], ); } @@ -3146,6 +3325,20 @@ mod tests { r#" T: object def g(t: 'T'): pass + "#, + &[Rule::UndefinedName], + ); + flakes( + r#" + T = object + def f(t: T): pass + "#, + &[], + ); + flakes( + r#" + T = object + def g(t: 'T'): pass "#, &[], ); @@ -3337,6 +3530,16 @@ mod tests { T: object def f(t: T): pass def g(t: 'T'): pass + "#, + &[Rule::UndefinedName, Rule::UndefinedName], + ); + + flakes( + r#" + from __future__ import annotations + T = object + def f(t: T): pass + def g(t: 'T'): pass "#, &[], ); diff --git a/crates/ruff/src/rules/pyflakes/rules/assert_tuple.rs b/crates/ruff/src/rules/pyflakes/rules/assert_tuple.rs index 9f76c7970d..1939ba4aa4 100644 --- a/crates/ruff/src/rules/pyflakes/rules/assert_tuple.rs +++ b/crates/ruff/src/rules/pyflakes/rules/assert_tuple.rs @@ -25,7 +25,7 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/reference/simple_stmts.html#the-assert-statement) +/// - [Python documentation: The `assert` statement](https://docs.python.org/3/reference/simple_stmts.html#the-assert-statement) #[violation] pub struct AssertTuple; diff --git a/crates/ruff/src/rules/pyflakes/rules/default_except_not_last.rs b/crates/ruff/src/rules/pyflakes/rules/default_except_not_last.rs index 197f34e349..49fc869109 100644 --- a/crates/ruff/src/rules/pyflakes/rules/default_except_not_last.rs +++ b/crates/ruff/src/rules/pyflakes/rules/default_except_not_last.rs @@ -1,8 +1,8 @@ -use rustpython_parser::ast::{self, Excepthandler}; +use rustpython_parser::ast::{self, ExceptHandler}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::except_range; +use ruff_python_ast::identifier::except; use ruff_python_ast::source_code::Locator; /// ## What it does @@ -42,7 +42,7 @@ use ruff_python_ast::source_code::Locator; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/reference/compound_stmts.html#except-clause) +/// - [Python documentation: `except` clause](https://docs.python.org/3/reference/compound_stmts.html#except-clause) #[violation] pub struct DefaultExceptNotLast; @@ -55,15 +55,15 @@ impl Violation for DefaultExceptNotLast { /// F707 pub(crate) fn default_except_not_last( - handlers: &[Excepthandler], + handlers: &[ExceptHandler], locator: &Locator, ) -> Option { for (idx, handler) in handlers.iter().enumerate() { - let Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { type_, .. }) = handler; + let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { type_, .. }) = handler; if type_.is_none() && idx < handlers.len() - 1 { return Some(Diagnostic::new( DefaultExceptNotLast, - except_range(handler, locator), + except(handler, locator), )); } } diff --git a/crates/ruff/src/rules/pyflakes/rules/f_string_missing_placeholders.rs b/crates/ruff/src/rules/pyflakes/rules/f_string_missing_placeholders.rs index b8d04af426..bef3508e15 100644 --- a/crates/ruff/src/rules/pyflakes/rules/f_string_missing_placeholders.rs +++ b/crates/ruff/src/rules/pyflakes/rules/f_string_missing_placeholders.rs @@ -79,24 +79,6 @@ fn find_useless_f_strings<'a>( }) } -fn unescape_f_string(content: &str) -> String { - content.replace("{{", "{").replace("}}", "}") -} - -fn fix_f_string_missing_placeholders( - prefix_range: TextRange, - tok_range: TextRange, - checker: &mut Checker, -) -> Fix { - let content = &checker.locator.contents()[TextRange::new(prefix_range.end(), tok_range.end())]; - #[allow(deprecated)] - Fix::unspecified(Edit::replacement( - unescape_f_string(content), - prefix_range.start(), - tok_range.end(), - )) -} - /// F541 pub(crate) fn f_string_missing_placeholders(expr: &Expr, values: &[Expr], checker: &mut Checker) { if !values @@ -106,13 +88,51 @@ pub(crate) fn f_string_missing_placeholders(expr: &Expr, values: &[Expr], checke for (prefix_range, tok_range) in find_useless_f_strings(expr, checker.locator) { let mut diagnostic = Diagnostic::new(FStringMissingPlaceholders, tok_range); if checker.patch(diagnostic.kind.rule()) { - diagnostic.set_fix(fix_f_string_missing_placeholders( + diagnostic.set_fix(convert_f_string_to_regular_string( prefix_range, tok_range, - checker, + checker.locator, )); } checker.diagnostics.push(diagnostic); } } } + +/// Unescape an f-string body by replacing `{{` with `{` and `}}` with `}`. +/// +/// In Python, curly-brace literals within f-strings must be escaped by doubling the braces. +/// When rewriting an f-string to a regular string, we need to unescape any curly-brace literals. +/// For example, given `{{Hello, world!}}`, return `{Hello, world!}`. +fn unescape_f_string(content: &str) -> String { + content.replace("{{", "{").replace("}}", "}") +} + +/// Generate a [`Fix`] to rewrite an f-string as a regular string. +fn convert_f_string_to_regular_string( + prefix_range: TextRange, + tok_range: TextRange, + locator: &Locator, +) -> Fix { + // Extract the f-string body. + let mut content = + unescape_f_string(locator.slice(TextRange::new(prefix_range.end(), tok_range.end()))); + + // If the preceding character is equivalent to the quote character, insert a space to avoid a + // syntax error. For example, when removing the `f` prefix in `""f""`, rewrite to `"" ""` + // instead of `""""`. + if locator + .slice(TextRange::up_to(prefix_range.start())) + .chars() + .last() + .map_or(false, |char| content.starts_with(char)) + { + content.insert(0, ' '); + } + + Fix::automatic(Edit::replacement( + content, + prefix_range.start(), + tok_range.end(), + )) +} diff --git a/crates/ruff/src/rules/pyflakes/rules/future_feature_not_defined.rs b/crates/ruff/src/rules/pyflakes/rules/future_feature_not_defined.rs index 9e234ed1a8..a00d5267e3 100644 --- a/crates/ruff/src/rules/pyflakes/rules/future_feature_not_defined.rs +++ b/crates/ruff/src/rules/pyflakes/rules/future_feature_not_defined.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::{Alias, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_stdlib::future::ALL_FEATURE_NAMES; +use ruff_python_stdlib::future::is_feature_name; use crate::checkers::ast::Checker; @@ -15,7 +15,7 @@ use crate::checkers::ast::Checker; /// a `SyntaxError`. /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/__future__.html) +/// - [Python documentation: `__future__`](https://docs.python.org/3/library/__future__.html) #[violation] pub struct FutureFeatureNotDefined { name: String, @@ -30,12 +30,14 @@ impl Violation for FutureFeatureNotDefined { } pub(crate) fn future_feature_not_defined(checker: &mut Checker, alias: &Alias) { - if !ALL_FEATURE_NAMES.contains(&alias.name.as_str()) { - checker.diagnostics.push(Diagnostic::new( - FutureFeatureNotDefined { - name: alias.name.to_string(), - }, - alias.range(), - )); + if is_feature_name(&alias.name) { + return; } + + checker.diagnostics.push(Diagnostic::new( + FutureFeatureNotDefined { + name: alias.name.to_string(), + }, + alias.range(), + )); } diff --git a/crates/ruff/src/rules/pyflakes/rules/if_tuple.rs b/crates/ruff/src/rules/pyflakes/rules/if_tuple.rs index 01f9e9a784..98ce068c20 100644 --- a/crates/ruff/src/rules/pyflakes/rules/if_tuple.rs +++ b/crates/ruff/src/rules/pyflakes/rules/if_tuple.rs @@ -25,7 +25,7 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/reference/compound_stmts.html#the-if-statement) +/// - [Python documentation: The `if` statement](https://docs.python.org/3/reference/compound_stmts.html#the-if-statement) #[violation] pub struct IfTuple; diff --git a/crates/ruff/src/rules/pyflakes/rules/imports.rs b/crates/ruff/src/rules/pyflakes/rules/imports.rs index 220925b709..b53b9394f1 100644 --- a/crates/ruff/src/rules/pyflakes/rules/imports.rs +++ b/crates/ruff/src/rules/pyflakes/rules/imports.rs @@ -51,7 +51,7 @@ impl Violation for ImportShadowedByLoopVar { /// ## Why is this bad? /// Wildcard imports (e.g., `from module import *`) make it hard to determine /// which symbols are available in the current namespace, and from which module -/// they were imported. +/// they were imported. They're also discouraged by [PEP 8]. /// /// ## Example /// ```python @@ -71,8 +71,7 @@ impl Violation for ImportShadowedByLoopVar { /// return pi * radius**2 /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#imports) +/// [PEP 8]: https://peps.python.org/pep-0008/#imports #[violation] pub struct UndefinedLocalWithImportStar { pub(crate) name: String, @@ -110,7 +109,7 @@ impl Violation for UndefinedLocalWithImportStar { /// ``` /// /// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#module-level-dunder-names) +/// - [Python documentation: Future statements](https://docs.python.org/3/reference/simple_stmts.html#future) #[violation] pub struct LateFutureImport; @@ -155,9 +154,6 @@ impl Violation for LateFutureImport { /// def area(radius): /// return pi * radius**2 /// ``` -/// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#imports) #[violation] pub struct UndefinedLocalWithImportStarUsage { pub(crate) name: String, @@ -183,8 +179,9 @@ impl Violation for UndefinedLocalWithImportStarUsage { /// The use of wildcard imports outside of the module namespace (e.g., within /// functions) can lead to confusion, as the import can shadow local variables. /// -/// Though wildcard imports are discouraged, when necessary, they should be placed -/// in the module namespace (i.e., at the top-level of a module). +/// Though wildcard imports are discouraged by [PEP 8], when necessary, they +/// should be placed in the module namespace (i.e., at the top-level of a +/// module). /// /// ## Example /// ```python @@ -201,8 +198,7 @@ impl Violation for UndefinedLocalWithImportStarUsage { /// ... /// ``` /// -/// ## References -/// - [PEP 8](https://peps.python.org/pep-0008/#imports) +/// [PEP 8]: https://peps.python.org/pep-0008/#imports #[violation] pub struct UndefinedLocalWithNestedImportStarUsage { pub(crate) name: String, diff --git a/crates/ruff/src/rules/pyflakes/rules/invalid_literal_comparisons.rs b/crates/ruff/src/rules/pyflakes/rules/invalid_literal_comparisons.rs index 10beb70983..f01023a4e9 100644 --- a/crates/ruff/src/rules/pyflakes/rules/invalid_literal_comparisons.rs +++ b/crates/ruff/src/rules/pyflakes/rules/invalid_literal_comparisons.rs @@ -1,7 +1,7 @@ use itertools::izip; use log::error; use once_cell::unsync::Lazy; -use rustpython_parser::ast::{Cmpop, Expr, Ranged}; +use rustpython_parser::ast::{CmpOp, Expr, Ranged}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; @@ -42,29 +42,29 @@ use crate::registry::AsRule; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/reference/expressions.html#is-not) -/// - [Python documentation](https://docs.python.org/3/reference/expressions.html#value-comparisons) +/// - [Python documentation: Identity comparisons](https://docs.python.org/3/reference/expressions.html#is-not) +/// - [Python documentation: Value comparisons](https://docs.python.org/3/reference/expressions.html#value-comparisons) /// - [_Why does Python log a SyntaxWarning for ‘is’ with literals?_ by Adam Johnson](https://adamj.eu/tech/2020/01/21/why-does-python-3-8-syntaxwarning-for-is-literal/) #[violation] pub struct IsLiteral { - cmpop: IsCmpop, + cmp_op: IsCmpOp, } impl AlwaysAutofixableViolation for IsLiteral { #[derive_message_formats] fn message(&self) -> String { - let IsLiteral { cmpop } = self; - match cmpop { - IsCmpop::Is => format!("Use `==` to compare constant literals"), - IsCmpop::IsNot => format!("Use `!=` to compare constant literals"), + let IsLiteral { cmp_op } = self; + match cmp_op { + IsCmpOp::Is => format!("Use `==` to compare constant literals"), + IsCmpOp::IsNot => format!("Use `!=` to compare constant literals"), } } fn autofix_title(&self) -> String { - let IsLiteral { cmpop } = self; - match cmpop { - IsCmpop::Is => "Replace `is` with `==`".to_string(), - IsCmpop::IsNot => "Replace `is not` with `!=`".to_string(), + let IsLiteral { cmp_op } = self; + match cmp_op { + IsCmpOp::Is => "Replace `is` with `==`".to_string(), + IsCmpOp::IsNot => "Replace `is not` with `!=`".to_string(), } } } @@ -73,31 +73,30 @@ impl AlwaysAutofixableViolation for IsLiteral { pub(crate) fn invalid_literal_comparison( checker: &mut Checker, left: &Expr, - ops: &[Cmpop], + ops: &[CmpOp], comparators: &[Expr], expr: &Expr, ) { - let located = Lazy::new(|| helpers::locate_cmpops(expr, checker.locator)); + let located = Lazy::new(|| helpers::locate_cmp_ops(expr, checker.locator)); let mut left = left; for (index, (op, right)) in izip!(ops, comparators).enumerate() { - if matches!(op, Cmpop::Is | Cmpop::IsNot) + if matches!(op, CmpOp::Is | CmpOp::IsNot) && (helpers::is_constant_non_singleton(left) || helpers::is_constant_non_singleton(right)) { - let mut diagnostic = Diagnostic::new(IsLiteral { cmpop: op.into() }, expr.range()); + let mut diagnostic = Diagnostic::new(IsLiteral { cmp_op: op.into() }, expr.range()); if checker.patch(diagnostic.kind.rule()) { if let Some(located_op) = &located.get(index) { assert_eq!(located_op.op, *op); if let Some(content) = match located_op.op { - Cmpop::Is => Some("==".to_string()), - Cmpop::IsNot => Some("!=".to_string()), + CmpOp::Is => Some("==".to_string()), + CmpOp::IsNot => Some("!=".to_string()), node => { error!("Failed to fix invalid comparison: {node:?}"); None } } { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( content, located_op.range + expr.start(), ))); @@ -113,17 +112,17 @@ pub(crate) fn invalid_literal_comparison( } #[derive(Debug, PartialEq, Eq, Copy, Clone)] -enum IsCmpop { +enum IsCmpOp { Is, IsNot, } -impl From<&Cmpop> for IsCmpop { - fn from(cmpop: &Cmpop) -> Self { - match cmpop { - Cmpop::Is => IsCmpop::Is, - Cmpop::IsNot => IsCmpop::IsNot, - _ => panic!("Expected Cmpop::Is | Cmpop::IsNot"), +impl From<&CmpOp> for IsCmpOp { + fn from(cmp_op: &CmpOp) -> Self { + match cmp_op { + CmpOp::Is => IsCmpOp::Is, + CmpOp::IsNot => IsCmpOp::IsNot, + _ => panic!("Expected CmpOp::Is | CmpOp::IsNot"), } } } diff --git a/crates/ruff/src/rules/pyflakes/rules/invalid_print_syntax.rs b/crates/ruff/src/rules/pyflakes/rules/invalid_print_syntax.rs index 651b03d737..9cc7a3eeaf 100644 --- a/crates/ruff/src/rules/pyflakes/rules/invalid_print_syntax.rs +++ b/crates/ruff/src/rules/pyflakes/rules/invalid_print_syntax.rs @@ -44,7 +44,7 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/functions.html#print) +/// - [Python documentation: `print`](https://docs.python.org/3/library/functions.html#print) #[violation] pub struct InvalidPrintSyntax; @@ -63,7 +63,7 @@ pub(crate) fn invalid_print_syntax(checker: &mut Checker, left: &Expr) { if id != "print" { return; } - if !checker.semantic_model().is_builtin("print") { + if !checker.semantic().is_builtin("print") { return; }; checker diff --git a/crates/ruff/src/rules/pyflakes/rules/mod.rs b/crates/ruff/src/rules/pyflakes/rules/mod.rs index 3a66bb0e8f..8c21d78d24 100644 --- a/crates/ruff/src/rules/pyflakes/rules/mod.rs +++ b/crates/ruff/src/rules/pyflakes/rules/mod.rs @@ -1,49 +1,27 @@ -pub(crate) use assert_tuple::{assert_tuple, AssertTuple}; -pub(crate) use break_outside_loop::{break_outside_loop, BreakOutsideLoop}; -pub(crate) use continue_outside_loop::{continue_outside_loop, ContinueOutsideLoop}; -pub(crate) use default_except_not_last::{default_except_not_last, DefaultExceptNotLast}; -pub(crate) use f_string_missing_placeholders::{ - f_string_missing_placeholders, FStringMissingPlaceholders, -}; -pub(crate) use forward_annotation_syntax_error::ForwardAnnotationSyntaxError; -pub(crate) use future_feature_not_defined::{future_feature_not_defined, FutureFeatureNotDefined}; -pub(crate) use if_tuple::{if_tuple, IfTuple}; -pub(crate) use imports::{ - ImportShadowedByLoopVar, LateFutureImport, UndefinedLocalWithImportStar, - UndefinedLocalWithImportStarUsage, UndefinedLocalWithNestedImportStarUsage, -}; -pub(crate) use invalid_literal_comparisons::{invalid_literal_comparison, IsLiteral}; -pub(crate) use invalid_print_syntax::{invalid_print_syntax, InvalidPrintSyntax}; -pub(crate) use raise_not_implemented::{raise_not_implemented, RaiseNotImplemented}; -pub(crate) use redefined_while_unused::RedefinedWhileUnused; -pub(crate) use repeated_keys::{ - repeated_keys, MultiValueRepeatedKeyLiteral, MultiValueRepeatedKeyVariable, -}; -pub(crate) use return_outside_function::{return_outside_function, ReturnOutsideFunction}; -pub(crate) use starred_expressions::{ - starred_expressions, ExpressionsInStarAssignment, MultipleStarredExpressions, -}; -pub(crate) use strings::{ - percent_format_expected_mapping, percent_format_expected_sequence, - percent_format_extra_named_arguments, percent_format_missing_arguments, - percent_format_mixed_positional_and_named, percent_format_positional_count_mismatch, - percent_format_star_requires_sequence, string_dot_format_extra_named_arguments, - string_dot_format_extra_positional_arguments, string_dot_format_missing_argument, - string_dot_format_mixing_automatic, PercentFormatExpectedMapping, - PercentFormatExpectedSequence, PercentFormatExtraNamedArguments, PercentFormatInvalidFormat, - PercentFormatMissingArgument, PercentFormatMixedPositionalAndNamed, - PercentFormatPositionalCountMismatch, PercentFormatStarRequiresSequence, - PercentFormatUnsupportedFormatCharacter, StringDotFormatExtraNamedArguments, - StringDotFormatExtraPositionalArguments, StringDotFormatInvalidFormat, - StringDotFormatMissingArguments, StringDotFormatMixingAutomatic, -}; -pub(crate) use undefined_export::{undefined_export, UndefinedExport}; -pub(crate) use undefined_local::{undefined_local, UndefinedLocal}; -pub(crate) use undefined_name::UndefinedName; -pub(crate) use unused_annotation::{unused_annotation, UnusedAnnotation}; -pub(crate) use unused_import::{unused_import, UnusedImport}; -pub(crate) use unused_variable::{unused_variable, UnusedVariable}; -pub(crate) use yield_outside_function::{yield_outside_function, YieldOutsideFunction}; +pub(crate) use assert_tuple::*; +pub(crate) use break_outside_loop::*; +pub(crate) use continue_outside_loop::*; +pub(crate) use default_except_not_last::*; +pub(crate) use f_string_missing_placeholders::*; +pub(crate) use forward_annotation_syntax_error::*; +pub(crate) use future_feature_not_defined::*; +pub(crate) use if_tuple::*; +pub(crate) use imports::*; +pub(crate) use invalid_literal_comparisons::*; +pub(crate) use invalid_print_syntax::*; +pub(crate) use raise_not_implemented::*; +pub(crate) use redefined_while_unused::*; +pub(crate) use repeated_keys::*; +pub(crate) use return_outside_function::*; +pub(crate) use starred_expressions::*; +pub(crate) use strings::*; +pub(crate) use undefined_export::*; +pub(crate) use undefined_local::*; +pub(crate) use undefined_name::*; +pub(crate) use unused_annotation::*; +pub(crate) use unused_import::*; +pub(crate) use unused_variable::*; +pub(crate) use yield_outside_function::*; mod assert_tuple; mod break_outside_loop; diff --git a/crates/ruff/src/rules/pyflakes/rules/raise_not_implemented.rs b/crates/ruff/src/rules/pyflakes/rules/raise_not_implemented.rs index f91e08f0bf..0bbdfffb2b 100644 --- a/crates/ruff/src/rules/pyflakes/rules/raise_not_implemented.rs +++ b/crates/ruff/src/rules/pyflakes/rules/raise_not_implemented.rs @@ -1,6 +1,6 @@ use rustpython_parser::ast::{self, Expr, Ranged}; -use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; +use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; @@ -37,14 +37,16 @@ use crate::registry::AsRule; #[violation] pub struct RaiseNotImplemented; -impl AlwaysAutofixableViolation for RaiseNotImplemented { +impl Violation for RaiseNotImplemented { + const AUTOFIX: AutofixKind = AutofixKind::Sometimes; + #[derive_message_formats] fn message(&self) -> String { format!("`raise NotImplemented` should be `raise NotImplementedError`") } - fn autofix_title(&self) -> String { - "Use `raise NotImplementedError`".to_string() + fn autofix_title(&self) -> Option { + Some("Use `raise NotImplementedError`".to_string()) } } @@ -74,11 +76,12 @@ pub(crate) fn raise_not_implemented(checker: &mut Checker, expr: &Expr) { }; let mut diagnostic = Diagnostic::new(RaiseNotImplemented, expr.range()); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( - "NotImplementedError".to_string(), - expr.range(), - ))); + if checker.semantic().is_builtin("NotImplementedError") { + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( + "NotImplementedError".to_string(), + expr.range(), + ))); + } } checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/pyflakes/rules/repeated_keys.rs b/crates/ruff/src/rules/pyflakes/rules/repeated_keys.rs index 44f2c70a5d..ec4412042c 100644 --- a/crates/ruff/src/rules/pyflakes/rules/repeated_keys.rs +++ b/crates/ruff/src/rules/pyflakes/rules/repeated_keys.rs @@ -1,11 +1,11 @@ -use std::hash::{BuildHasherDefault, Hash}; +use std::hash::BuildHasherDefault; use rustc_hash::{FxHashMap, FxHashSet}; -use rustpython_parser::ast::{self, Expr, Ranged}; +use rustpython_parser::ast::{Expr, Ranged}; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::comparable::{ComparableConstant, ComparableExpr}; +use ruff_python_ast::comparable::ComparableExpr; use crate::checkers::ast::Checker; use crate::registry::{AsRule, Rule}; @@ -39,11 +39,10 @@ use crate::registry::{AsRule, Rule}; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/tutorial/datastructures.html#dictionaries) +/// - [Python documentation: Dictionaries](https://docs.python.org/3/tutorial/datastructures.html#dictionaries) #[violation] pub struct MultiValueRepeatedKeyLiteral { name: String, - repeated_value: bool, } impl Violation for MultiValueRepeatedKeyLiteral { @@ -51,20 +50,13 @@ impl Violation for MultiValueRepeatedKeyLiteral { #[derive_message_formats] fn message(&self) -> String { - let MultiValueRepeatedKeyLiteral { name, .. } = self; + let MultiValueRepeatedKeyLiteral { name } = self; format!("Dictionary key literal `{name}` repeated") } fn autofix_title(&self) -> Option { - let MultiValueRepeatedKeyLiteral { - repeated_value, - name, - } = self; - if *repeated_value { - Some(format!("Remove repeated key literal `{name}`")) - } else { - None - } + let MultiValueRepeatedKeyLiteral { name } = self; + Some(format!("Remove repeated key literal `{name}`")) } } @@ -96,11 +88,10 @@ impl Violation for MultiValueRepeatedKeyLiteral { /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/tutorial/datastructures.html#dictionaries) +/// - [Python documentation: Dictionaries](https://docs.python.org/3/tutorial/datastructures.html#dictionaries) #[violation] pub struct MultiValueRepeatedKeyVariable { name: String, - repeated_value: bool, } impl Violation for MultiValueRepeatedKeyVariable { @@ -108,43 +99,20 @@ impl Violation for MultiValueRepeatedKeyVariable { #[derive_message_formats] fn message(&self) -> String { - let MultiValueRepeatedKeyVariable { name, .. } = self; + let MultiValueRepeatedKeyVariable { name } = self; format!("Dictionary key `{name}` repeated") } fn autofix_title(&self) -> Option { - let MultiValueRepeatedKeyVariable { - repeated_value, - name, - } = self; - if *repeated_value { - Some(format!("Remove repeated key `{name}`")) - } else { - None - } - } -} - -#[derive(Debug, Eq, PartialEq, Hash)] -enum DictionaryKey<'a> { - Constant(ComparableConstant<'a>), - Variable(&'a str), -} - -fn into_dictionary_key(expr: &Expr) -> Option { - match expr { - Expr::Constant(ast::ExprConstant { value, .. }) => { - Some(DictionaryKey::Constant(value.into())) - } - Expr::Name(ast::ExprName { id, .. }) => Some(DictionaryKey::Variable(id)), - _ => None, + let MultiValueRepeatedKeyVariable { name } = self; + Some(format!("Remove repeated key `{name}`")) } } /// F601, F602 pub(crate) fn repeated_keys(checker: &mut Checker, keys: &[Option], values: &[Expr]) { // Generate a map from key to (index, value). - let mut seen: FxHashMap> = + let mut seen: FxHashMap> = FxHashMap::with_capacity_and_hasher(keys.len(), BuildHasherDefault::default()); // Detect duplicate keys. @@ -152,63 +120,56 @@ pub(crate) fn repeated_keys(checker: &mut Checker, keys: &[Option], values let Some(key) = key else { continue; }; - if let Some(dict_key) = into_dictionary_key(key) { - if let Some(seen_values) = seen.get_mut(&dict_key) { - match dict_key { - DictionaryKey::Constant(..) => { - if checker.enabled(Rule::MultiValueRepeatedKeyLiteral) { - let comparable_value: ComparableExpr = (&values[i]).into(); - let is_duplicate_value = seen_values.contains(&comparable_value); - let mut diagnostic = Diagnostic::new( - MultiValueRepeatedKeyLiteral { - name: checker.generator().expr(key), - repeated_value: is_duplicate_value, - }, - key.range(), - ); - if is_duplicate_value { - if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::deletion( - values[i - 1].end(), - values[i].end(), - ))); - } - } else { - seen_values.insert(comparable_value); - } - checker.diagnostics.push(diagnostic); - } - } - DictionaryKey::Variable(dict_key) => { - if checker.enabled(Rule::MultiValueRepeatedKeyVariable) { - let comparable_value: ComparableExpr = (&values[i]).into(); - let is_duplicate_value = seen_values.contains(&comparable_value); - let mut diagnostic = Diagnostic::new( - MultiValueRepeatedKeyVariable { - name: dict_key.to_string(), - repeated_value: is_duplicate_value, - }, - key.range(), - ); - if is_duplicate_value { - if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::deletion( - values[i - 1].end(), - values[i].end(), - ))); - } - } else { - seen_values.insert(comparable_value); - } - checker.diagnostics.push(diagnostic); + + let comparable_key = ComparableExpr::from(key); + let comparable_value = ComparableExpr::from(&values[i]); + + let Some(seen_values) = seen.get_mut(&comparable_key) else { + seen.insert(comparable_key, FxHashSet::from_iter([comparable_value])); + continue; + }; + + match key { + Expr::Constant(_) | Expr::Tuple(_) | Expr::JoinedStr(_) => { + if checker.enabled(Rule::MultiValueRepeatedKeyLiteral) { + let mut diagnostic = Diagnostic::new( + MultiValueRepeatedKeyLiteral { + name: checker.locator.slice(key.range()).to_string(), + }, + key.range(), + ); + if checker.patch(diagnostic.kind.rule()) { + if !seen_values.insert(comparable_value) { + diagnostic.set_fix(Fix::suggested(Edit::deletion( + values[i - 1].end(), + values[i].end(), + ))); } } + checker.diagnostics.push(diagnostic); } - } else { - seen.insert(dict_key, FxHashSet::from_iter([(&values[i]).into()])); } + Expr::Name(_) => { + if checker.enabled(Rule::MultiValueRepeatedKeyVariable) { + let mut diagnostic = Diagnostic::new( + MultiValueRepeatedKeyVariable { + name: checker.locator.slice(key.range()).to_string(), + }, + key.range(), + ); + if checker.patch(diagnostic.kind.rule()) { + let comparable_value: ComparableExpr = (&values[i]).into(); + if !seen_values.insert(comparable_value) { + diagnostic.set_fix(Fix::suggested(Edit::deletion( + values[i - 1].end(), + values[i].end(), + ))); + } + } + checker.diagnostics.push(diagnostic); + } + } + _ => {} } } } diff --git a/crates/ruff/src/rules/pyflakes/rules/return_outside_function.rs b/crates/ruff/src/rules/pyflakes/rules/return_outside_function.rs index c41d3a0aaa..54acb483fd 100644 --- a/crates/ruff/src/rules/pyflakes/rules/return_outside_function.rs +++ b/crates/ruff/src/rules/pyflakes/rules/return_outside_function.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::{Ranged, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::scope::ScopeKind; +use ruff_python_semantic::ScopeKind; use crate::checkers::ast::Checker; @@ -33,7 +33,7 @@ impl Violation for ReturnOutsideFunction { pub(crate) fn return_outside_function(checker: &mut Checker, stmt: &Stmt) { if matches!( - checker.semantic_model().scope().kind, + checker.semantic().scope().kind, ScopeKind::Class(_) | ScopeKind::Module ) { checker diff --git a/crates/ruff/src/rules/pyflakes/rules/starred_expressions.rs b/crates/ruff/src/rules/pyflakes/rules/starred_expressions.rs index d4b3a875fa..723d2dcc31 100644 --- a/crates/ruff/src/rules/pyflakes/rules/starred_expressions.rs +++ b/crates/ruff/src/rules/pyflakes/rules/starred_expressions.rs @@ -59,7 +59,7 @@ pub(crate) fn starred_expressions( let mut has_starred: bool = false; let mut starred_index: Option = None; for (index, elt) in elts.iter().enumerate() { - if matches!(elt, Expr::Starred(_)) { + if elt.is_starred_expr() { if has_starred && check_two_starred_expressions { return Some(Diagnostic::new(MultipleStarredExpressions, location)); } diff --git a/crates/ruff/src/rules/pyflakes/rules/strings.rs b/crates/ruff/src/rules/pyflakes/rules/strings.rs index a89a6b602a..5d5c79a35d 100644 --- a/crates/ruff/src/rules/pyflakes/rules/strings.rs +++ b/crates/ruff/src/rules/pyflakes/rules/strings.rs @@ -35,7 +35,7 @@ use super::super::format::FormatSummary; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) +/// - [Python documentation: `printf`-style String Formatting](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) #[violation] pub struct PercentFormatInvalidFormat { pub(crate) message: String, @@ -74,7 +74,7 @@ impl Violation for PercentFormatInvalidFormat { /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) +/// - [Python documentation: `printf`-style String Formatting](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) #[violation] pub struct PercentFormatExpectedMapping; @@ -110,7 +110,7 @@ impl Violation for PercentFormatExpectedMapping { /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) +/// - [Python documentation: `printf`-style String Formatting](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) #[violation] pub struct PercentFormatExpectedSequence; @@ -139,7 +139,7 @@ impl Violation for PercentFormatExpectedSequence { /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) +/// - [Python documentation: `printf`-style String Formatting](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) #[violation] pub struct PercentFormatExtraNamedArguments { missing: Vec, @@ -179,7 +179,7 @@ impl AlwaysAutofixableViolation for PercentFormatExtraNamedArguments { /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) +/// - [Python documentation: `printf`-style String Formatting](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) #[violation] pub struct PercentFormatMissingArgument { missing: Vec, @@ -219,7 +219,7 @@ impl Violation for PercentFormatMissingArgument { /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) +/// - [Python documentation: `printf`-style String Formatting](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) #[violation] pub struct PercentFormatMixedPositionalAndNamed; @@ -249,7 +249,7 @@ impl Violation for PercentFormatMixedPositionalAndNamed { /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) +/// - [Python documentation: `printf`-style String Formatting](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) #[violation] pub struct PercentFormatPositionalCountMismatch { wanted: usize, @@ -287,7 +287,7 @@ impl Violation for PercentFormatPositionalCountMismatch { /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) +/// - [Python documentation: `printf`-style String Formatting](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) #[violation] pub struct PercentFormatStarRequiresSequence; @@ -317,7 +317,7 @@ impl Violation for PercentFormatStarRequiresSequence { /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) +/// - [Python documentation: `printf`-style String Formatting](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) #[violation] pub struct PercentFormatUnsupportedFormatCharacter { pub(crate) char: char, @@ -348,7 +348,7 @@ impl Violation for PercentFormatUnsupportedFormatCharacter { /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/stdtypes.html#str.format) +/// - [Python documentation: `str.format`](https://docs.python.org/3/library/stdtypes.html#str.format) #[violation] pub struct StringDotFormatInvalidFormat { pub(crate) message: String, @@ -380,7 +380,7 @@ impl Violation for StringDotFormatInvalidFormat { /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/stdtypes.html#str.format) +/// - [Python documentation: `str.format`](https://docs.python.org/3/library/stdtypes.html#str.format) #[violation] pub struct StringDotFormatExtraNamedArguments { missing: Vec, @@ -421,7 +421,7 @@ impl Violation for StringDotFormatExtraNamedArguments { /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/stdtypes.html#str.format) +/// - [Python documentation: `str.format`](https://docs.python.org/3/library/stdtypes.html#str.format) #[violation] pub struct StringDotFormatExtraPositionalArguments { missing: Vec, @@ -464,7 +464,7 @@ impl Violation for StringDotFormatExtraPositionalArguments { /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/stdtypes.html#str.format) +/// - [Python documentation: `str.format`](https://docs.python.org/3/library/stdtypes.html#str.format) #[violation] pub struct StringDotFormatMissingArguments { missing: Vec, @@ -502,7 +502,7 @@ impl Violation for StringDotFormatMissingArguments { /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/stdtypes.html#str.format) +/// - [Python documentation: `str.format`](https://docs.python.org/3/library/stdtypes.html#str.format) #[violation] pub struct StringDotFormatMixingAutomatic; @@ -514,14 +514,13 @@ impl Violation for StringDotFormatMixingAutomatic { } fn has_star_star_kwargs(keywords: &[Keyword]) -> bool { - keywords.iter().any(|k| { - let Keyword { arg, .. } = &k; - arg.is_none() - }) + keywords + .iter() + .any(|keyword| matches!(keyword, Keyword { arg: None, .. })) } fn has_star_args(args: &[Expr]) -> bool { - args.iter().any(|arg| matches!(&arg, Expr::Starred(_))) + args.iter().any(Expr::is_starred_expr) } /// F502 @@ -805,9 +804,7 @@ pub(crate) fn string_dot_format_extra_positional_arguments( .iter() .enumerate() .filter(|(i, arg)| { - !(matches!(arg, Expr::Starred(_)) - || summary.autos.contains(i) - || summary.indices.contains(i)) + !(arg.is_starred_expr() || summary.autos.contains(i) || summary.indices.contains(i)) }) .map(|(i, _)| i) .collect(); diff --git a/crates/ruff/src/rules/pyflakes/rules/undefined_export.rs b/crates/ruff/src/rules/pyflakes/rules/undefined_export.rs index e4edea697d..46e9cc5a87 100644 --- a/crates/ruff/src/rules/pyflakes/rules/undefined_export.rs +++ b/crates/ruff/src/rules/pyflakes/rules/undefined_export.rs @@ -2,7 +2,7 @@ use ruff_text_size::TextRange; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::scope::Scope; +use ruff_python_semantic::Scope; /// ## What it does /// Checks for undefined names in `__all__`. diff --git a/crates/ruff/src/rules/pyflakes/rules/undefined_local.rs b/crates/ruff/src/rules/pyflakes/rules/undefined_local.rs index 312e5a1d77..2312963920 100644 --- a/crates/ruff/src/rules/pyflakes/rules/undefined_local.rs +++ b/crates/ruff/src/rules/pyflakes/rules/undefined_local.rs @@ -46,7 +46,7 @@ impl Violation for UndefinedLocal { /// F823 pub(crate) fn undefined_local(checker: &mut Checker, name: &str) { // If the name hasn't already been defined in the current scope... - let current = checker.semantic_model().scope(); + let current = checker.semantic().scope(); if !current.kind.is_any_function() || current.has(name) { return; } @@ -57,7 +57,7 @@ pub(crate) fn undefined_local(checker: &mut Checker, name: &str) { // For every function and module scope above us... let local_access = checker - .semantic_model() + .semantic() .scopes .ancestors(parent) .find_map(|scope| { @@ -68,15 +68,12 @@ pub(crate) fn undefined_local(checker: &mut Checker, name: &str) { // If the name was defined in that scope... if let Some(binding) = scope .get(name) - .map(|binding_id| &checker.semantic_model().bindings[binding_id]) + .map(|binding_id| checker.semantic().binding(binding_id)) { // And has already been accessed in the current scope... if let Some(range) = binding.references().find_map(|reference_id| { - let reference = checker.semantic_model().reference(reference_id); - if checker - .semantic_model() - .is_current_scope(reference.scope_id()) - { + let reference = checker.semantic().reference(reference_id); + if checker.semantic().is_current_scope(reference.scope_id()) { Some(reference.range()) } else { None diff --git a/crates/ruff/src/rules/pyflakes/rules/undefined_name.rs b/crates/ruff/src/rules/pyflakes/rules/undefined_name.rs index a4619cd618..9fae09e6c2 100644 --- a/crates/ruff/src/rules/pyflakes/rules/undefined_name.rs +++ b/crates/ruff/src/rules/pyflakes/rules/undefined_name.rs @@ -20,7 +20,7 @@ use ruff_macros::{derive_message_formats, violation}; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/reference/executionmodel.html#naming-and-binding) +/// - [Python documentation: Naming and binding](https://docs.python.org/3/reference/executionmodel.html#naming-and-binding) #[violation] pub struct UndefinedName { pub(crate) name: String, diff --git a/crates/ruff/src/rules/pyflakes/rules/unused_annotation.rs b/crates/ruff/src/rules/pyflakes/rules/unused_annotation.rs index 44e96c53f6..1f2e92f25d 100644 --- a/crates/ruff/src/rules/pyflakes/rules/unused_annotation.rs +++ b/crates/ruff/src/rules/pyflakes/rules/unused_annotation.rs @@ -1,6 +1,6 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::scope::ScopeId; +use ruff_python_semantic::ScopeId; use crate::checkers::ast::Checker; @@ -34,12 +34,12 @@ impl Violation for UnusedAnnotation { /// F842 pub(crate) fn unused_annotation(checker: &mut Checker, scope: ScopeId) { - let scope = &checker.semantic_model().scopes[scope]; + let scope = &checker.semantic().scopes[scope]; let bindings: Vec<_> = scope .bindings() .filter_map(|(name, binding_id)| { - let binding = &checker.semantic_model().bindings[binding_id]; + let binding = checker.semantic().binding(binding_id); if binding.kind.is_annotation() && !binding.is_used() && !checker.settings.dummy_variable_rgx.is_match(name) diff --git a/crates/ruff/src/rules/pyflakes/rules/unused_import.rs b/crates/ruff/src/rules/pyflakes/rules/unused_import.rs index 648f6bebce..540197ab38 100644 --- a/crates/ruff/src/rules/pyflakes/rules/unused_import.rs +++ b/crates/ruff/src/rules/pyflakes/rules/unused_import.rs @@ -4,9 +4,7 @@ use rustc_hash::FxHashMap; use ruff_diagnostics::{AutofixKind, Diagnostic, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::binding::Exceptions; -use ruff_python_semantic::node::NodeId; -use ruff_python_semantic::scope::Scope; +use ruff_python_semantic::{Exceptions, NodeId, Scope}; use crate::autofix; use crate::checkers::ast::Checker; @@ -28,10 +26,6 @@ enum UnusedImportContext { /// If an import statement is used to check for the availability or existence /// of a module, consider using `importlib.util.find_spec` instead. /// -/// ## Options -/// -/// - `pyflakes.extend-generics` -/// /// ## Example /// ```python /// import numpy as np # unused import @@ -57,6 +51,9 @@ enum UnusedImportContext { /// print("numpy is not installed") /// ``` /// +/// ## Options +/// - `pyflakes.extend-generics` +/// /// ## References /// - [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement) /// - [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec) @@ -104,9 +101,13 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut let mut ignored: FxHashMap<(NodeId, Exceptions), Vec> = FxHashMap::default(); for binding_id in scope.binding_ids() { - let top_binding = &checker.semantic_model().bindings[binding_id]; + let top_binding = checker.semantic().binding(binding_id); - if top_binding.is_used() || top_binding.is_explicit_export() { + if top_binding.is_used() + || top_binding.is_explicit_export() + || top_binding.is_nonlocal() + || top_binding.is_global() + { continue; } @@ -123,16 +124,16 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut }; let Some(stmt_id) = binding.source else { - break; + continue; }; let import = Import { qualified_name, - trimmed_range: binding.trimmed_range(checker.semantic_model(), checker.locator), - parent_range: binding.parent_range(checker.semantic_model()), + range: binding.range, + parent_range: binding.parent_range(checker.semantic()), }; - if checker.rule_is_ignored(Rule::UnusedImport, import.trimmed_range.start()) + if checker.rule_is_ignored(Rule::UnusedImport, import.range.start()) || import.parent_range.map_or(false, |parent_range| { checker.rule_is_ignored(Rule::UnusedImport, parent_range.start()) }) @@ -168,7 +169,7 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut for Import { qualified_name, - trimmed_range, + range, parent_range, } in imports { @@ -184,7 +185,7 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut }, multiple, }, - trimmed_range, + range, ); if let Some(range) = parent_range { diagnostic.set_parent(range.start()); @@ -202,7 +203,7 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut // suppression comments aren't marked as unused. for Import { qualified_name, - trimmed_range, + range, parent_range, } in ignored.into_values().flatten() { @@ -212,7 +213,7 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut context: None, multiple: false, }, - trimmed_range, + range, ); if let Some(range) = parent_range { diagnostic.set_parent(range.start()); @@ -226,15 +227,15 @@ struct Import<'a> { /// The qualified name of the import (e.g., `typing.List` for `from typing import List`). qualified_name: &'a str, /// The trimmed range of the import (e.g., `List` in `from typing import List`). - trimmed_range: TextRange, + range: TextRange, /// The range of the import's parent statement. parent_range: Option, } /// Generate a [`Fix`] to remove unused imports from a statement. fn fix_imports(checker: &Checker, stmt_id: NodeId, imports: &[Import]) -> Result { - let stmt = checker.semantic_model().stmts[stmt_id]; - let parent = checker.semantic_model().stmts.parent(stmt); + let stmt = checker.semantic().stmts[stmt_id]; + let parent = checker.semantic().stmts.parent(stmt); let edit = autofix::edits::remove_unused_imports( imports .iter() @@ -242,8 +243,8 @@ fn fix_imports(checker: &Checker, stmt_id: NodeId, imports: &[Import]) -> Result stmt, parent, checker.locator, - checker.indexer, checker.stylist, + checker.indexer, )?; Ok(Fix::automatic(edit).isolate(checker.isolation(parent))) } diff --git a/crates/ruff/src/rules/pyflakes/rules/unused_variable.rs b/crates/ruff/src/rules/pyflakes/rules/unused_variable.rs index fb6932ca3f..a611eb8375 100644 --- a/crates/ruff/src/rules/pyflakes/rules/unused_variable.rs +++ b/crates/ruff/src/rules/pyflakes/rules/unused_variable.rs @@ -7,7 +7,7 @@ use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::contains_effect; use ruff_python_ast::source_code::Locator; -use ruff_python_semantic::scope::ScopeId; +use ruff_python_semantic::ScopeId; use crate::autofix::edits::delete_stmt; use crate::checkers::ast::Checker; @@ -24,9 +24,6 @@ use crate::registry::AsRule; /// prefixed with an underscore, or some other value that adheres to the /// [`dummy-variable-rgx`] pattern. /// -/// ## Options -/// - `dummy-variable-rgx` -/// /// ## Example /// ```python /// def foo(): @@ -41,6 +38,9 @@ use crate::registry::AsRule; /// x = 1 /// return x /// ``` +/// +/// ## Options +/// - `dummy-variable-rgx` #[violation] pub struct UnusedVariable { pub name: String, @@ -199,7 +199,7 @@ fn remove_unused_variable( if let Some(target) = targets.iter().find(|target| range == target.range()) { if target.is_name_expr() { return if targets.len() > 1 - || contains_effect(value, |id| checker.semantic_model().is_builtin(id)) + || contains_effect(value, |id| checker.semantic().is_builtin(id)) { // If the expression is complex (`x = foo()`), remove the assignment, // but preserve the right-hand side. @@ -210,13 +210,7 @@ fn remove_unused_variable( Some(Fix::suggested(edit)) } else { // If (e.g.) assigning to a constant (`x = 1`), delete the entire statement. - let edit = delete_stmt( - stmt, - parent, - checker.locator, - checker.indexer, - checker.stylist, - ); + let edit = delete_stmt(stmt, parent, checker.locator, checker.indexer); Some(Fix::suggested(edit).isolate(checker.isolation(parent))) }; } @@ -231,7 +225,7 @@ fn remove_unused_variable( }) = stmt { if target.is_name_expr() { - return if contains_effect(value, |id| checker.semantic_model().is_builtin(id)) { + return if contains_effect(value, |id| checker.semantic().is_builtin(id)) { // If the expression is complex (`x = foo()`), remove the assignment, // but preserve the right-hand side. let start = stmt.start(); @@ -241,22 +235,16 @@ fn remove_unused_variable( Some(Fix::suggested(edit)) } else { // If (e.g.) assigning to a constant (`x = 1`), delete the entire statement. - let edit = delete_stmt( - stmt, - parent, - checker.locator, - checker.indexer, - checker.stylist, - ); + let edit = delete_stmt(stmt, parent, checker.locator, checker.indexer); Some(Fix::suggested(edit).isolate(checker.isolation(parent))) }; } } - // Third case: withitem (`with foo() as x:`) + // Third case: with_item (`with foo() as x:`) if let Stmt::With(ast::StmtWith { items, .. }) = stmt { // Find the binding that matches the given `Range`. - // TODO(charlie): Store the `Withitem` in the `Binding`. + // TODO(charlie): Store the `WithItem` in the `Binding`. for item in items { if let Some(optional_vars) = &item.optional_vars { if optional_vars.range() == range { @@ -285,16 +273,18 @@ fn remove_unused_variable( /// F841 pub(crate) fn unused_variable(checker: &mut Checker, scope: ScopeId) { - let scope = &checker.semantic_model().scopes[scope]; + let scope = &checker.semantic().scopes[scope]; if scope.uses_locals() && scope.kind.is_any_function() { return; } let bindings: Vec<_> = scope .bindings() - .map(|(name, binding_id)| (name, &checker.semantic_model().bindings[binding_id])) + .map(|(name, binding_id)| (name, checker.semantic().binding(binding_id))) .filter_map(|(name, binding)| { if (binding.kind.is_assignment() || binding.kind.is_named_expr_assignment()) + && !binding.is_nonlocal() + && !binding.is_global() && !binding.is_used() && !checker.settings.dummy_variable_rgx.is_match(name) && name != "__tracebackhide__" @@ -313,8 +303,8 @@ pub(crate) fn unused_variable(checker: &mut Checker, scope: ScopeId) { let mut diagnostic = Diagnostic::new(UnusedVariable { name }, range); if checker.patch(diagnostic.kind.rule()) { if let Some(source) = source { - let stmt = checker.semantic_model().stmts[source]; - let parent = checker.semantic_model().stmts.parent(stmt); + let stmt = checker.semantic().stmts[source]; + let parent = checker.semantic().stmts.parent(stmt); if let Some(fix) = remove_unused_variable(stmt, parent, range, checker) { diagnostic.set_fix(fix); } diff --git a/crates/ruff/src/rules/pyflakes/rules/yield_outside_function.rs b/crates/ruff/src/rules/pyflakes/rules/yield_outside_function.rs index baa6cc3b93..f57f092ce5 100644 --- a/crates/ruff/src/rules/pyflakes/rules/yield_outside_function.rs +++ b/crates/ruff/src/rules/pyflakes/rules/yield_outside_function.rs @@ -4,12 +4,12 @@ use rustpython_parser::ast::{Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::scope::ScopeKind; +use ruff_python_semantic::ScopeKind; use crate::checkers::ast::Checker; #[derive(Debug, PartialEq, Eq)] -pub(crate) enum DeferralKeyword { +enum DeferralKeyword { Yield, YieldFrom, Await, @@ -55,7 +55,7 @@ impl Violation for YieldOutsideFunction { pub(crate) fn yield_outside_function(checker: &mut Checker, expr: &Expr) { if matches!( - checker.semantic_model().scope().kind, + checker.semantic().scope().kind, ScopeKind::Class(_) | ScopeKind::Module ) { let keyword = match expr { diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_18.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_18.py.snap index beeffcaedc..d49e67f268 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_18.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_18.py.snap @@ -1,54 +1,22 @@ --- source: crates/ruff/src/rules/pyflakes/mod.rs --- -F401_18.py:7:8: F401 [*] `multiprocessing.connection` imported but unused +F401_18.py:5:12: F401 [*] `__future__` imported but unused | -5 | multiprocessing = None -6 | -7 | import multiprocessing.connection - | ^^^^^^^^^^^^^^^^^^^^^^^^^^ F401 -8 | import multiprocessing.pool -9 | import multiprocessing.queues +4 | def f(): +5 | import __future__ + | ^^^^^^^^^^ F401 | - = help: Remove unused import: `multiprocessing.connection` + = help: Remove unused import: `future` ℹ Fix -4 4 | # imports. It should only detect imports, not any other kind of binding. -5 5 | multiprocessing = None +2 2 | +3 3 | +4 4 | def f(): +5 |- import __future__ + 5 |+ pass 6 6 | -7 |-import multiprocessing.connection -8 7 | import multiprocessing.pool -9 8 | import multiprocessing.queues - -F401_18.py:8:8: F401 [*] `multiprocessing.pool` imported but unused - | -7 | import multiprocessing.connection -8 | import multiprocessing.pool - | ^^^^^^^^^^^^^^^^^^^^ F401 -9 | import multiprocessing.queues - | - = help: Remove unused import: `multiprocessing.pool` - -ℹ Fix -5 5 | multiprocessing = None -6 6 | -7 7 | import multiprocessing.connection -8 |-import multiprocessing.pool -9 8 | import multiprocessing.queues - -F401_18.py:9:8: F401 [*] `multiprocessing.queues` imported but unused - | -7 | import multiprocessing.connection -8 | import multiprocessing.pool -9 | import multiprocessing.queues - | ^^^^^^^^^^^^^^^^^^^^^^ F401 - | - = help: Remove unused import: `multiprocessing.queues` - -ℹ Fix -6 6 | -7 7 | import multiprocessing.connection -8 8 | import multiprocessing.pool -9 |-import multiprocessing.queues +7 7 | +8 8 | def f(): diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_5.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_5.py.snap index 1c7f2d6cab..7be6dbbe89 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_5.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_5.py.snap @@ -18,12 +18,12 @@ F401_5.py:2:17: F401 [*] `a.b.c` imported but unused 4 3 | import h.i 5 4 | import j.k as l -F401_5.py:3:17: F401 [*] `d.e.f` imported but unused +F401_5.py:3:22: F401 [*] `d.e.f` imported but unused | 1 | """Test: removal of multi-segment and aliases imports.""" 2 | from a.b import c 3 | from d.e import f as g - | ^^^^^^ F401 + | ^ F401 4 | import h.i 5 | import j.k as l | @@ -53,12 +53,12 @@ F401_5.py:4:8: F401 [*] `h.i` imported but unused 4 |-import h.i 5 4 | import j.k as l -F401_5.py:5:8: F401 [*] `j.k` imported but unused +F401_5.py:5:15: F401 [*] `j.k` imported but unused | 3 | from d.e import f as g 4 | import h.i 5 | import j.k as l - | ^^^^^^^^ F401 + | ^ F401 | = help: Remove unused import: `j.k` diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_6.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_6.py.snap index bdfda37456..a6b21228c3 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_6.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_6.py.snap @@ -20,11 +20,11 @@ F401_6.py:7:25: F401 [*] `.background.BackgroundTasks` imported but unused 9 8 | # F401 `datastructures.UploadFile` imported but unused 10 9 | from .datastructures import UploadFile as FileUpload -F401_6.py:10:29: F401 [*] `.datastructures.UploadFile` imported but unused +F401_6.py:10:43: F401 [*] `.datastructures.UploadFile` imported but unused | 9 | # F401 `datastructures.UploadFile` imported but unused 10 | from .datastructures import UploadFile as FileUpload - | ^^^^^^^^^^^^^^^^^^^^^^^^ F401 + | ^^^^^^^^^^ F401 11 | 12 | # OK | @@ -58,11 +58,11 @@ F401_6.py:16:8: F401 [*] `background` imported but unused 18 17 | # F401 `datastructures` imported but unused 19 18 | import datastructures as structures -F401_6.py:19:8: F401 [*] `datastructures` imported but unused +F401_6.py:19:26: F401 [*] `datastructures` imported but unused | 18 | # F401 `datastructures` imported but unused 19 | import datastructures as structures - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ F401 + | ^^^^^^^^^^ F401 | = help: Remove unused import: `datastructures` diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F404_F404.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F404_F404.py.snap index 6c63ee705b..feba0f8d20 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F404_F404.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F404_F404.py.snap @@ -11,12 +11,4 @@ F404.py:6:1: F404 `from __future__` imports must occur at the beginning of the f 8 | import __future__ | -F404.py:8:1: F404 `from __future__` imports must occur at the beginning of the file - | -6 | from __future__ import print_function -7 | -8 | import __future__ - | ^^^^^^^^^^^^^^^^^ F404 - | - diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F541_F541.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F541_F541.py.snap index 34c7b466fa..7ca008c27e 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F541_F541.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F541_F541.py.snap @@ -11,7 +11,7 @@ F541.py:6:5: F541 [*] f-string without any placeholders | = help: Remove extraneous `f` prefix -ℹ Suggested fix +ℹ Fix 3 3 | b = f"ghi{'jkl'}" 4 4 | 5 5 | # Errors @@ -32,7 +32,7 @@ F541.py:7:5: F541 [*] f-string without any placeholders | = help: Remove extraneous `f` prefix -ℹ Suggested fix +ℹ Fix 4 4 | 5 5 | # Errors 6 6 | c = f"def" @@ -53,7 +53,7 @@ F541.py:9:5: F541 [*] f-string without any placeholders | = help: Remove extraneous `f` prefix -ℹ Suggested fix +ℹ Fix 6 6 | c = f"def" 7 7 | d = f"def" + "ghi" 8 8 | e = ( @@ -74,7 +74,7 @@ F541.py:13:5: F541 [*] f-string without any placeholders | = help: Remove extraneous `f` prefix -ℹ Suggested fix +ℹ Fix 10 10 | "ghi" 11 11 | ) 12 12 | f = ( @@ -95,7 +95,7 @@ F541.py:14:5: F541 [*] f-string without any placeholders | = help: Remove extraneous `f` prefix -ℹ Suggested fix +ℹ Fix 11 11 | ) 12 12 | f = ( 13 13 | f"a" @@ -116,7 +116,7 @@ F541.py:16:5: F541 [*] f-string without any placeholders | = help: Remove extraneous `f` prefix -ℹ Suggested fix +ℹ Fix 13 13 | f"a" 14 14 | F"b" 15 15 | "c" @@ -137,7 +137,7 @@ F541.py:17:5: F541 [*] f-string without any placeholders | = help: Remove extraneous `f` prefix -ℹ Suggested fix +ℹ Fix 14 14 | F"b" 15 15 | "c" 16 16 | rf"d" @@ -158,7 +158,7 @@ F541.py:19:5: F541 [*] f-string without any placeholders | = help: Remove extraneous `f` prefix -ℹ Suggested fix +ℹ Fix 16 16 | rf"d" 17 17 | fr"e" 18 18 | ) @@ -178,7 +178,7 @@ F541.py:25:13: F541 [*] f-string without any placeholders | = help: Remove extraneous `f` prefix -ℹ Suggested fix +ℹ Fix 22 22 | g = f"ghi{123:{45}}" 23 23 | 24 24 | # Error @@ -198,7 +198,7 @@ F541.py:34:7: F541 [*] f-string without any placeholders | = help: Remove extraneous `f` prefix -ℹ Suggested fix +ℹ Fix 31 31 | f"{f'{v:0.2f}'}" 32 32 | 33 33 | # Errors @@ -219,7 +219,7 @@ F541.py:35:4: F541 [*] f-string without any placeholders | = help: Remove extraneous `f` prefix -ℹ Suggested fix +ℹ Fix 32 32 | 33 33 | # Errors 34 34 | f"{v:{f'0.2f'}}" @@ -240,7 +240,7 @@ F541.py:36:1: F541 [*] f-string without any placeholders | = help: Remove extraneous `f` prefix -ℹ Suggested fix +ℹ Fix 33 33 | # Errors 34 34 | f"{v:{f'0.2f'}}" 35 35 | f"{f''}" @@ -261,7 +261,7 @@ F541.py:37:1: F541 [*] f-string without any placeholders | = help: Remove extraneous `f` prefix -ℹ Suggested fix +ℹ Fix 34 34 | f"{v:{f'0.2f'}}" 35 35 | f"{f''}" 36 36 | f"{{test}}" @@ -269,7 +269,7 @@ F541.py:37:1: F541 [*] f-string without any placeholders 37 |+'{ 40 }' 38 38 | f"{{a {{x}}" 39 39 | f"{{{{x}}}}" -40 40 | +40 40 | ""f"" F541.py:38:1: F541 [*] f-string without any placeholders | @@ -278,18 +278,19 @@ F541.py:38:1: F541 [*] f-string without any placeholders 38 | f"{{a {{x}}" | ^^^^^^^^^^^^ F541 39 | f"{{{{x}}}}" +40 | ""f"" | = help: Remove extraneous `f` prefix -ℹ Suggested fix +ℹ Fix 35 35 | f"{f''}" 36 36 | f"{{test}}" 37 37 | f'{{ 40 }}' 38 |-f"{{a {{x}}" 38 |+"{a {x}" 39 39 | f"{{{{x}}}}" -40 40 | -41 41 | # To be fixed +40 40 | ""f"" +41 41 | ''f"" F541.py:39:1: F541 [*] f-string without any placeholders | @@ -297,19 +298,81 @@ F541.py:39:1: F541 [*] f-string without any placeholders 38 | f"{{a {{x}}" 39 | f"{{{{x}}}}" | ^^^^^^^^^^^^ F541 -40 | -41 | # To be fixed +40 | ""f"" +41 | ''f"" | = help: Remove extraneous `f` prefix -ℹ Suggested fix +ℹ Fix 36 36 | f"{{test}}" 37 37 | f'{{ 40 }}' 38 38 | f"{{a {{x}}" 39 |-f"{{{{x}}}}" 39 |+"{{x}}" -40 40 | -41 41 | # To be fixed -42 42 | # Error: f-string: single '}' is not allowed at line 41 column 8 +40 40 | ""f"" +41 41 | ''f"" +42 42 | (""f""r"") + +F541.py:40:3: F541 [*] f-string without any placeholders + | +38 | f"{{a {{x}}" +39 | f"{{{{x}}}}" +40 | ""f"" + | ^^^ F541 +41 | ''f"" +42 | (""f""r"") + | + = help: Remove extraneous `f` prefix + +ℹ Fix +37 37 | f'{{ 40 }}' +38 38 | f"{{a {{x}}" +39 39 | f"{{{{x}}}}" +40 |-""f"" + 40 |+"" "" +41 41 | ''f"" +42 42 | (""f""r"") +43 43 | + +F541.py:41:3: F541 [*] f-string without any placeholders + | +39 | f"{{{{x}}}}" +40 | ""f"" +41 | ''f"" + | ^^^ F541 +42 | (""f""r"") + | + = help: Remove extraneous `f` prefix + +ℹ Fix +38 38 | f"{{a {{x}}" +39 39 | f"{{{{x}}}}" +40 40 | ""f"" +41 |-''f"" + 41 |+''"" +42 42 | (""f""r"") +43 43 | +44 44 | # To be fixed + +F541.py:42:4: F541 [*] f-string without any placeholders + | +40 | ""f"" +41 | ''f"" +42 | (""f""r"") + | ^^^ F541 +43 | +44 | # To be fixed + | + = help: Remove extraneous `f` prefix + +ℹ Fix +39 39 | f"{{{{x}}}}" +40 40 | ""f"" +41 41 | ''f"" +42 |-(""f""r"") + 42 |+("" ""r"") +43 43 | +44 44 | # To be fixed +45 45 | # Error: f-string: single '}' is not allowed at line 41 column 8 diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F601_F601.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F601_F601.py.snap index f6fa5fdd65..1a5b94a092 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F601_F601.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F601_F601.py.snap @@ -10,6 +10,18 @@ F601.py:3:5: F601 Dictionary key literal `"a"` repeated 4 | "b": 3, 5 | ("a", "b"): 3, | + = help: Remove repeated key literal `"a"` + +F601.py:6:5: F601 Dictionary key literal `("a", "b")` repeated + | +4 | "b": 3, +5 | ("a", "b"): 3, +6 | ("a", "b"): 4, + | ^^^^^^^^^^ F601 +7 | 1.0: 2, +8 | 1: 0, + | + = help: Remove repeated key literal `("a", "b")` F601.py:9:5: F601 Dictionary key literal `1` repeated | @@ -20,6 +32,7 @@ F601.py:9:5: F601 Dictionary key literal `1` repeated 10 | b"123": 1, 11 | b"123": 4, | + = help: Remove repeated key literal `1` F601.py:11:5: F601 Dictionary key literal `b"123"` repeated | @@ -29,6 +42,7 @@ F601.py:11:5: F601 Dictionary key literal `b"123"` repeated | ^^^^^^ F601 12 | } | + = help: Remove repeated key literal `b"123"` F601.py:16:5: F601 Dictionary key literal `"a"` repeated | @@ -39,6 +53,7 @@ F601.py:16:5: F601 Dictionary key literal `"a"` repeated 17 | "a": 3, 18 | "a": 3, | + = help: Remove repeated key literal `"a"` F601.py:17:5: F601 Dictionary key literal `"a"` repeated | @@ -49,6 +64,7 @@ F601.py:17:5: F601 Dictionary key literal `"a"` repeated 18 | "a": 3, 19 | } | + = help: Remove repeated key literal `"a"` F601.py:18:5: F601 [*] Dictionary key literal `"a"` repeated | @@ -78,6 +94,7 @@ F601.py:23:5: F601 Dictionary key literal `"a"` repeated 24 | "a": 3, 25 | "a": 3, | + = help: Remove repeated key literal `"a"` F601.py:24:5: F601 Dictionary key literal `"a"` repeated | @@ -88,6 +105,7 @@ F601.py:24:5: F601 Dictionary key literal `"a"` repeated 25 | "a": 3, 26 | "a": 4, | + = help: Remove repeated key literal `"a"` F601.py:25:5: F601 [*] Dictionary key literal `"a"` repeated | @@ -117,6 +135,7 @@ F601.py:26:5: F601 Dictionary key literal `"a"` repeated | ^^^ F601 27 | } | + = help: Remove repeated key literal `"a"` F601.py:31:5: F601 [*] Dictionary key literal `"a"` repeated | @@ -147,6 +166,7 @@ F601.py:32:5: F601 Dictionary key literal `"a"` repeated 33 | "a": 3, 34 | "a": 4, | + = help: Remove repeated key literal `"a"` F601.py:33:5: F601 Dictionary key literal `"a"` repeated | @@ -157,6 +177,7 @@ F601.py:33:5: F601 Dictionary key literal `"a"` repeated 34 | "a": 4, 35 | } | + = help: Remove repeated key literal `"a"` F601.py:34:5: F601 Dictionary key literal `"a"` repeated | @@ -166,6 +187,7 @@ F601.py:34:5: F601 Dictionary key literal `"a"` repeated | ^^^ F601 35 | } | + = help: Remove repeated key literal `"a"` F601.py:41:5: F601 Dictionary key literal `"a"` repeated | @@ -176,6 +198,7 @@ F601.py:41:5: F601 Dictionary key literal `"a"` repeated 42 | a: 2, 43 | "a": 3, | + = help: Remove repeated key literal `"a"` F601.py:43:5: F601 Dictionary key literal `"a"` repeated | @@ -186,6 +209,7 @@ F601.py:43:5: F601 Dictionary key literal `"a"` repeated 44 | a: 3, 45 | "a": 3, | + = help: Remove repeated key literal `"a"` F601.py:45:5: F601 [*] Dictionary key literal `"a"` repeated | @@ -224,12 +248,16 @@ F601.py:49:14: F601 [*] Dictionary key literal `"a"` repeated 49 |-x = {"a": 1, "a": 1} 49 |+x = {"a": 1} 50 50 | x = {"a": 1, "b": 2, "a": 1} +51 51 | +52 52 | x = { F601.py:50:22: F601 [*] Dictionary key literal `"a"` repeated | 49 | x = {"a": 1, "a": 1} 50 | x = {"a": 1, "b": 2, "a": 1} | ^^^ F601 +51 | +52 | x = { | = help: Remove repeated key literal `"a"` @@ -239,5 +267,18 @@ F601.py:50:22: F601 [*] Dictionary key literal `"a"` repeated 49 49 | x = {"a": 1, "a": 1} 50 |-x = {"a": 1, "b": 2, "a": 1} 50 |+x = {"a": 1, "b": 2} +51 51 | +52 52 | x = { +53 53 | ('a', 'b'): 'asdf', + +F601.py:54:5: F601 Dictionary key literal `('a', 'b')` repeated + | +52 | x = { +53 | ('a', 'b'): 'asdf', +54 | ('a', 'b'): 'qwer', + | ^^^^^^^^^^ F601 +55 | } + | + = help: Remove repeated key literal `('a', 'b')` diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F602_F602.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F602_F602.py.snap index cdbc8c3290..c29e85b114 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F602_F602.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F602_F602.py.snap @@ -10,6 +10,7 @@ F602.py:5:5: F602 Dictionary key `a` repeated 6 | b: 3, 7 | } | + = help: Remove repeated key `a` F602.py:11:5: F602 Dictionary key `a` repeated | @@ -20,6 +21,7 @@ F602.py:11:5: F602 Dictionary key `a` repeated 12 | a: 3, 13 | a: 3, | + = help: Remove repeated key `a` F602.py:12:5: F602 Dictionary key `a` repeated | @@ -30,6 +32,7 @@ F602.py:12:5: F602 Dictionary key `a` repeated 13 | a: 3, 14 | } | + = help: Remove repeated key `a` F602.py:13:5: F602 [*] Dictionary key `a` repeated | @@ -59,6 +62,7 @@ F602.py:18:5: F602 Dictionary key `a` repeated 19 | a: 3, 20 | a: 3, | + = help: Remove repeated key `a` F602.py:19:5: F602 Dictionary key `a` repeated | @@ -69,6 +73,7 @@ F602.py:19:5: F602 Dictionary key `a` repeated 20 | a: 3, 21 | a: 4, | + = help: Remove repeated key `a` F602.py:20:5: F602 [*] Dictionary key `a` repeated | @@ -98,6 +103,7 @@ F602.py:21:5: F602 Dictionary key `a` repeated | ^ F602 22 | } | + = help: Remove repeated key `a` F602.py:26:5: F602 [*] Dictionary key `a` repeated | @@ -128,6 +134,7 @@ F602.py:27:5: F602 Dictionary key `a` repeated 28 | a: 3, 29 | a: 4, | + = help: Remove repeated key `a` F602.py:28:5: F602 Dictionary key `a` repeated | @@ -138,6 +145,7 @@ F602.py:28:5: F602 Dictionary key `a` repeated 29 | a: 4, 30 | } | + = help: Remove repeated key `a` F602.py:29:5: F602 Dictionary key `a` repeated | @@ -147,6 +155,7 @@ F602.py:29:5: F602 Dictionary key `a` repeated | ^ F602 30 | } | + = help: Remove repeated key `a` F602.py:35:5: F602 [*] Dictionary key `a` repeated | @@ -177,6 +186,7 @@ F602.py:37:5: F602 Dictionary key `a` repeated 38 | "a": 3, 39 | a: 3, | + = help: Remove repeated key `a` F602.py:39:5: F602 Dictionary key `a` repeated | @@ -187,6 +197,7 @@ F602.py:39:5: F602 Dictionary key `a` repeated 40 | "a": 3, 41 | a: 4, | + = help: Remove repeated key `a` F602.py:41:5: F602 Dictionary key `a` repeated | @@ -196,6 +207,7 @@ F602.py:41:5: F602 Dictionary key `a` repeated | ^ F602 42 | } | + = help: Remove repeated key `a` F602.py:44:12: F602 [*] Dictionary key `a` repeated | diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F632_F632.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F632_F632.py.snap index e84c43f460..fe6785eab5 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F632_F632.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F632_F632.py.snap @@ -9,7 +9,7 @@ F632.py:1:4: F632 [*] Use `==` to compare constant literals | = help: Replace `is` with `==` -ℹ Suggested fix +ℹ Fix 1 |-if x is "abc": 1 |+if x == "abc": 2 2 | pass @@ -26,7 +26,7 @@ F632.py:4:4: F632 [*] Use `!=` to compare constant literals | = help: Replace `is not` with `!=` -ℹ Suggested fix +ℹ Fix 1 1 | if x is "abc": 2 2 | pass 3 3 | @@ -48,7 +48,7 @@ F632.py:7:4: F632 [*] Use `!=` to compare constant literals | = help: Replace `is not` with `!=` -ℹ Suggested fix +ℹ Fix 4 4 | if 123 is not y: 5 5 | pass 6 6 | @@ -69,7 +69,7 @@ F632.py:11:4: F632 [*] Use `==` to compare constant literals | = help: Replace `is` with `==` -ℹ Suggested fix +ℹ Fix 8 8 | not y: 9 9 | pass 10 10 | @@ -89,7 +89,7 @@ F632.py:14:4: F632 [*] Use `==` to compare constant literals | = help: Replace `is` with `==` -ℹ Suggested fix +ℹ Fix 11 11 | if "123" is x < 3: 12 12 | pass 13 13 | @@ -109,7 +109,7 @@ F632.py:17:4: F632 [*] Use `==` to compare constant literals | = help: Replace `is` with `==` -ℹ Suggested fix +ℹ Fix 14 14 | if "123" != x is 3: 15 15 | pass 16 16 | @@ -129,7 +129,7 @@ F632.py:20:14: F632 [*] Use `==` to compare constant literals | = help: Replace `is` with `==` -ℹ Suggested fix +ℹ Fix 17 17 | if ("123" != x) is 3: 18 18 | pass 19 19 | @@ -152,7 +152,7 @@ F632.py:23:2: F632 [*] Use `!=` to compare constant literals | = help: Replace `is not` with `!=` -ℹ Suggested fix +ℹ Fix 20 20 | if "123" != (x is 3): 21 21 | pass 22 22 | @@ -174,7 +174,7 @@ F632.py:26:2: F632 [*] Use `!=` to compare constant literals | = help: Replace `is not` with `!=` -ℹ Suggested fix +ℹ Fix 23 23 | {2 is 24 24 | not ''} 25 25 | diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F811_F811_1.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F811_F811_1.py.snap index 6f08dc26a2..365bef1c04 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F811_F811_1.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F811_F811_1.py.snap @@ -1,10 +1,10 @@ --- source: crates/ruff/src/rules/pyflakes/mod.rs --- -F811_1.py:1:18: F811 Redefinition of unused `FU` from line 1 +F811_1.py:1:25: F811 Redefinition of unused `FU` from line 1 | 1 | import fu as FU, bar as FU - | ^^^^^^^^^ F811 + | ^^ F811 | diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F811_F811_2.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F811_F811_2.py.snap index 55c6dc663f..bf0275f23e 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F811_F811_2.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F811_F811_2.py.snap @@ -1,10 +1,10 @@ --- source: crates/ruff/src/rules/pyflakes/mod.rs --- -F811_2.py:1:27: F811 Redefinition of unused `FU` from line 1 +F811_2.py:1:34: F811 Redefinition of unused `FU` from line 1 | 1 | from moo import fu as FU, bar as FU - | ^^^^^^^^^ F811 + | ^^ F811 | diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F811_F811_23.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F811_F811_23.py.snap index 9a73c7bf9b..6f9198fc9d 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F811_F811_23.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F811_F811_23.py.snap @@ -1,11 +1,11 @@ --- source: crates/ruff/src/rules/pyflakes/mod.rs --- -F811_23.py:4:8: F811 Redefinition of unused `foo` from line 3 +F811_23.py:4:15: F811 Redefinition of unused `foo` from line 3 | 3 | import foo as foo 4 | import bar as foo - | ^^^^^^^^^^ F811 + | ^^^ F811 | diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F841_F841_0.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F841_F841_0.py.snap index b5faa05575..d6ecba5159 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F841_F841_0.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F841_F841_0.py.snap @@ -11,7 +11,7 @@ F841_0.py:3:22: F841 [*] Local variable `e` is assigned to but never used | = help: Remove assignment to unused variable `e` -ℹ Suggested fix +ℹ Fix 1 1 | try: 2 2 | 1 / 0 3 |-except ValueError as e: diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F841_F841_3.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F841_F841_3.py.snap index c2d8095edb..be49b03119 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F841_F841_3.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F841_F841_3.py.snap @@ -225,7 +225,7 @@ F841_3.py:40:26: F841 [*] Local variable `x1` is assigned to but never used | = help: Remove assignment to unused variable `x1` -ℹ Suggested fix +ℹ Fix 37 37 | def f(): 38 38 | try: 39 39 | 1 / 0 @@ -245,7 +245,7 @@ F841_3.py:45:47: F841 [*] Local variable `x2` is assigned to but never used | = help: Remove assignment to unused variable `x2` -ℹ Suggested fix +ℹ Fix 42 42 | 43 43 | try: 44 44 | 1 / 0 @@ -502,6 +502,9 @@ F841_3.py:110:5: F841 [*] Local variable `toplevel` is assigned to but never use 109 109 | def f(): 110 |- toplevel = tt = 1 110 |+ tt = 1 +111 111 | +112 112 | +113 113 | def f(provided: int) -> int: F841_3.py:110:16: F841 [*] Local variable `tt` is assigned to but never used | @@ -517,5 +520,68 @@ F841_3.py:110:16: F841 [*] Local variable `tt` is assigned to but never used 109 109 | def f(): 110 |- toplevel = tt = 1 110 |+ toplevel = 1 +111 111 | +112 112 | +113 113 | def f(provided: int) -> int: + +F841_3.py:115:19: F841 Local variable `x` is assigned to but never used + | +113 | def f(provided: int) -> int: +114 | match provided: +115 | case [_, *x]: + | ^ F841 +116 | pass + | + = help: Remove assignment to unused variable `x` + +F841_3.py:121:14: F841 Local variable `x` is assigned to but never used + | +119 | def f(provided: int) -> int: +120 | match provided: +121 | case x: + | ^ F841 +122 | pass + | + = help: Remove assignment to unused variable `x` + +F841_3.py:127:18: F841 Local variable `bar` is assigned to but never used + | +125 | def f(provided: int) -> int: +126 | match provided: +127 | case Foo(bar) as x: + | ^^^ F841 +128 | pass + | + = help: Remove assignment to unused variable `bar` + +F841_3.py:127:26: F841 Local variable `x` is assigned to but never used + | +125 | def f(provided: int) -> int: +126 | match provided: +127 | case Foo(bar) as x: + | ^ F841 +128 | pass + | + = help: Remove assignment to unused variable `x` + +F841_3.py:133:27: F841 Local variable `x` is assigned to but never used + | +131 | def f(provided: int) -> int: +132 | match provided: +133 | case {"foo": 0, **x}: + | ^ F841 +134 | pass + | + = help: Remove assignment to unused variable `x` + +F841_3.py:139:17: F841 Local variable `x` is assigned to but never used + | +137 | def f(provided: int) -> int: +138 | match provided: +139 | case {**x}: + | ^ F841 +140 | pass + | + = help: Remove assignment to unused variable `x` diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F901_F901.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F901_F901.py.snap index d9b24f9ee1..a5cac50d13 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F901_F901.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F901_F901.py.snap @@ -9,7 +9,7 @@ F901.py:2:11: F901 [*] `raise NotImplemented` should be `raise NotImplementedErr | = help: Use `raise NotImplementedError` -ℹ Suggested fix +ℹ Fix 1 1 | def f() -> None: 2 |- raise NotImplemented() 2 |+ raise NotImplementedError() @@ -25,7 +25,7 @@ F901.py:6:11: F901 [*] `raise NotImplemented` should be `raise NotImplementedErr | = help: Use `raise NotImplementedError` -ℹ Suggested fix +ℹ Fix 3 3 | 4 4 | 5 5 | def g() -> None: diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__augmented_assignment_after_del.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__augmented_assignment_after_del.snap new file mode 100644 index 0000000000..57a88f04c4 --- /dev/null +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__augmented_assignment_after_del.snap @@ -0,0 +1,21 @@ +--- +source: crates/ruff/src/rules/pyflakes/mod.rs +--- +:10:5: F821 Undefined name `x` + | + 8 | # entirely after the `del` statement. However, it should be an F821 + 9 | # error, because the name is defined in the scope, but unbound. +10 | x += 1 + | ^ F821 + | + +:10:5: F841 Local variable `x` is assigned to but never used + | + 8 | # entirely after the `del` statement. However, it should be an F821 + 9 | # error, because the name is defined in the scope, but unbound. +10 | x += 1 + | ^ F841 + | + = help: Remove assignment to unused variable `x` + + diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__del_shadowed_global_import_in_local_scope.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__del_shadowed_global_import_in_local_scope.snap index 37beaccce1..b2b02e92a7 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__del_shadowed_global_import_in_local_scope.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__del_shadowed_global_import_in_local_scope.snap @@ -17,4 +17,13 @@ source: crates/ruff/src/rules/pyflakes/mod.rs 4 3 | def f(): 5 4 | import os +:5:12: F811 Redefinition of unused `os` from line 2 + | +4 | def f(): +5 | import os + | ^^ F811 +6 | +7 | # Despite this `del`, `import os` in `f` should still be flagged as shadowing an unused + | + diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__del_shadowed_import_shadow_in_local_scope.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__del_shadowed_import_shadow_in_local_scope.snap new file mode 100644 index 0000000000..747002c934 --- /dev/null +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__del_shadowed_import_shadow_in_local_scope.snap @@ -0,0 +1,28 @@ +--- +source: crates/ruff/src/rules/pyflakes/mod.rs +--- +:2:8: F401 [*] `os` imported but unused + | +2 | import os + | ^^ F401 +3 | +4 | def f(): + | + = help: Remove unused import: `os` + +ℹ Fix +1 1 | +2 |-import os +3 2 | +4 3 | def f(): +5 4 | os = 1 + +:5:5: F811 Redefinition of unused `os` from line 2 + | +4 | def f(): +5 | os = 1 + | ^^ F811 +6 | print(os) + | + + diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__double_del.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__double_del.snap new file mode 100644 index 0000000000..ef01e14323 --- /dev/null +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__double_del.snap @@ -0,0 +1,12 @@ +--- +source: crates/ruff/src/rules/pyflakes/mod.rs +--- +:5:9: F821 Undefined name `x` + | +3 | x = 1 +4 | del x +5 | del x + | ^ F821 + | + + diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__f841_dummy_variable_rgx.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__f841_dummy_variable_rgx.snap index e7f674a510..ff53df87e1 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__f841_dummy_variable_rgx.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__f841_dummy_variable_rgx.snap @@ -11,7 +11,7 @@ F841_0.py:3:22: F841 [*] Local variable `e` is assigned to but never used | = help: Remove assignment to unused variable `e` -ℹ Suggested fix +ℹ Fix 1 1 | try: 2 2 | 1 / 0 3 |-except ValueError as e: diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__load_after_multiple_unbinds_from_module_scope.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__load_after_multiple_unbinds_from_module_scope.snap new file mode 100644 index 0000000000..32d31a0137 --- /dev/null +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__load_after_multiple_unbinds_from_module_scope.snap @@ -0,0 +1,44 @@ +--- +source: crates/ruff/src/rules/pyflakes/mod.rs +--- +:7:26: F841 [*] Local variable `x` is assigned to but never used + | +5 | try: +6 | pass +7 | except ValueError as x: + | ^ F841 +8 | pass + | + = help: Remove assignment to unused variable `x` + +ℹ Fix +4 4 | def f(): +5 5 | try: +6 6 | pass +7 |- except ValueError as x: + 7 |+ except ValueError: +8 8 | pass +9 9 | +10 10 | try: + +:12:26: F841 [*] Local variable `x` is assigned to but never used + | +10 | try: +11 | pass +12 | except ValueError as x: + | ^ F841 +13 | pass + | + = help: Remove assignment to unused variable `x` + +ℹ Fix +9 9 | +10 10 | try: +11 11 | pass +12 |- except ValueError as x: + 12 |+ except ValueError: +13 13 | pass +14 14 | +15 15 | # This should resolve to the `x` in `x = 1`. + + diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__load_after_unbind_from_class_scope.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__load_after_unbind_from_class_scope.snap new file mode 100644 index 0000000000..058a92ae6f --- /dev/null +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__load_after_unbind_from_class_scope.snap @@ -0,0 +1,32 @@ +--- +source: crates/ruff/src/rules/pyflakes/mod.rs +--- +:8:30: F841 [*] Local variable `x` is assigned to but never used + | +6 | try: +7 | pass +8 | except ValueError as x: + | ^ F841 +9 | pass + | + = help: Remove assignment to unused variable `x` + +ℹ Fix +5 5 | def f(): +6 6 | try: +7 7 | pass +8 |- except ValueError as x: + 8 |+ except ValueError: +9 9 | pass +10 10 | +11 11 | # This should raise an F821 error, rather than resolving to the + +:13:15: F821 Undefined name `x` + | +11 | # This should raise an F821 error, rather than resolving to the +12 | # `x` in `x = 1`. +13 | print(x) + | ^ F821 + | + + diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__load_after_unbind_from_module_scope.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__load_after_unbind_from_module_scope.snap new file mode 100644 index 0000000000..2f25f19aeb --- /dev/null +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__load_after_unbind_from_module_scope.snap @@ -0,0 +1,24 @@ +--- +source: crates/ruff/src/rules/pyflakes/mod.rs +--- +:7:26: F841 [*] Local variable `x` is assigned to but never used + | +5 | try: +6 | pass +7 | except ValueError as x: + | ^ F841 +8 | pass + | + = help: Remove assignment to unused variable `x` + +ℹ Fix +4 4 | def f(): +5 5 | try: +6 6 | pass +7 |- except ValueError as x: + 7 |+ except ValueError: +8 8 | pass +9 9 | +10 10 | # This should resolve to the `x` in `x = 1`. + + diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__load_after_unbind_from_nested_module_scope.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__load_after_unbind_from_nested_module_scope.snap new file mode 100644 index 0000000000..7ab30bf910 --- /dev/null +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__load_after_unbind_from_nested_module_scope.snap @@ -0,0 +1,44 @@ +--- +source: crates/ruff/src/rules/pyflakes/mod.rs +--- +:7:26: F841 [*] Local variable `x` is assigned to but never used + | +5 | try: +6 | pass +7 | except ValueError as x: + | ^ F841 +8 | pass + | + = help: Remove assignment to unused variable `x` + +ℹ Fix +4 4 | def f(): +5 5 | try: +6 6 | pass +7 |- except ValueError as x: + 7 |+ except ValueError: +8 8 | pass +9 9 | +10 10 | def g(): + +:13:30: F841 [*] Local variable `x` is assigned to but never used + | +11 | try: +12 | pass +13 | except ValueError as x: + | ^ F841 +14 | pass + | + = help: Remove assignment to unused variable `x` + +ℹ Fix +10 10 | def g(): +11 11 | try: +12 12 | pass +13 |- except ValueError as x: + 13 |+ except ValueError: +14 14 | pass +15 15 | +16 16 | # This should resolve to the `x` in `x = 1`. + + diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__multi_statement_lines.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__multi_statement_lines.snap index 00f0e38f00..f384bf81ca 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__multi_statement_lines.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__multi_statement_lines.snap @@ -1,257 +1,325 @@ --- source: crates/ruff/src/rules/pyflakes/mod.rs --- -multi_statement_lines.py:3:12: F401 [*] `foo1` imported but unused +multi_statement_lines.py:2:12: F401 [*] `foo1` imported but unused | -2 | if True: -3 | import foo1; x = 1 +1 | if True: +2 | import foo1; x = 1 | ^^^^ F401 -4 | import foo2; x = 1 +3 | import foo2; x = 1 | = help: Remove unused import: `foo1` ℹ Fix -1 1 | -2 2 | if True: -3 |- import foo1; x = 1 - 3 |+ x = 1 -4 4 | import foo2; x = 1 -5 5 | -6 6 | if True: +1 1 | if True: +2 |- import foo1; x = 1 + 2 |+ x = 1 +3 3 | import foo2; x = 1 +4 4 | +5 5 | if True: -multi_statement_lines.py:4:12: F401 [*] `foo2` imported but unused +multi_statement_lines.py:3:12: F401 [*] `foo2` imported but unused | -2 | if True: -3 | import foo1; x = 1 -4 | import foo2; x = 1 +1 | if True: +2 | import foo1; x = 1 +3 | import foo2; x = 1 | ^^^^ F401 -5 | -6 | if True: +4 | +5 | if True: | = help: Remove unused import: `foo2` ℹ Fix -1 1 | -2 2 | if True: -3 3 | import foo1; x = 1 -4 |- import foo2; x = 1 - 4 |+ x = 1 -5 5 | -6 6 | if True: -7 7 | import foo3; \ +1 1 | if True: +2 2 | import foo1; x = 1 +3 |- import foo2; x = 1 + 3 |+ x = 1 +4 4 | +5 5 | if True: +6 6 | import foo3; \ -multi_statement_lines.py:7:12: F401 [*] `foo3` imported but unused +multi_statement_lines.py:6:12: F401 [*] `foo3` imported but unused | -6 | if True: -7 | import foo3; \ +5 | if True: +6 | import foo3; \ | ^^^^ F401 -8 | x = 1 +7 | x = 1 | = help: Remove unused import: `foo3` ℹ Fix -4 4 | import foo2; x = 1 -5 5 | -6 6 | if True: -7 |- import foo3; \ -8 |-x = 1 - 7 |+ x = 1 -9 8 | -10 9 | if True: -11 10 | import foo4 \ +3 3 | import foo2; x = 1 +4 4 | +5 5 | if True: +6 |- import foo3; \ +7 |-x = 1 + 6 |+ x = 1 +8 7 | +9 8 | if True: +10 9 | import foo4 \ -multi_statement_lines.py:11:12: F401 [*] `foo4` imported but unused +multi_statement_lines.py:10:12: F401 [*] `foo4` imported but unused | -10 | if True: -11 | import foo4 \ + 9 | if True: +10 | import foo4 \ | ^^^^ F401 -12 | ; x = 1 +11 | ; x = 1 | = help: Remove unused import: `foo4` ℹ Fix -8 8 | x = 1 -9 9 | -10 10 | if True: -11 |- import foo4 \ -12 |- ; x = 1 - 11 |+ x = 1 -13 12 | -14 13 | -15 14 | if True: +7 7 | x = 1 +8 8 | +9 9 | if True: +10 |- import foo4 \ +11 |- ; x = 1 + 10 |+ x = 1 +12 11 | +13 12 | if True: +14 13 | x = 1; import foo5 -multi_statement_lines.py:16:19: F401 [*] `foo5` imported but unused +multi_statement_lines.py:14:19: F401 [*] `foo5` imported but unused | -15 | if True: -16 | x = 1; import foo5 +13 | if True: +14 | x = 1; import foo5 | ^^^^ F401 | = help: Remove unused import: `foo5` ℹ Fix -13 13 | -14 14 | -15 15 | if True: -16 |- x = 1; import foo5 - 16 |+ x = 1; -17 17 | -18 18 | -19 19 | if True: +11 11 | ; x = 1 +12 12 | +13 13 | if True: +14 |- x = 1; import foo5 + 14 |+ x = 1; +15 15 | +16 16 | +17 17 | if True: -multi_statement_lines.py:21:17: F401 [*] `foo6` imported but unused +multi_statement_lines.py:19:17: F401 [*] `foo6` imported but unused | -19 | if True: -20 | x = 1; \ -21 | import foo6 +17 | if True: +18 | x = 1; \ +19 | import foo6 | ^^^^ F401 +20 | +21 | if True: | = help: Remove unused import: `foo6` ℹ Fix -18 18 | -19 19 | if True: -20 20 | x = 1; \ -21 |- import foo6 - 21 |+ -22 22 | -23 23 | -24 24 | if True: +15 15 | +16 16 | +17 17 | if True: +18 |- x = 1; \ +19 |- import foo6 + 18 |+ x = 1; +20 19 | +21 20 | if True: +22 21 | x = 1 \ -multi_statement_lines.py:26:18: F401 [*] `foo7` imported but unused +multi_statement_lines.py:23:18: F401 [*] `foo7` imported but unused | -24 | if True: -25 | x = 1 \ -26 | ; import foo7 +21 | if True: +22 | x = 1 \ +23 | ; import foo7 | ^^^^ F401 +24 | +25 | if True: | = help: Remove unused import: `foo7` ℹ Fix -23 23 | -24 24 | if True: -25 25 | x = 1 \ -26 |- ; import foo7 - 26 |+ ; -27 27 | -28 28 | -29 29 | if True: +20 20 | +21 21 | if True: +22 22 | x = 1 \ +23 |- ; import foo7 + 23 |+ ; +24 24 | +25 25 | if True: +26 26 | x = 1; import foo8; x = 1 -multi_statement_lines.py:30:19: F401 [*] `foo8` imported but unused +multi_statement_lines.py:26:19: F401 [*] `foo8` imported but unused | -29 | if True: -30 | x = 1; import foo8; x = 1 +25 | if True: +26 | x = 1; import foo8; x = 1 | ^^^^ F401 -31 | x = 1; import foo9; x = 1 +27 | x = 1; import foo9; x = 1 | = help: Remove unused import: `foo8` ℹ Fix -27 27 | +23 23 | ; import foo7 +24 24 | +25 25 | if True: +26 |- x = 1; import foo8; x = 1 + 26 |+ x = 1; x = 1 +27 27 | x = 1; import foo9; x = 1 28 28 | 29 29 | if True: -30 |- x = 1; import foo8; x = 1 - 30 |+ x = 1; x = 1 -31 31 | x = 1; import foo9; x = 1 -32 32 | -33 33 | if True: -multi_statement_lines.py:31:23: F401 [*] `foo9` imported but unused +multi_statement_lines.py:27:23: F401 [*] `foo9` imported but unused | -29 | if True: -30 | x = 1; import foo8; x = 1 -31 | x = 1; import foo9; x = 1 +25 | if True: +26 | x = 1; import foo8; x = 1 +27 | x = 1; import foo9; x = 1 | ^^^^ F401 -32 | -33 | if True: +28 | +29 | if True: | = help: Remove unused import: `foo9` ℹ Fix +24 24 | +25 25 | if True: +26 26 | x = 1; import foo8; x = 1 +27 |- x = 1; import foo9; x = 1 + 27 |+ x = 1; x = 1 28 28 | 29 29 | if True: -30 30 | x = 1; import foo8; x = 1 -31 |- x = 1; import foo9; x = 1 - 31 |+ x = 1; x = 1 -32 32 | -33 33 | if True: -34 34 | x = 1; \ +30 30 | x = 1; \ -multi_statement_lines.py:35:16: F401 [*] `foo10` imported but unused +multi_statement_lines.py:31:16: F401 [*] `foo10` imported but unused | -33 | if True: -34 | x = 1; \ -35 | import foo10; \ +29 | if True: +30 | x = 1; \ +31 | import foo10; \ | ^^^^^ F401 -36 | x = 1 +32 | x = 1 | = help: Remove unused import: `foo10` ℹ Fix -32 32 | -33 33 | if True: -34 34 | x = 1; \ -35 |- import foo10; \ -36 |- x = 1 - 35 |+ x = 1 -37 36 | -38 37 | if True: -39 38 | x = 1 \ +28 28 | +29 29 | if True: +30 30 | x = 1; \ +31 |- import foo10; \ +32 |- x = 1 + 31 |+ x = 1 +33 32 | +34 33 | if True: +35 34 | x = 1 \ -multi_statement_lines.py:40:17: F401 [*] `foo11` imported but unused +multi_statement_lines.py:36:17: F401 [*] `foo11` imported but unused | -38 | if True: -39 | x = 1 \ -40 | ;import foo11 \ +34 | if True: +35 | x = 1 \ +36 | ;import foo11 \ | ^^^^^ F401 -41 | ;x = 1 +37 | ;x = 1 | = help: Remove unused import: `foo11` ℹ Fix -37 37 | -38 38 | if True: -39 39 | x = 1 \ -40 |- ;import foo11 \ -41 40 | ;x = 1 -42 41 | -43 42 | +33 33 | +34 34 | if True: +35 35 | x = 1 \ +36 |- ;import foo11 \ +37 36 | ;x = 1 +38 37 | +39 38 | if True: -multi_statement_lines.py:46:8: F401 [*] `foo12` imported but unused +multi_statement_lines.py:42:16: F401 [*] `foo12` imported but unused | -44 | # Continuation, but not as the last content in the file. -45 | x = 1; \ -46 | import foo12 - | ^^^^^ F401 -47 | -48 | # Continuation, followed by end-of-file. (Removing `import foo` would cause a syntax +40 | x = 1; \ +41 | \ +42 | import foo12 + | ^^^^^ F401 +43 | +44 | if True: | = help: Remove unused import: `foo12` ℹ Fix -43 43 | -44 44 | # Continuation, but not as the last content in the file. -45 45 | x = 1; \ -46 |-import foo12 -47 46 | - 47 |+ -48 48 | # Continuation, followed by end-of-file. (Removing `import foo` would cause a syntax -49 49 | # error.) -50 50 | x = 1; \ +37 37 | ;x = 1 +38 38 | +39 39 | if True: +40 |- x = 1; \ +41 |- \ +42 |- import foo12 + 40 |+ x = 1; +43 41 | +44 42 | if True: +45 43 | x = 1; \ -multi_statement_lines.py:51:8: F401 [*] `foo13` imported but unused +multi_statement_lines.py:47:12: F401 [*] `foo13` imported but unused | -49 | # error.) -50 | x = 1; \ -51 | import foo13 - | ^^^^^ F401 +45 | x = 1; \ +46 | \ +47 | import foo13 + | ^^^^^ F401 | = help: Remove unused import: `foo13` ℹ Fix -48 48 | # Continuation, followed by end-of-file. (Removing `import foo` would cause a syntax -49 49 | # error.) -50 50 | x = 1; \ -51 |-import foo13 - 51 |+ +42 42 | import foo12 +43 43 | +44 44 | if True: +45 |- x = 1; \ +46 |-\ +47 |- import foo13 + 45 |+ x = 1; +48 46 | +49 47 | +50 48 | if True: + +multi_statement_lines.py:53:12: F401 [*] `foo14` imported but unused + | +51 | x = 1; \ +52 | # \ +53 | import foo14 + | ^^^^^ F401 +54 | +55 | # Continuation, but not as the last content in the file. + | + = help: Remove unused import: `foo14` + +ℹ Fix +50 50 | if True: +51 51 | x = 1; \ +52 52 | # \ +53 |- import foo14 +54 53 | +55 54 | # Continuation, but not as the last content in the file. +56 55 | x = 1; \ + +multi_statement_lines.py:57:8: F401 [*] `foo15` imported but unused + | +55 | # Continuation, but not as the last content in the file. +56 | x = 1; \ +57 | import foo15 + | ^^^^^ F401 +58 | +59 | # Continuation, followed by end-of-file. (Removing `import foo` would cause a syntax + | + = help: Remove unused import: `foo15` + +ℹ Fix +53 53 | import foo14 +54 54 | +55 55 | # Continuation, but not as the last content in the file. +56 |-x = 1; \ +57 |-import foo15 + 56 |+x = 1; +58 57 | +59 58 | # Continuation, followed by end-of-file. (Removing `import foo` would cause a syntax +60 59 | # error.) + +multi_statement_lines.py:62:8: F401 [*] `foo16` imported but unused + | +60 | # error.) +61 | x = 1; \ +62 | import foo16 + | ^^^^^ F401 + | + = help: Remove unused import: `foo16` + +ℹ Fix +58 58 | +59 59 | # Continuation, followed by end-of-file. (Removing `import foo` would cause a syntax +60 60 | # error.) +61 |-x = 1; \ +62 |-import foo16 + 61 |+x = 1; diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__print_in_body_after_double_shadowing_except.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__print_in_body_after_double_shadowing_except.snap new file mode 100644 index 0000000000..616577852c --- /dev/null +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__print_in_body_after_double_shadowing_except.snap @@ -0,0 +1,45 @@ +--- +source: crates/ruff/src/rules/pyflakes/mod.rs +--- +:7:26: F841 [*] Local variable `x` is assigned to but never used + | +5 | try: +6 | 1 / 0 +7 | except ValueError as x: + | ^ F841 +8 | pass +9 | except ImportError as x: + | + = help: Remove assignment to unused variable `x` + +ℹ Fix +4 4 | +5 5 | try: +6 6 | 1 / 0 +7 |- except ValueError as x: + 7 |+ except ValueError: +8 8 | pass +9 9 | except ImportError as x: +10 10 | pass + +:9:27: F841 [*] Local variable `x` is assigned to but never used + | + 7 | except ValueError as x: + 8 | pass + 9 | except ImportError as x: + | ^ F841 +10 | pass + | + = help: Remove assignment to unused variable `x` + +ℹ Fix +6 6 | 1 / 0 +7 7 | except ValueError as x: +8 8 | pass +9 |- except ImportError as x: + 9 |+ except ImportError: +10 10 | pass +11 11 | +12 12 | # No error here, though it should arguably be an F821 error. `x` will + + diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__print_in_body_after_shadowing_except.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__print_in_body_after_shadowing_except.snap new file mode 100644 index 0000000000..085cb7c453 --- /dev/null +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__print_in_body_after_shadowing_except.snap @@ -0,0 +1,24 @@ +--- +source: crates/ruff/src/rules/pyflakes/mod.rs +--- +:7:25: F841 [*] Local variable `x` is assigned to but never used + | +5 | try: +6 | 1 / 0 +7 | except Exception as x: + | ^ F841 +8 | pass + | + = help: Remove assignment to unused variable `x` + +ℹ Fix +4 4 | +5 5 | try: +6 6 | 1 / 0 +7 |- except Exception as x: + 7 |+ except Exception: +8 8 | pass +9 9 | +10 10 | # No error here, though it should arguably be an F821 error. `x` will + + diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__print_in_if_else_after_shadowing_except.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__print_in_if_else_after_shadowing_except.snap new file mode 100644 index 0000000000..1976c4331d --- /dev/null +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__print_in_if_else_after_shadowing_except.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/pyflakes/mod.rs +--- + diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__print_in_try_else_after_shadowing_except.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__print_in_try_else_after_shadowing_except.snap new file mode 100644 index 0000000000..1976c4331d --- /dev/null +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__print_in_try_else_after_shadowing_except.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/pyflakes/mod.rs +--- + diff --git a/crates/ruff/src/rules/pygrep_hooks/rules/deprecated_log_warn.rs b/crates/ruff/src/rules/pygrep_hooks/rules/deprecated_log_warn.rs index ba941d1d85..19765c7876 100644 --- a/crates/ruff/src/rules/pygrep_hooks/rules/deprecated_log_warn.rs +++ b/crates/ruff/src/rules/pygrep_hooks/rules/deprecated_log_warn.rs @@ -30,7 +30,7 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/logging.html#logging.Logger.warning) +/// - [Python documentation: `logger.Logger.warning`](https://docs.python.org/3/library/logging.html#logging.Logger.warning) #[violation] pub struct DeprecatedLogWarn; @@ -44,10 +44,10 @@ impl Violation for DeprecatedLogWarn { /// PGH002 pub(crate) fn deprecated_log_warn(checker: &mut Checker, func: &Expr) { if checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["logging", "warn"] + matches!(call_path.as_slice(), ["logging", "warn"]) }) { checker diff --git a/crates/ruff/src/rules/pygrep_hooks/rules/mod.rs b/crates/ruff/src/rules/pygrep_hooks/rules/mod.rs index b8fbfb773c..1126d348de 100644 --- a/crates/ruff/src/rules/pygrep_hooks/rules/mod.rs +++ b/crates/ruff/src/rules/pygrep_hooks/rules/mod.rs @@ -1,10 +1,8 @@ -pub(crate) use blanket_noqa::{blanket_noqa, BlanketNOQA}; -pub(crate) use blanket_type_ignore::{blanket_type_ignore, BlanketTypeIgnore}; -pub(crate) use deprecated_log_warn::{deprecated_log_warn, DeprecatedLogWarn}; -pub(crate) use invalid_mock_access::{ - non_existent_mock_method, uncalled_mock_method, InvalidMockAccess, -}; -pub(crate) use no_eval::{no_eval, Eval}; +pub(crate) use blanket_noqa::*; +pub(crate) use blanket_type_ignore::*; +pub(crate) use deprecated_log_warn::*; +pub(crate) use invalid_mock_access::*; +pub(crate) use no_eval::*; mod blanket_noqa; mod blanket_type_ignore; diff --git a/crates/ruff/src/rules/pygrep_hooks/rules/no_eval.rs b/crates/ruff/src/rules/pygrep_hooks/rules/no_eval.rs index 40b224a22c..a5d8572f95 100644 --- a/crates/ruff/src/rules/pygrep_hooks/rules/no_eval.rs +++ b/crates/ruff/src/rules/pygrep_hooks/rules/no_eval.rs @@ -6,7 +6,7 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; /// ## What it does -/// Checks for usages of the builtin `eval()` function. +/// Checks for uses of the builtin `eval()` function. /// /// ## Why is this bad? /// The `eval()` function is insecure as it enables arbitrary code execution. @@ -26,7 +26,7 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/functions.html#eval) +/// - [Python documentation: `eval`](https://docs.python.org/3/library/functions.html#eval) /// - [_Eval really is dangerous_ by Ned Batchelder](https://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html) #[violation] pub struct Eval; @@ -46,7 +46,7 @@ pub(crate) fn no_eval(checker: &mut Checker, func: &Expr) { if id != "eval" { return; } - if !checker.semantic_model().is_builtin("eval") { + if !checker.semantic().is_builtin("eval") { return; } checker diff --git a/crates/ruff/src/rules/pylint/helpers.rs b/crates/ruff/src/rules/pylint/helpers.rs index 306737b28c..02bbd98207 100644 --- a/crates/ruff/src/rules/pylint/helpers.rs +++ b/crates/ruff/src/rules/pylint/helpers.rs @@ -1,75 +1,98 @@ -use ruff_python_semantic::analyze::function_type; -use ruff_python_semantic::analyze::function_type::FunctionType; -use ruff_python_semantic::model::SemanticModel; -use ruff_python_semantic::scope::ScopeKind; -use rustpython_parser::ast; -use rustpython_parser::ast::Cmpop; use std::fmt; +use rustpython_parser::ast; +use rustpython_parser::ast::{CmpOp, Constant, Expr, Keyword}; + +use ruff_python_semantic::analyze::function_type; +use ruff_python_semantic::{ScopeKind, SemanticModel}; + use crate::settings::Settings; -pub(super) fn in_dunder_init(model: &SemanticModel, settings: &Settings) -> bool { - let scope = model.scope(); - let ( - ScopeKind::Function(ast::StmtFunctionDef { - name, - decorator_list, - .. - }) | - ScopeKind::AsyncFunction(ast::StmtAsyncFunctionDef { - name, - decorator_list, - .. +/// Returns the value of the `name` parameter to, e.g., a `TypeVar` constructor. +pub(super) fn type_param_name<'a>(args: &'a [Expr], keywords: &'a [Keyword]) -> Option<&'a str> { + // Handle both `TypeVar("T")` and `TypeVar(name="T")`. + let name_param = keywords + .iter() + .find(|keyword| { + keyword + .arg + .as_ref() + .map_or(false, |keyword| keyword.as_str() == "name") }) - ) = scope.kind else { + .map(|keyword| &keyword.value) + .or_else(|| args.get(0))?; + if let Expr::Constant(ast::ExprConstant { + value: Constant::Str(name), + .. + }) = &name_param + { + Some(name) + } else { + None + } +} + +pub(super) fn in_dunder_init(semantic: &SemanticModel, settings: &Settings) -> bool { + let scope = semantic.scope(); + let (ScopeKind::Function(ast::StmtFunctionDef { + name, + decorator_list, + .. + }) + | ScopeKind::AsyncFunction(ast::StmtAsyncFunctionDef { + name, + decorator_list, + .. + })) = scope.kind + else { return false; }; if name != "__init__" { return false; } - let Some(parent) = scope.parent.map(|scope_id| &model.scopes[scope_id]) else { + let Some(parent) = scope.parent.map(|scope_id| &semantic.scopes[scope_id]) else { return false; }; if !matches!( function_type::classify( - model, - parent, name, decorator_list, + parent, + semantic, &settings.pep8_naming.classmethod_decorators, &settings.pep8_naming.staticmethod_decorators, ), - FunctionType::Method + function_type::FunctionType::Method ) { return false; } true } -/// A wrapper around [`Cmpop`] that implements `Display`. +/// A wrapper around [`CmpOp`] that implements `Display`. #[derive(Debug)] -pub(super) struct CmpopExt(Cmpop); +pub(super) struct CmpOpExt(CmpOp); -impl From<&Cmpop> for CmpopExt { - fn from(cmpop: &Cmpop) -> Self { - CmpopExt(*cmpop) +impl From<&CmpOp> for CmpOpExt { + fn from(cmp_op: &CmpOp) -> Self { + CmpOpExt(*cmp_op) } } -impl fmt::Display for CmpopExt { +impl fmt::Display for CmpOpExt { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let representation = match self.0 { - Cmpop::Eq => "==", - Cmpop::NotEq => "!=", - Cmpop::Lt => "<", - Cmpop::LtE => "<=", - Cmpop::Gt => ">", - Cmpop::GtE => ">=", - Cmpop::Is => "is", - Cmpop::IsNot => "is not", - Cmpop::In => "in", - Cmpop::NotIn => "not in", + CmpOp::Eq => "==", + CmpOp::NotEq => "!=", + CmpOp::Lt => "<", + CmpOp::LtE => "<=", + CmpOp::Gt => ">", + CmpOp::GtE => ">=", + CmpOp::Is => "is", + CmpOp::IsNot => "is not", + CmpOp::In => "in", + CmpOp::NotIn => "not in", }; write!(f, "{representation}") } diff --git a/crates/ruff/src/rules/pylint/mod.rs b/crates/ruff/src/rules/pylint/mod.rs index c99c843d06..c182a14b23 100644 --- a/crates/ruff/src/rules/pylint/mod.rs +++ b/crates/ruff/src/rules/pylint/mod.rs @@ -33,6 +33,7 @@ mod tests { )] #[test_case(Rule::ComparisonWithItself, Path::new("comparison_with_itself.py"))] #[test_case(Rule::ManualFromImport, Path::new("import_aliasing.py"))] + #[test_case(Rule::SingleStringSlots, Path::new("single_string_slots.py"))] #[test_case(Rule::SysExitAlias, Path::new("sys_exit_alias_0.py"))] #[test_case(Rule::SysExitAlias, Path::new("sys_exit_alias_1.py"))] #[test_case(Rule::SysExitAlias, Path::new("sys_exit_alias_2.py"))] @@ -84,6 +85,12 @@ mod tests { Path::new("too_many_return_statements.py") )] #[test_case(Rule::TooManyStatements, Path::new("too_many_statements.py"))] + #[test_case(Rule::TypeBivariance, Path::new("type_bivariance.py"))] + #[test_case( + Rule::TypeNameIncorrectVariance, + Path::new("type_name_incorrect_variance.py") + )] + #[test_case(Rule::TypeParamNameMismatch, Path::new("type_param_name_mismatch.py"))] #[test_case( Rule::UnexpectedSpecialMethodSignature, Path::new("unexpected_special_method_signature.py") @@ -105,6 +112,10 @@ mod tests { )] #[test_case(Rule::YieldInInit, Path::new("yield_in_init.py"))] #[test_case(Rule::NestedMinMax, Path::new("nested_min_max.py"))] + #[test_case( + Rule::RepeatedEqualityComparisonTarget, + Path::new("repeated_equality_comparison_target.py") + )] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); let diagnostics = test_path( diff --git a/crates/ruff/src/rules/pylint/rules/await_outside_async.rs b/crates/ruff/src/rules/pylint/rules/await_outside_async.rs index 0a7c2936aa..850cf7a9f7 100644 --- a/crates/ruff/src/rules/pylint/rules/await_outside_async.rs +++ b/crates/ruff/src/rules/pylint/rules/await_outside_async.rs @@ -6,7 +6,7 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; /// ## What it does -/// Checks for usages of `await` outside of `async` functions. +/// Checks for uses of `await` outside of `async` functions. /// /// ## Why is this bad? /// Using `await` outside of an `async` function is a syntax error. @@ -30,7 +30,7 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/reference/expressions.html#await) +/// - [Python documentation: Await expression](https://docs.python.org/3/reference/expressions.html#await) /// - [PEP 492](https://peps.python.org/pep-0492/#await-expression) #[violation] pub struct AwaitOutsideAsync; @@ -44,7 +44,7 @@ impl Violation for AwaitOutsideAsync { /// PLE1142 pub(crate) fn await_outside_async(checker: &mut Checker, expr: &Expr) { - if !checker.semantic_model().in_async_context() { + if !checker.semantic().in_async_context() { checker .diagnostics .push(Diagnostic::new(AwaitOutsideAsync, expr.range())); diff --git a/crates/ruff/src/rules/pylint/rules/bad_str_strip_call.rs b/crates/ruff/src/rules/pylint/rules/bad_str_strip_call.rs index 4e961f434b..e394928bcd 100644 --- a/crates/ruff/src/rules/pylint/rules/bad_str_strip_call.rs +++ b/crates/ruff/src/rules/pylint/rules/bad_str_strip_call.rs @@ -17,18 +17,32 @@ use crate::settings::types::PythonVersion; /// trailing ends of the string. Including duplicate characters in the call /// is redundant and often indicative of a mistake. /// +/// In Python 3.9 and later, you can use `str#removeprefix` and +/// `str#removesuffix` to remove an exact prefix or suffix from a string, +/// respectively, which should be preferred when possible. +/// /// ## Example /// ```python -/// "bar foo baz".strip("bar baz ") # "foo" +/// # Evaluates to "foo". +/// "bar foo baz".strip("bar baz ") /// ``` /// /// Use instead: /// ```python +/// # Evaluates to "foo". /// "bar foo baz".strip("abrz ") # "foo" /// ``` /// +/// Or: +/// ```python +/// # Evaluates to "foo". +/// "bar foo baz".removeprefix("bar ").removesuffix(" baz") +/// +/// ## Options +/// - `target-version` +/// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/stdtypes.html?highlight=strip#str.strip) +/// - [Python documentation: `str.strip`](https://docs.python.org/3/library/stdtypes.html?highlight=strip#str.strip) #[violation] pub struct BadStrStripCall { strip: StripKind, diff --git a/crates/ruff/src/rules/pylint/rules/bad_string_format_type.rs b/crates/ruff/src/rules/pylint/rules/bad_string_format_type.rs index 137b346cda..9eb1ec8865 100644 --- a/crates/ruff/src/rules/pylint/rules/bad_string_format_type.rs +++ b/crates/ruff/src/rules/pylint/rules/bad_string_format_type.rs @@ -4,7 +4,7 @@ use ruff_text_size::TextRange; use rustc_hash::FxHashMap; use rustpython_format::cformat::{CFormatPart, CFormatSpec, CFormatStrOrBytes, CFormatString}; use rustpython_parser::ast::{self, Constant, Expr, Ranged}; -use rustpython_parser::{lexer, Mode, Tok}; +use rustpython_parser::{lexer, Mode}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -205,9 +205,9 @@ pub(crate) fn bad_string_format_type(checker: &mut Checker, expr: &Expr, right: let content = checker.locator.slice(expr.range()); let mut strings: Vec = vec![]; for (tok, range) in lexer::lex_starts_at(content, Mode::Module, expr.start()).flatten() { - if matches!(tok, Tok::String { .. }) { + if tok.is_string() { strings.push(range); - } else if matches!(tok, Tok::Percent) { + } else if tok.is_percent() { // Break as soon as we find the modulo symbol. break; } diff --git a/crates/ruff/src/rules/pylint/rules/binary_op_exception.rs b/crates/ruff/src/rules/pylint/rules/binary_op_exception.rs index 8ce1fc2bfd..fc871a02ec 100644 --- a/crates/ruff/src/rules/pylint/rules/binary_op_exception.rs +++ b/crates/ruff/src/rules/pylint/rules/binary_op_exception.rs @@ -1,4 +1,4 @@ -use rustpython_parser::ast::{self, Excepthandler, Expr, Ranged}; +use rustpython_parser::ast::{self, ExceptHandler, Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -6,16 +6,16 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; #[derive(Debug, PartialEq, Eq, Copy, Clone)] -enum Boolop { +enum BoolOp { And, Or, } -impl From<&ast::Boolop> for Boolop { - fn from(op: &ast::Boolop) -> Self { +impl From<&ast::BoolOp> for BoolOp { + fn from(op: &ast::BoolOp) -> Self { match op { - ast::Boolop::And => Boolop::And, - ast::Boolop::Or => Boolop::Or, + ast::BoolOp::And => BoolOp::And, + ast::BoolOp::Or => BoolOp::Or, } } } @@ -47,7 +47,7 @@ impl From<&ast::Boolop> for Boolop { /// ``` #[violation] pub struct BinaryOpException { - op: Boolop, + op: BoolOp, } impl Violation for BinaryOpException { @@ -55,15 +55,16 @@ impl Violation for BinaryOpException { fn message(&self) -> String { let BinaryOpException { op } = self; match op { - Boolop::And => format!("Exception to catch is the result of a binary `and` operation"), - Boolop::Or => format!("Exception to catch is the result of a binary `or` operation"), + BoolOp::And => format!("Exception to catch is the result of a binary `and` operation"), + BoolOp::Or => format!("Exception to catch is the result of a binary `or` operation"), } } } /// PLW0711 -pub(crate) fn binary_op_exception(checker: &mut Checker, excepthandler: &Excepthandler) { - let Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { type_, .. }) = excepthandler; +pub(crate) fn binary_op_exception(checker: &mut Checker, except_handler: &ExceptHandler) { + let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { type_, .. }) = + except_handler; let Some(type_) = type_ else { return; diff --git a/crates/ruff/src/rules/pylint/rules/collapsible_else_if.rs b/crates/ruff/src/rules/pylint/rules/collapsible_else_if.rs index d53fd72869..57a1bf8405 100644 --- a/crates/ruff/src/rules/pylint/rules/collapsible_else_if.rs +++ b/crates/ruff/src/rules/pylint/rules/collapsible_else_if.rs @@ -35,7 +35,7 @@ use ruff_python_ast::source_code::Locator; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/tutorial/controlflow.html#if-statements) +/// - [Python documentation: `if` Statements](https://docs.python.org/3/tutorial/controlflow.html#if-statements) #[violation] pub struct CollapsibleElseIf; diff --git a/crates/ruff/src/rules/pylint/rules/compare_to_empty_string.rs b/crates/ruff/src/rules/pylint/rules/compare_to_empty_string.rs index 8cde351c82..3e2b96e0a4 100644 --- a/crates/ruff/src/rules/pylint/rules/compare_to_empty_string.rs +++ b/crates/ruff/src/rules/pylint/rules/compare_to_empty_string.rs @@ -1,55 +1,12 @@ use anyhow::bail; use itertools::Itertools; -use rustpython_parser::ast::{self, Cmpop, Constant, Expr, Ranged}; +use rustpython_parser::ast::{self, CmpOp, Constant, Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; -#[derive(Debug, PartialEq, Eq, Copy, Clone)] -pub(crate) enum EmptyStringCmpop { - Is, - IsNot, - Eq, - NotEq, -} - -impl TryFrom<&Cmpop> for EmptyStringCmpop { - type Error = anyhow::Error; - - fn try_from(value: &Cmpop) -> Result { - match value { - Cmpop::Is => Ok(Self::Is), - Cmpop::IsNot => Ok(Self::IsNot), - Cmpop::Eq => Ok(Self::Eq), - Cmpop::NotEq => Ok(Self::NotEq), - _ => bail!("{value:?} cannot be converted to EmptyStringCmpop"), - } - } -} - -impl EmptyStringCmpop { - pub(crate) fn into_unary(self) -> &'static str { - match self { - Self::Is | Self::Eq => "not ", - Self::IsNot | Self::NotEq => "", - } - } -} - -impl std::fmt::Display for EmptyStringCmpop { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let repr = match self { - Self::Is => "is", - Self::IsNot => "is not", - Self::Eq => "==", - Self::NotEq => "!=", - }; - write!(f, "{repr}") - } -} - /// ## What it does /// Checks for comparisons to empty strings. /// @@ -73,7 +30,7 @@ impl std::fmt::Display for EmptyStringCmpop { /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/stdtypes.html#truth-value-testing) +/// - [Python documentation: Truth Value Testing](https://docs.python.org/3/library/stdtypes.html#truth-value-testing) #[violation] pub struct CompareToEmptyString { existing: String, @@ -83,34 +40,38 @@ pub struct CompareToEmptyString { impl Violation for CompareToEmptyString { #[derive_message_formats] fn message(&self) -> String { - format!( - "`{}` can be simplified to `{}` as an empty string is falsey", - self.existing, self.replacement, - ) + let CompareToEmptyString { + existing, + replacement, + } = self; + format!("`{existing}` can be simplified to `{replacement}` as an empty string is falsey",) } } +/// PLC1901 pub(crate) fn compare_to_empty_string( checker: &mut Checker, left: &Expr, - ops: &[Cmpop], + ops: &[CmpOp], comparators: &[Expr], ) { // Omit string comparison rules within subscripts. This is mostly commonly used within // DataFrame and np.ndarray indexing. - for parent in checker.semantic_model().expr_ancestors() { - if matches!(parent, Expr::Subscript(_)) { - return; - } + if checker + .semantic() + .expr_ancestors() + .any(|parent| parent.is_subscript_expr()) + { + return; } let mut first = true; for ((lhs, rhs), op) in std::iter::once(left) .chain(comparators.iter()) - .tuple_windows::<(&Expr<_>, &Expr<_>)>() + .tuple_windows::<(&Expr, &Expr)>() .zip(ops) { - if let Ok(op) = EmptyStringCmpop::try_from(op) { + if let Ok(op) = EmptyStringCmpOp::try_from(op) { if std::mem::take(&mut first) { // Check the left-most expression. if let Expr::Constant(ast::ExprConstant { value, .. }) = &lhs { @@ -153,3 +114,46 @@ pub(crate) fn compare_to_empty_string( } } } + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +enum EmptyStringCmpOp { + Is, + IsNot, + Eq, + NotEq, +} + +impl TryFrom<&CmpOp> for EmptyStringCmpOp { + type Error = anyhow::Error; + + fn try_from(value: &CmpOp) -> Result { + match value { + CmpOp::Is => Ok(Self::Is), + CmpOp::IsNot => Ok(Self::IsNot), + CmpOp::Eq => Ok(Self::Eq), + CmpOp::NotEq => Ok(Self::NotEq), + _ => bail!("{value:?} cannot be converted to EmptyStringCmpOp"), + } + } +} + +impl EmptyStringCmpOp { + fn into_unary(self) -> &'static str { + match self { + Self::Is | Self::Eq => "not ", + Self::IsNot | Self::NotEq => "", + } + } +} + +impl std::fmt::Display for EmptyStringCmpOp { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let repr = match self { + Self::Is => "is", + Self::IsNot => "is not", + Self::Eq => "==", + Self::NotEq => "!=", + }; + write!(f, "{repr}") + } +} diff --git a/crates/ruff/src/rules/pylint/rules/comparison_of_constant.rs b/crates/ruff/src/rules/pylint/rules/comparison_of_constant.rs index b38f72ad2d..0caac387c4 100644 --- a/crates/ruff/src/rules/pylint/rules/comparison_of_constant.rs +++ b/crates/ruff/src/rules/pylint/rules/comparison_of_constant.rs @@ -1,11 +1,11 @@ use itertools::Itertools; -use rustpython_parser::ast::{self, Cmpop, Expr, Ranged}; +use rustpython_parser::ast::{self, CmpOp, Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; -use crate::rules::pylint::helpers::CmpopExt; +use crate::rules::pylint::helpers::CmpOpExt; /// ## What it does /// Checks for comparisons between constants. @@ -26,11 +26,11 @@ use crate::rules::pylint::helpers::CmpopExt; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/reference/expressions.html#comparisons) +/// - [Python documentation: Comparisons](https://docs.python.org/3/reference/expressions.html#comparisons) #[violation] pub struct ComparisonOfConstant { left_constant: String, - op: Cmpop, + op: CmpOp, right_constant: String, } @@ -45,7 +45,7 @@ impl Violation for ComparisonOfConstant { format!( "Two constants compared in a comparison, consider replacing `{left_constant} {} {right_constant}`", - CmpopExt::from(op) + CmpOpExt::from(op) ) } } @@ -54,7 +54,7 @@ impl Violation for ComparisonOfConstant { pub(crate) fn comparison_of_constant( checker: &mut Checker, left: &Expr, - ops: &[Cmpop], + ops: &[CmpOp], comparators: &[Expr], ) { for ((left, right), op) in std::iter::once(left) diff --git a/crates/ruff/src/rules/pylint/rules/comparison_with_itself.rs b/crates/ruff/src/rules/pylint/rules/comparison_with_itself.rs index 12824054cd..7758eb2989 100644 --- a/crates/ruff/src/rules/pylint/rules/comparison_with_itself.rs +++ b/crates/ruff/src/rules/pylint/rules/comparison_with_itself.rs @@ -1,11 +1,11 @@ use itertools::Itertools; -use rustpython_parser::ast::{Cmpop, Expr, Ranged}; +use rustpython_parser::ast::{CmpOp, Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; -use crate::rules::pylint::helpers::CmpopExt; +use crate::rules::pylint::helpers::CmpOpExt; /// ## What it does /// Checks for operations that compare a name to itself. @@ -24,7 +24,7 @@ use crate::rules::pylint::helpers::CmpopExt; #[violation] pub struct ComparisonWithItself { left: String, - op: Cmpop, + op: CmpOp, right: String, } @@ -34,7 +34,7 @@ impl Violation for ComparisonWithItself { let ComparisonWithItself { left, op, right } = self; format!( "Name compared with itself, consider replacing `{left} {} {right}`", - CmpopExt::from(op) + CmpOpExt::from(op) ) } } @@ -43,7 +43,7 @@ impl Violation for ComparisonWithItself { pub(crate) fn comparison_with_itself( checker: &mut Checker, left: &Expr, - ops: &[Cmpop], + ops: &[CmpOp], comparators: &[Expr], ) { for ((left, right), op) in std::iter::once(left) diff --git a/crates/ruff/src/rules/pylint/rules/continue_in_finally.rs b/crates/ruff/src/rules/pylint/rules/continue_in_finally.rs index 09e6c21793..600ee4306e 100644 --- a/crates/ruff/src/rules/pylint/rules/continue_in_finally.rs +++ b/crates/ruff/src/rules/pylint/rules/continue_in_finally.rs @@ -32,6 +32,9 @@ use crate::checkers::ast::Checker; /// else: /// continue /// ``` +/// +/// ## Options +/// - `target-version` #[violation] pub struct ContinueInFinally; diff --git a/crates/ruff/src/rules/pylint/rules/duplicate_bases.rs b/crates/ruff/src/rules/pylint/rules/duplicate_bases.rs index 6d6256f472..860e393565 100644 --- a/crates/ruff/src/rules/pylint/rules/duplicate_bases.rs +++ b/crates/ruff/src/rules/pylint/rules/duplicate_bases.rs @@ -1,7 +1,7 @@ use std::hash::BuildHasherDefault; use rustc_hash::FxHashSet; -use rustpython_parser::ast::{self, Expr, Identifier, Ranged}; +use rustpython_parser::ast::{self, Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -35,7 +35,7 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/reference/compound_stmts.html#class-definitions) +/// - [Python documentation: Class definitions](https://docs.python.org/3/reference/compound_stmts.html#class-definitions) #[violation] pub struct DuplicateBases { base: String, @@ -52,7 +52,7 @@ impl Violation for DuplicateBases { /// PLE0241 pub(crate) fn duplicate_bases(checker: &mut Checker, name: &str, bases: &[Expr]) { - let mut seen: FxHashSet<&Identifier> = + let mut seen: FxHashSet<&str> = FxHashSet::with_capacity_and_hasher(bases.len(), BuildHasherDefault::default()); for base in bases { if let Expr::Name(ast::ExprName { id, .. }) = base { diff --git a/crates/ruff/src/rules/pylint/rules/global_statement.rs b/crates/ruff/src/rules/pylint/rules/global_statement.rs index c65bf96d57..3eef3e4123 100644 --- a/crates/ruff/src/rules/pylint/rules/global_statement.rs +++ b/crates/ruff/src/rules/pylint/rules/global_statement.rs @@ -55,22 +55,21 @@ impl Violation for GlobalStatement { /// PLW0603 pub(crate) fn global_statement(checker: &mut Checker, name: &str) { - let scope = checker.semantic_model().scope(); + let scope = checker.semantic().scope(); if let Some(binding_id) = scope.get(name) { - let binding = &checker.semantic_model().bindings[binding_id]; - if binding.kind.is_global() { - let source = checker.semantic_model().stmts[binding - .source - .expect("`global` bindings should always have a `source`")]; - let diagnostic = Diagnostic::new( - GlobalStatement { - name: name.to_string(), - }, - // Match Pylint's behavior by reporting on the `global` statement`, rather - // than the variable usage. - source.range(), - ); - checker.diagnostics.push(diagnostic); + let binding = checker.semantic().binding(binding_id); + if binding.is_global() { + if let Some(source) = binding.source { + let source = checker.semantic().stmts[source]; + checker.diagnostics.push(Diagnostic::new( + GlobalStatement { + name: name.to_string(), + }, + // Match Pylint's behavior by reporting on the `global` statement`, rather + // than the variable usage. + source.range(), + )); + } } } } diff --git a/crates/ruff/src/rules/pylint/rules/global_variable_not_assigned.rs b/crates/ruff/src/rules/pylint/rules/global_variable_not_assigned.rs index 9ac5cbe295..34857086de 100644 --- a/crates/ruff/src/rules/pylint/rules/global_variable_not_assigned.rs +++ b/crates/ruff/src/rules/pylint/rules/global_variable_not_assigned.rs @@ -34,7 +34,7 @@ use ruff_macros::{derive_message_formats, violation}; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/reference/simple_stmts.html#the-global-statement) +/// - [Python documentation: The `global` statement](https://docs.python.org/3/reference/simple_stmts.html#the-global-statement) #[violation] pub struct GlobalVariableNotAssigned { pub name: String, diff --git a/crates/ruff/src/rules/pylint/rules/import_self.rs b/crates/ruff/src/rules/pylint/rules/import_self.rs index a06f84bfe4..63ba4cfb99 100644 --- a/crates/ruff/src/rules/pylint/rules/import_self.rs +++ b/crates/ruff/src/rules/pylint/rules/import_self.rs @@ -60,7 +60,8 @@ pub(crate) fn import_from_self( let Some(module_path) = module_path else { return None; }; - let Some(imported_module_path) = resolve_imported_module_path(level, module, Some(module_path)) else { + let Some(imported_module_path) = resolve_imported_module_path(level, module, Some(module_path)) + else { return None; }; diff --git a/crates/ruff/src/rules/pylint/rules/invalid_all_format.rs b/crates/ruff/src/rules/pylint/rules/invalid_all_format.rs index 0a818814ee..5b275a48a1 100644 --- a/crates/ruff/src/rules/pylint/rules/invalid_all_format.rs +++ b/crates/ruff/src/rules/pylint/rules/invalid_all_format.rs @@ -24,7 +24,7 @@ use ruff_macros::{derive_message_formats, violation}; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement) +/// - [Python documentation: The `import` statement](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement) #[violation] pub struct InvalidAllFormat; diff --git a/crates/ruff/src/rules/pylint/rules/invalid_all_object.rs b/crates/ruff/src/rules/pylint/rules/invalid_all_object.rs index 46e05c80e7..c555e64aa1 100644 --- a/crates/ruff/src/rules/pylint/rules/invalid_all_object.rs +++ b/crates/ruff/src/rules/pylint/rules/invalid_all_object.rs @@ -24,7 +24,7 @@ use ruff_macros::{derive_message_formats, violation}; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement) +/// - [Python documentation: The `import` statement](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement) #[violation] pub struct InvalidAllObject; diff --git a/crates/ruff/src/rules/pylint/rules/invalid_envvar_default.rs b/crates/ruff/src/rules/pylint/rules/invalid_envvar_default.rs index a70ae406d6..b8c78baab8 100644 --- a/crates/ruff/src/rules/pylint/rules/invalid_envvar_default.rs +++ b/crates/ruff/src/rules/pylint/rules/invalid_envvar_default.rs @@ -84,15 +84,22 @@ pub(crate) fn invalid_envvar_default( keywords: &[Keyword], ) { if checker - .semantic_model() + .semantic() .resolve_call_path(func) - .map_or(false, |call_path| call_path.as_slice() == ["os", "getenv"]) + .map_or(false, |call_path| { + matches!(call_path.as_slice(), ["os", "getenv"]) + }) { // Find the `default` argument, if it exists. let Some(expr) = args.get(1).or_else(|| { keywords .iter() - .find(|keyword| keyword.arg.as_ref().map_or(false, |arg| arg .as_str()== "default")) + .find(|keyword| { + keyword + .arg + .as_ref() + .map_or(false, |arg| arg.as_str() == "default") + }) .map(|keyword| &keyword.value) }) else { return; diff --git a/crates/ruff/src/rules/pylint/rules/invalid_envvar_value.rs b/crates/ruff/src/rules/pylint/rules/invalid_envvar_value.rs index 2377de8466..f7ebdcb6ee 100644 --- a/crates/ruff/src/rules/pylint/rules/invalid_envvar_value.rs +++ b/crates/ruff/src/rules/pylint/rules/invalid_envvar_value.rs @@ -81,9 +81,11 @@ pub(crate) fn invalid_envvar_value( keywords: &[Keyword], ) { if checker - .semantic_model() + .semantic() .resolve_call_path(func) - .map_or(false, |call_path| call_path.as_slice() == ["os", "getenv"]) + .map_or(false, |call_path| { + matches!(call_path.as_slice(), ["os", "getenv"]) + }) { // Find the `key` argument, if it exists. let Some(expr) = args.get(0).or_else(|| { diff --git a/crates/ruff/src/rules/pylint/rules/invalid_str_return.rs b/crates/ruff/src/rules/pylint/rules/invalid_str_return.rs index 486c8a474e..9f43d0d96c 100644 --- a/crates/ruff/src/rules/pylint/rules/invalid_str_return.rs +++ b/crates/ruff/src/rules/pylint/rules/invalid_str_return.rs @@ -29,7 +29,7 @@ pub(crate) fn invalid_str_return(checker: &mut Checker, name: &str, body: &[Stmt return; } - if !checker.semantic_model().scope().kind.is_class() { + if !checker.semantic().scope().kind.is_class() { return; } diff --git a/crates/ruff/src/rules/pylint/rules/invalid_string_characters.rs b/crates/ruff/src/rules/pylint/rules/invalid_string_characters.rs index 7a792d3bcb..5bccae0694 100644 --- a/crates/ruff/src/rules/pylint/rules/invalid_string_characters.rs +++ b/crates/ruff/src/rules/pylint/rules/invalid_string_characters.rs @@ -171,8 +171,11 @@ impl AlwaysAutofixableViolation for InvalidCharacterZeroWidthSpace { } /// PLE2510, PLE2512, PLE2513, PLE2514, PLE2515 -pub(crate) fn invalid_string_characters(locator: &Locator, range: TextRange) -> Vec { - let mut diagnostics = Vec::new(); +pub(crate) fn invalid_string_characters( + diagnostics: &mut Vec, + range: TextRange, + locator: &Locator, +) { let text = locator.slice(range); for (column, match_) in text.match_indices(&['\x08', '\x1A', '\x1B', '\0', '\u{200b}']) { @@ -191,11 +194,8 @@ pub(crate) fn invalid_string_characters(locator: &Locator, range: TextRange) -> let location = range.start() + TextSize::try_from(column).unwrap(); let range = TextRange::at(location, c.text_len()); - #[allow(deprecated)] - diagnostics.push(Diagnostic::new(rule, range).with_fix(Fix::unspecified( + diagnostics.push(Diagnostic::new(rule, range).with_fix(Fix::automatic( Edit::range_replacement(replacement.to_string(), range), ))); } - - diagnostics } diff --git a/crates/ruff/src/rules/pylint/rules/iteration_over_set.rs b/crates/ruff/src/rules/pylint/rules/iteration_over_set.rs index b98806c7c8..38725f7e4f 100644 --- a/crates/ruff/src/rules/pylint/rules/iteration_over_set.rs +++ b/crates/ruff/src/rules/pylint/rules/iteration_over_set.rs @@ -1,4 +1,4 @@ -use rustpython_parser::ast::{Expr, Ranged}; +use rustpython_parser::ast::{self, Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -38,9 +38,15 @@ impl Violation for IterationOverSet { /// PLC0208 pub(crate) fn iteration_over_set(checker: &mut Checker, expr: &Expr) { - if expr.is_set_expr() { - checker - .diagnostics - .push(Diagnostic::new(IterationOverSet, expr.range())); + let Expr::Set(ast::ExprSet { elts, .. }) = expr else { + return; + }; + + if elts.iter().any(Expr::is_starred_expr) { + return; } + + checker + .diagnostics + .push(Diagnostic::new(IterationOverSet, expr.range())); } diff --git a/crates/ruff/src/rules/pylint/rules/load_before_global_declaration.rs b/crates/ruff/src/rules/pylint/rules/load_before_global_declaration.rs index 5e936bf334..e97da007cb 100644 --- a/crates/ruff/src/rules/pylint/rules/load_before_global_declaration.rs +++ b/crates/ruff/src/rules/pylint/rules/load_before_global_declaration.rs @@ -7,7 +7,7 @@ use ruff_python_ast::source_code::OneIndexed; use crate::checkers::ast::Checker; /// ## What it does -/// Checks for usages of names that are declared as `global` prior to the +/// Checks for uses of names that are declared as `global` prior to the /// relevant `global` declaration. /// /// ## Why is this bad? @@ -38,7 +38,7 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/reference/simple_stmts.html#the-global-statement) +/// - [Python documentation: The `global` statement](https://docs.python.org/3/reference/simple_stmts.html#the-global-statement) #[violation] pub struct LoadBeforeGlobalDeclaration { name: String, @@ -52,9 +52,10 @@ impl Violation for LoadBeforeGlobalDeclaration { format!("Name `{name}` is used prior to global declaration on line {line}") } } + /// PLE0118 pub(crate) fn load_before_global_declaration(checker: &mut Checker, name: &str, expr: &Expr) { - if let Some(stmt) = checker.semantic_model().global(name) { + if let Some(stmt) = checker.semantic().global(name) { if expr.start() < stmt.start() { #[allow(deprecated)] let location = checker.locator.compute_source_location(stmt.start()); diff --git a/crates/ruff/src/rules/pylint/rules/logging.rs b/crates/ruff/src/rules/pylint/rules/logging.rs index 2cf9930caa..8391113612 100644 --- a/crates/ruff/src/rules/pylint/rules/logging.rs +++ b/crates/ruff/src/rules/pylint/rules/logging.rs @@ -93,7 +93,7 @@ pub(crate) fn logging_call( keywords: &[Keyword], ) { // If there are any starred arguments, abort. - if args.iter().any(|arg| matches!(arg, Expr::Starred(_))) { + if args.iter().any(Expr::is_starred_expr) { return; } @@ -102,7 +102,7 @@ pub(crate) fn logging_call( return; } - if !logging::is_logger_candidate(func, checker.semantic_model()) { + if !logging::is_logger_candidate(func, checker.semantic()) { return; } @@ -122,7 +122,7 @@ pub(crate) fn logging_call( return; } - let message_args = call_args.args.len() - 1; + let message_args = call_args.num_args() - 1; if checker.enabled(Rule::LoggingTooManyArgs) { if summary.num_positional < message_args { @@ -134,7 +134,7 @@ pub(crate) fn logging_call( if checker.enabled(Rule::LoggingTooFewArgs) { if message_args > 0 - && call_args.kwargs.is_empty() + && call_args.num_kwargs() == 0 && summary.num_positional > message_args { checker diff --git a/crates/ruff/src/rules/pylint/rules/magic_value_comparison.rs b/crates/ruff/src/rules/pylint/rules/magic_value_comparison.rs index 60a6a8e4c2..0051f8907e 100644 --- a/crates/ruff/src/rules/pylint/rules/magic_value_comparison.rs +++ b/crates/ruff/src/rules/pylint/rules/magic_value_comparison.rs @@ -1,5 +1,5 @@ use itertools::Itertools; -use rustpython_parser::ast::{self, Constant, Expr, Ranged, Unaryop}; +use rustpython_parser::ast::{self, Constant, Expr, Ranged, UnaryOp}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -12,8 +12,9 @@ use crate::rules::pylint::settings::ConstantType; /// comparisons. /// /// ## Why is this bad? -/// The use of "magic" can make code harder to read and maintain, as readers -/// will have to infer the meaning of the value from the context. +/// The use of "magic" values can make code harder to read and maintain, as +/// readers will have to infer the meaning of the value from the context. +/// Such values are discouraged by [PEP 8]. /// /// For convenience, this rule excludes a variety of common values from the /// "magic" value definition, such as `0`, `1`, `""`, and `"__main__"`. @@ -33,9 +34,7 @@ use crate::rules::pylint::settings::ConstantType; /// return price * (1 - DISCOUNT_RATE) /// ``` /// -/// ## References -/// - [Wikipedia](https://en.wikipedia.org/wiki/Magic_number_(programming)#Unnamed_numerical_constants) -/// - [PEP 8](https://peps.python.org/pep-0008/#constants) +/// [PEP 8]: https://peps.python.org/pep-0008/#constants #[violation] pub struct MagicValueComparison { value: String, @@ -56,7 +55,7 @@ fn as_constant(expr: &Expr) -> Option<&Constant> { match expr { Expr::Constant(ast::ExprConstant { value, .. }) => Some(value), Expr::UnaryOp(ast::ExprUnaryOp { - op: Unaryop::UAdd | Unaryop::USub | Unaryop::Invert, + op: UnaryOp::UAdd | UnaryOp::USub | UnaryOp::Invert, operand, range: _, }) => match operand.as_ref() { diff --git a/crates/ruff/src/rules/pylint/rules/manual_import_from.rs b/crates/ruff/src/rules/pylint/rules/manual_import_from.rs index 5201bc906d..0343cd1eb7 100644 --- a/crates/ruff/src/rules/pylint/rules/manual_import_from.rs +++ b/crates/ruff/src/rules/pylint/rules/manual_import_from.rs @@ -1,5 +1,5 @@ use ruff_text_size::TextRange; -use rustpython_parser::ast::{self, Alias, Int, Ranged, Stmt}; +use rustpython_parser::ast::{self, Alias, Identifier, Int, Ranged, Stmt}; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -25,7 +25,7 @@ use crate::registry::AsRule; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/reference/import.html#submodules) +/// - [Python documentation: Submodules](https://docs.python.org/3/reference/import.html#submodules) #[violation] pub struct ManualFromImport { module: String, @@ -74,7 +74,7 @@ pub(crate) fn manual_from_import( if checker.patch(diagnostic.kind.rule()) { if names.len() == 1 { let node = ast::StmtImportFrom { - module: Some(module.into()), + module: Some(Identifier::new(module.to_string(), TextRange::default())), names: vec![Alias { name: asname.clone(), asname: None, @@ -83,8 +83,7 @@ pub(crate) fn manual_from_import( level: Some(Int::new(0)), range: TextRange::default(), }; - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( checker.generator().stmt(&node.into()), stmt.range(), ))); diff --git a/crates/ruff/src/rules/pylint/rules/mod.rs b/crates/ruff/src/rules/pylint/rules/mod.rs index b254b79541..829c9e6916 100644 --- a/crates/ruff/src/rules/pylint/rules/mod.rs +++ b/crates/ruff/src/rules/pylint/rules/mod.rs @@ -1,59 +1,53 @@ -pub(crate) use assert_on_string_literal::{assert_on_string_literal, AssertOnStringLiteral}; -pub(crate) use await_outside_async::{await_outside_async, AwaitOutsideAsync}; -pub(crate) use bad_str_strip_call::{bad_str_strip_call, BadStrStripCall}; -pub(crate) use bad_string_format_type::{bad_string_format_type, BadStringFormatType}; -pub(crate) use bidirectional_unicode::{bidirectional_unicode, BidirectionalUnicode}; -pub(crate) use binary_op_exception::{binary_op_exception, BinaryOpException}; -pub(crate) use collapsible_else_if::{collapsible_else_if, CollapsibleElseIf}; -pub(crate) use compare_to_empty_string::{compare_to_empty_string, CompareToEmptyString}; -pub(crate) use comparison_of_constant::{comparison_of_constant, ComparisonOfConstant}; -pub(crate) use comparison_with_itself::{comparison_with_itself, ComparisonWithItself}; -pub(crate) use continue_in_finally::{continue_in_finally, ContinueInFinally}; -pub(crate) use duplicate_bases::{duplicate_bases, DuplicateBases}; -pub(crate) use global_statement::{global_statement, GlobalStatement}; -pub(crate) use global_variable_not_assigned::GlobalVariableNotAssigned; -pub(crate) use import_self::{import_from_self, import_self, ImportSelf}; -pub(crate) use invalid_all_format::{invalid_all_format, InvalidAllFormat}; -pub(crate) use invalid_all_object::{invalid_all_object, InvalidAllObject}; -pub(crate) use invalid_envvar_default::{invalid_envvar_default, InvalidEnvvarDefault}; -pub(crate) use invalid_envvar_value::{invalid_envvar_value, InvalidEnvvarValue}; -pub(crate) use invalid_str_return::{invalid_str_return, InvalidStrReturnType}; -pub(crate) use invalid_string_characters::{ - invalid_string_characters, InvalidCharacterBackspace, InvalidCharacterEsc, InvalidCharacterNul, - InvalidCharacterSub, InvalidCharacterZeroWidthSpace, -}; -pub(crate) use iteration_over_set::{iteration_over_set, IterationOverSet}; -pub(crate) use load_before_global_declaration::{ - load_before_global_declaration, LoadBeforeGlobalDeclaration, -}; -pub(crate) use logging::{logging_call, LoggingTooFewArgs, LoggingTooManyArgs}; -pub(crate) use magic_value_comparison::{magic_value_comparison, MagicValueComparison}; -pub(crate) use manual_import_from::{manual_from_import, ManualFromImport}; -pub(crate) use named_expr_without_context::{named_expr_without_context, NamedExprWithoutContext}; -pub(crate) use nested_min_max::{nested_min_max, NestedMinMax}; -pub(crate) use nonlocal_without_binding::NonlocalWithoutBinding; -pub(crate) use property_with_parameters::{property_with_parameters, PropertyWithParameters}; -pub(crate) use redefined_loop_name::{redefined_loop_name, RedefinedLoopName}; -pub(crate) use repeated_isinstance_calls::{repeated_isinstance_calls, RepeatedIsinstanceCalls}; -pub(crate) use return_in_init::{return_in_init, ReturnInInit}; -pub(crate) use sys_exit_alias::{sys_exit_alias, SysExitAlias}; -pub(crate) use too_many_arguments::{too_many_arguments, TooManyArguments}; -pub(crate) use too_many_branches::{too_many_branches, TooManyBranches}; -pub(crate) use too_many_return_statements::{too_many_return_statements, TooManyReturnStatements}; -pub(crate) use too_many_statements::{too_many_statements, TooManyStatements}; -pub(crate) use unexpected_special_method_signature::{ - unexpected_special_method_signature, UnexpectedSpecialMethodSignature, -}; -pub(crate) use unnecessary_direct_lambda_call::{ - unnecessary_direct_lambda_call, UnnecessaryDirectLambdaCall, -}; -pub(crate) use useless_else_on_loop::{useless_else_on_loop, UselessElseOnLoop}; -pub(crate) use useless_import_alias::{useless_import_alias, UselessImportAlias}; -pub(crate) use useless_return::{useless_return, UselessReturn}; -pub(crate) use yield_from_in_async_function::{ - yield_from_in_async_function, YieldFromInAsyncFunction, -}; -pub(crate) use yield_in_init::{yield_in_init, YieldInInit}; +pub(crate) use assert_on_string_literal::*; +pub(crate) use await_outside_async::*; +pub(crate) use bad_str_strip_call::*; +pub(crate) use bad_string_format_type::*; +pub(crate) use bidirectional_unicode::*; +pub(crate) use binary_op_exception::*; +pub(crate) use collapsible_else_if::*; +pub(crate) use compare_to_empty_string::*; +pub(crate) use comparison_of_constant::*; +pub(crate) use comparison_with_itself::*; +pub(crate) use continue_in_finally::*; +pub(crate) use duplicate_bases::*; +pub(crate) use global_statement::*; +pub(crate) use global_variable_not_assigned::*; +pub(crate) use import_self::*; +pub(crate) use invalid_all_format::*; +pub(crate) use invalid_all_object::*; +pub(crate) use invalid_envvar_default::*; +pub(crate) use invalid_envvar_value::*; +pub(crate) use invalid_str_return::*; +pub(crate) use invalid_string_characters::*; +pub(crate) use iteration_over_set::*; +pub(crate) use load_before_global_declaration::*; +pub(crate) use logging::*; +pub(crate) use magic_value_comparison::*; +pub(crate) use manual_import_from::*; +pub(crate) use named_expr_without_context::*; +pub(crate) use nested_min_max::*; +pub(crate) use nonlocal_without_binding::*; +pub(crate) use property_with_parameters::*; +pub(crate) use redefined_loop_name::*; +pub(crate) use repeated_equality_comparison_target::*; +pub(crate) use repeated_isinstance_calls::*; +pub(crate) use return_in_init::*; +pub(crate) use single_string_slots::*; +pub(crate) use sys_exit_alias::*; +pub(crate) use too_many_arguments::*; +pub(crate) use too_many_branches::*; +pub(crate) use too_many_return_statements::*; +pub(crate) use too_many_statements::*; +pub(crate) use type_bivariance::*; +pub(crate) use type_name_incorrect_variance::*; +pub(crate) use type_param_name_mismatch::*; +pub(crate) use unexpected_special_method_signature::*; +pub(crate) use unnecessary_direct_lambda_call::*; +pub(crate) use useless_else_on_loop::*; +pub(crate) use useless_import_alias::*; +pub(crate) use useless_return::*; +pub(crate) use yield_from_in_async_function::*; +pub(crate) use yield_in_init::*; mod assert_on_string_literal; mod await_outside_async; @@ -86,13 +80,18 @@ mod nested_min_max; mod nonlocal_without_binding; mod property_with_parameters; mod redefined_loop_name; +mod repeated_equality_comparison_target; mod repeated_isinstance_calls; mod return_in_init; +mod single_string_slots; mod sys_exit_alias; mod too_many_arguments; mod too_many_branches; mod too_many_return_statements; mod too_many_statements; +mod type_bivariance; +mod type_name_incorrect_variance; +mod type_param_name_mismatch; mod unexpected_special_method_signature; mod unnecessary_direct_lambda_call; mod useless_else_on_loop; diff --git a/crates/ruff/src/rules/pylint/rules/named_expr_without_context.rs b/crates/ruff/src/rules/pylint/rules/named_expr_without_context.rs index 3576d36ea3..27627585c8 100644 --- a/crates/ruff/src/rules/pylint/rules/named_expr_without_context.rs +++ b/crates/ruff/src/rules/pylint/rules/named_expr_without_context.rs @@ -6,7 +6,7 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; /// ## What it does -/// Checks for usages of named expressions (e.g., `a := 42`) that can be +/// Checks for uses of named expressions (e.g., `a := 42`) that can be /// replaced by regular assignment statements (e.g., `a = 42`). /// /// ## Why is this bad? diff --git a/crates/ruff/src/rules/pylint/rules/nested_min_max.rs b/crates/ruff/src/rules/pylint/rules/nested_min_max.rs index 3c5aee7852..05cc249fc3 100644 --- a/crates/ruff/src/rules/pylint/rules/nested_min_max.rs +++ b/crates/ruff/src/rules/pylint/rules/nested_min_max.rs @@ -4,7 +4,7 @@ use rustpython_parser::ast::{self, Expr, Keyword, Ranged}; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::has_comments; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; use crate::{checkers::ast::Checker, registry::AsRule}; @@ -59,16 +59,20 @@ impl Violation for NestedMinMax { impl MinMax { /// Converts a function call [`Expr`] into a [`MinMax`] if it is a call to `min` or `max`. - fn try_from_call(func: &Expr, keywords: &[Keyword], model: &SemanticModel) -> Option { + fn try_from_call( + func: &Expr, + keywords: &[Keyword], + semantic: &SemanticModel, + ) -> Option { if !keywords.is_empty() { return None; } let Expr::Name(ast::ExprName { id, .. }) = func else { return None; }; - if id.as_str() == "min" && model.is_builtin("min") { + if id.as_str() == "min" && semantic.is_builtin("min") { Some(MinMax::Min) - } else if id.as_str() == "max" && model.is_builtin("max") { + } else if id.as_str() == "max" && semantic.is_builtin("max") { Some(MinMax::Max) } else { None @@ -87,8 +91,8 @@ impl std::fmt::Display for MinMax { /// Collect a new set of arguments to by either accepting existing args as-is or /// collecting child arguments, if it's a call to the same function. -fn collect_nested_args(model: &SemanticModel, min_max: MinMax, args: &[Expr]) -> Vec { - fn inner(model: &SemanticModel, min_max: MinMax, args: &[Expr], new_args: &mut Vec) { +fn collect_nested_args(min_max: MinMax, args: &[Expr], semantic: &SemanticModel) -> Vec { + fn inner(min_max: MinMax, args: &[Expr], semantic: &SemanticModel, new_args: &mut Vec) { for arg in args { if let Expr::Call(ast::ExprCall { func, @@ -106,8 +110,8 @@ fn collect_nested_args(model: &SemanticModel, min_max: MinMax, args: &[Expr]) -> new_args.push(new_arg); continue; } - if MinMax::try_from_call(func, keywords, model) == Some(min_max) { - inner(model, min_max, args, new_args); + if MinMax::try_from_call(func, keywords, semantic) == Some(min_max) { + inner(min_max, args, semantic, new_args); continue; } } @@ -116,7 +120,7 @@ fn collect_nested_args(model: &SemanticModel, min_max: MinMax, args: &[Expr]) -> } let mut new_args = Vec::with_capacity(args.len()); - inner(model, min_max, args, &mut new_args); + inner(min_max, args, semantic, &mut new_args); new_args } @@ -128,7 +132,7 @@ pub(crate) fn nested_min_max( args: &[Expr], keywords: &[Keyword], ) { - let Some(min_max) = MinMax::try_from_call(func, keywords, checker.semantic_model()) else { + let Some(min_max) = MinMax::try_from_call(func, keywords, checker.semantic()) else { return; }; @@ -139,23 +143,21 @@ pub(crate) fn nested_min_max( } if args.iter().any(|arg| { - let Expr::Call(ast::ExprCall { func, keywords, ..} )= arg else { + let Expr::Call(ast::ExprCall { func, keywords, .. }) = arg else { return false; }; - MinMax::try_from_call(func.as_ref(), keywords.as_ref(), checker.semantic_model()) - == Some(min_max) + MinMax::try_from_call(func.as_ref(), keywords.as_ref(), checker.semantic()) == Some(min_max) }) { let mut diagnostic = Diagnostic::new(NestedMinMax { func: min_max }, expr.range()); if checker.patch(diagnostic.kind.rule()) { if !has_comments(expr, checker.locator) { let flattened_expr = Expr::Call(ast::ExprCall { func: Box::new(func.clone()), - args: collect_nested_args(checker.semantic_model(), min_max, args), + args: collect_nested_args(min_max, args, checker.semantic()), keywords: keywords.to_owned(), range: TextRange::default(), }); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( checker.generator().expr(&flattened_expr), expr.range(), ))); diff --git a/crates/ruff/src/rules/pylint/rules/nonlocal_without_binding.rs b/crates/ruff/src/rules/pylint/rules/nonlocal_without_binding.rs index 9f66ff859a..d29ef9bf44 100644 --- a/crates/ruff/src/rules/pylint/rules/nonlocal_without_binding.rs +++ b/crates/ruff/src/rules/pylint/rules/nonlocal_without_binding.rs @@ -26,7 +26,7 @@ use ruff_macros::{derive_message_formats, violation}; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/reference/simple_stmts.html#nonlocal) +/// - [Python documentation: The `nonlocal` statement](https://docs.python.org/3/reference/simple_stmts.html#nonlocal) /// - [PEP 3104](https://peps.python.org/pep-3104/) #[violation] pub struct NonlocalWithoutBinding { diff --git a/crates/ruff/src/rules/pylint/rules/property_with_parameters.rs b/crates/ruff/src/rules/pylint/rules/property_with_parameters.rs index d63dd5025b..08d229a25f 100644 --- a/crates/ruff/src/rules/pylint/rules/property_with_parameters.rs +++ b/crates/ruff/src/rules/pylint/rules/property_with_parameters.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::{self, Arguments, Decorator, Expr, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::identifier_range; +use ruff_python_ast::identifier::Identifier; use crate::checkers::ast::Checker; @@ -35,7 +35,7 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/functions.html#property) +/// - [Python documentation: `property`](https://docs.python.org/3/library/functions.html#property) #[violation] pub struct PropertyWithParameters; @@ -55,22 +55,21 @@ pub(crate) fn property_with_parameters( ) { if !decorator_list .iter() - .any(|d| matches!(&d.expression, Expr::Name(ast::ExprName { id, .. }) if id == "property")) + .any(|decorator| matches!(&decorator.expression, Expr::Name(ast::ExprName { id, .. }) if id == "property")) { return; } - if checker.semantic_model().is_builtin("property") - && args - .args - .iter() - .chain(args.posonlyargs.iter()) - .chain(args.kwonlyargs.iter()) - .count() - > 1 + if args + .posonlyargs + .iter() + .chain(&args.args) + .chain(&args.kwonlyargs) + .count() + > 1 + && checker.semantic().is_builtin("property") { - checker.diagnostics.push(Diagnostic::new( - PropertyWithParameters, - identifier_range(stmt, checker.locator), - )); + checker + .diagnostics + .push(Diagnostic::new(PropertyWithParameters, stmt.identifier())); } } diff --git a/crates/ruff/src/rules/pylint/rules/redefined_loop_name.rs b/crates/ruff/src/rules/pylint/rules/redefined_loop_name.rs index 8964206d1c..c0536daee1 100644 --- a/crates/ruff/src/rules/pylint/rules/redefined_loop_name.rs +++ b/crates/ruff/src/rules/pylint/rules/redefined_loop_name.rs @@ -1,59 +1,16 @@ use std::{fmt, iter}; use regex::Regex; -use rustpython_parser::ast::{self, Expr, ExprContext, Ranged, Stmt, Withitem}; +use rustpython_parser::ast::{self, Expr, ExprContext, Ranged, Stmt, WithItem}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::comparable::ComparableExpr; use ruff_python_ast::statement_visitor::{walk_stmt, StatementVisitor}; -use ruff_python_ast::types::Node; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; use crate::checkers::ast::Checker; -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub(crate) enum OuterBindingKind { - For, - With, -} - -impl fmt::Display for OuterBindingKind { - fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { - match self { - OuterBindingKind::For => fmt.write_str("`for` loop"), - OuterBindingKind::With => fmt.write_str("`with` statement"), - } - } -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub(crate) enum InnerBindingKind { - For, - With, - Assignment, -} - -impl fmt::Display for InnerBindingKind { - fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { - match self { - InnerBindingKind::For => fmt.write_str("`for` loop"), - InnerBindingKind::With => fmt.write_str("`with` statement"), - InnerBindingKind::Assignment => fmt.write_str("assignment"), - } - } -} - -impl PartialEq for OuterBindingKind { - fn eq(&self, other: &InnerBindingKind) -> bool { - matches!( - (self, other), - (OuterBindingKind::For, InnerBindingKind::For) - | (OuterBindingKind::With, InnerBindingKind::With) - ) - } -} - /// ## What it does /// Checks for variables defined in `for` loops and `with` statements that /// get overwritten within the body, for example by another `for` loop or @@ -128,6 +85,48 @@ impl Violation for RedefinedLoopName { } } +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +enum OuterBindingKind { + For, + With, +} + +impl fmt::Display for OuterBindingKind { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match self { + OuterBindingKind::For => fmt.write_str("`for` loop"), + OuterBindingKind::With => fmt.write_str("`with` statement"), + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +enum InnerBindingKind { + For, + With, + Assignment, +} + +impl fmt::Display for InnerBindingKind { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match self { + InnerBindingKind::For => fmt.write_str("`for` loop"), + InnerBindingKind::With => fmt.write_str("`with` statement"), + InnerBindingKind::Assignment => fmt.write_str("assignment"), + } + } +} + +impl PartialEq for OuterBindingKind { + fn eq(&self, other: &InnerBindingKind) -> bool { + matches!( + (self, other), + (OuterBindingKind::For, InnerBindingKind::For) + | (OuterBindingKind::With, InnerBindingKind::With) + ) + } +} + struct ExprWithOuterBindingKind<'a> { expr: &'a Expr, binding_kind: OuterBindingKind, @@ -176,7 +175,7 @@ impl<'a, 'b> StatementVisitor<'b> for InnerForWithAssignTargetsVisitor<'a, 'b> { // Check for single-target assignments which are of the // form `x = cast(..., x)`. if targets.first().map_or(false, |target| { - assignment_is_cast_expr(self.context, value, target) + assignment_is_cast_expr(value, target, self.context) }) { return; } @@ -236,7 +235,7 @@ impl<'a, 'b> StatementVisitor<'b> for InnerForWithAssignTargetsVisitor<'a, 'b> { /// /// x = cast(int, x) /// ``` -fn assignment_is_cast_expr(model: &SemanticModel, value: &Expr, target: &Expr) -> bool { +fn assignment_is_cast_expr(value: &Expr, target: &Expr, semantic: &SemanticModel) -> bool { let Expr::Call(ast::ExprCall { func, args, .. }) = value else { return false; }; @@ -252,13 +251,13 @@ fn assignment_is_cast_expr(model: &SemanticModel, value: &Expr, target: &Expr) - if arg_id != target_id { return false; } - model.match_typing_expr(func, "cast") + semantic.match_typing_expr(func, "cast") } -fn assignment_targets_from_expr<'a, U>( - expr: &'a Expr, +fn assignment_targets_from_expr<'a>( + expr: &'a Expr, dummy_variable_rgx: &'a Regex, -) -> Box> + 'a> { +) -> Box + 'a> { // The Box is necessary to ensure the match arms have the same return type - we can't use // a cast to "impl Iterator", since at the time of writing that is only allowed for // return types and argument types. @@ -308,77 +307,73 @@ fn assignment_targets_from_expr<'a, U>( } } -fn assignment_targets_from_with_items<'a, U>( - items: &'a [Withitem], +fn assignment_targets_from_with_items<'a>( + items: &'a [WithItem], dummy_variable_rgx: &'a Regex, -) -> impl Iterator> + 'a { +) -> impl Iterator + 'a { items .iter() .filter_map(|item| { item.optional_vars .as_ref() - .map(|expr| assignment_targets_from_expr(&**expr, dummy_variable_rgx)) + .map(|expr| assignment_targets_from_expr(expr, dummy_variable_rgx)) }) .flatten() } -fn assignment_targets_from_assign_targets<'a, U>( - targets: &'a [Expr], +fn assignment_targets_from_assign_targets<'a>( + targets: &'a [Expr], dummy_variable_rgx: &'a Regex, -) -> impl Iterator> + 'a { +) -> impl Iterator + 'a { targets .iter() .flat_map(|target| assignment_targets_from_expr(target, dummy_variable_rgx)) } /// PLW2901 -pub(crate) fn redefined_loop_name<'a, 'b>(checker: &'a mut Checker<'b>, node: &Node<'b>) { - let (outer_assignment_targets, inner_assignment_targets) = match node { - Node::Stmt(stmt) => match stmt { - // With. - Stmt::With(ast::StmtWith { items, body, .. }) => { - let outer_assignment_targets: Vec> = - assignment_targets_from_with_items(items, &checker.settings.dummy_variable_rgx) - .map(|expr| ExprWithOuterBindingKind { - expr, - binding_kind: OuterBindingKind::With, - }) - .collect(); - let mut visitor = InnerForWithAssignTargetsVisitor { - context: checker.semantic_model(), - dummy_variable_rgx: &checker.settings.dummy_variable_rgx, - assignment_targets: vec![], - }; - for stmt in body { - visitor.visit_stmt(stmt); - } - (outer_assignment_targets, visitor.assignment_targets) +pub(crate) fn redefined_loop_name(checker: &mut Checker, stmt: &Stmt) { + let (outer_assignment_targets, inner_assignment_targets) = match stmt { + Stmt::With(ast::StmtWith { items, body, .. }) + | Stmt::AsyncWith(ast::StmtAsyncWith { items, body, .. }) => { + let outer_assignment_targets: Vec = + assignment_targets_from_with_items(items, &checker.settings.dummy_variable_rgx) + .map(|expr| ExprWithOuterBindingKind { + expr, + binding_kind: OuterBindingKind::With, + }) + .collect(); + let mut visitor = InnerForWithAssignTargetsVisitor { + context: checker.semantic(), + dummy_variable_rgx: &checker.settings.dummy_variable_rgx, + assignment_targets: vec![], + }; + for stmt in body { + visitor.visit_stmt(stmt); } - // For and async for. - Stmt::For(ast::StmtFor { target, body, .. }) - | Stmt::AsyncFor(ast::StmtAsyncFor { target, body, .. }) => { - let outer_assignment_targets: Vec> = - assignment_targets_from_expr(target, &checker.settings.dummy_variable_rgx) - .map(|expr| ExprWithOuterBindingKind { - expr, - binding_kind: OuterBindingKind::For, - }) - .collect(); - let mut visitor = InnerForWithAssignTargetsVisitor { - context: checker.semantic_model(), - dummy_variable_rgx: &checker.settings.dummy_variable_rgx, - assignment_targets: vec![], - }; - for stmt in body { - visitor.visit_stmt(stmt); - } - (outer_assignment_targets, visitor.assignment_targets) + (outer_assignment_targets, visitor.assignment_targets) + } + Stmt::For(ast::StmtFor { target, body, .. }) + | Stmt::AsyncFor(ast::StmtAsyncFor { target, body, .. }) => { + let outer_assignment_targets: Vec = + assignment_targets_from_expr(target, &checker.settings.dummy_variable_rgx) + .map(|expr| ExprWithOuterBindingKind { + expr, + binding_kind: OuterBindingKind::For, + }) + .collect(); + let mut visitor = InnerForWithAssignTargetsVisitor { + context: checker.semantic(), + dummy_variable_rgx: &checker.settings.dummy_variable_rgx, + assignment_targets: vec![], + }; + for stmt in body { + visitor.visit_stmt(stmt); } - _ => panic!( - "redefined_loop_name called on Statement that is not a With, For, or AsyncFor" - ), - }, - Node::Expr(_) => panic!("redefined_loop_name called on Node that is not a Statement"), + (outer_assignment_targets, visitor.assignment_targets) + } + _ => panic!( + "redefined_loop_name called on Statement that is not a With, For, AsyncWith, or AsyncFor" + ) }; let mut diagnostics = Vec::new(); diff --git a/crates/ruff/src/rules/pylint/rules/repeated_equality_comparison_target.rs b/crates/ruff/src/rules/pylint/rules/repeated_equality_comparison_target.rs new file mode 100644 index 0000000000..78608cd005 --- /dev/null +++ b/crates/ruff/src/rules/pylint/rules/repeated_equality_comparison_target.rs @@ -0,0 +1,151 @@ +use std::hash::BuildHasherDefault; +use std::ops::Deref; + +use itertools::{any, Itertools}; +use rustc_hash::FxHashMap; +use rustpython_parser::ast::{BoolOp, CmpOp, Expr, ExprBoolOp, ExprCompare, Ranged}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::hashable::HashableExpr; +use ruff_python_ast::source_code::Locator; + +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for repeated equality comparisons that can rewritten as a membership +/// test. +/// +/// ## Why is this bad? +/// To check if a variable is equal to one of many values, it is common to +/// write a series of equality comparisons (e.g., +/// `foo == "bar" or foo == "baz"). +/// +/// Instead, prefer to combine the values into a collection and use the `in` +/// operator to check for membership, which is more performant and succinct. +/// If the items are hashable, use a `set` for efficiency; otherwise, use a +/// `tuple`. +/// +/// ## Example +/// ```python +/// foo == "bar" or foo == "baz" or foo == "qux" +/// ``` +/// +/// Use instead: +/// ```python +/// foo in {"bar", "baz", "qux"} +/// ``` +/// +/// ## References +/// - [Python documentation: Comparisons](https://docs.python.org/3/reference/expressions.html#comparisons) +/// - [Python documentation: Membership test operations](https://docs.python.org/3/reference/expressions.html#membership-test-operations) +/// - [Python documentation: `set`](https://docs.python.org/3/library/stdtypes.html#set) +#[violation] +pub struct RepeatedEqualityComparisonTarget { + expr: String, +} + +impl Violation for RepeatedEqualityComparisonTarget { + #[derive_message_formats] + fn message(&self) -> String { + let RepeatedEqualityComparisonTarget { expr } = self; + format!( + "Consider merging multiple comparisons: `{expr}`. Use a `set` if the elements are hashable." + ) + } +} + +/// PLR1714 +pub(crate) fn repeated_equality_comparison_target(checker: &mut Checker, bool_op: &ExprBoolOp) { + if bool_op + .values + .iter() + .any(|value| !is_allowed_value(bool_op.op, value)) + { + return; + } + + let mut left_to_comparators: FxHashMap)> = + FxHashMap::with_capacity_and_hasher(bool_op.values.len(), BuildHasherDefault::default()); + for value in &bool_op.values { + if let Expr::Compare(ExprCompare { + left, comparators, .. + }) = value + { + let (count, matches) = left_to_comparators + .entry(left.deref().into()) + .or_insert_with(|| (0, Vec::new())); + *count += 1; + matches.extend(comparators); + } + } + + for (left, (count, comparators)) in left_to_comparators { + if count > 1 { + checker.diagnostics.push(Diagnostic::new( + RepeatedEqualityComparisonTarget { + expr: merged_membership_test( + left.as_expr(), + bool_op.op, + &comparators, + checker.locator, + ), + }, + bool_op.range(), + )); + } + } +} + +/// Return `true` if the given expression is compatible with a membership test. +/// E.g., `==` operators can be joined with `or` and `!=` operators can be +/// joined with `and`. +fn is_allowed_value(bool_op: BoolOp, value: &Expr) -> bool { + let Expr::Compare(ExprCompare { + left, + ops, + comparators, + .. + }) = value + else { + return false; + }; + + ops.iter().all(|op| { + if match bool_op { + BoolOp::Or => !matches!(op, CmpOp::Eq), + BoolOp::And => !matches!(op, CmpOp::NotEq), + } { + return false; + } + + if left.is_call_expr() { + return false; + } + + if any(comparators.iter(), Expr::is_call_expr) { + return false; + } + + true + }) +} + +/// Generate a string like `obj in (a, b, c)` or `obj not in (a, b, c)`. +fn merged_membership_test( + left: &Expr, + op: BoolOp, + comparators: &[&Expr], + locator: &Locator, +) -> String { + let op = match op { + BoolOp::Or => "in", + BoolOp::And => "not in", + }; + let left = locator.slice(left.range()); + let members = comparators + .iter() + .map(|comparator| locator.slice(comparator.range())) + .join(", "); + format!("{left} {op} ({members})",) +} diff --git a/crates/ruff/src/rules/pylint/rules/repeated_isinstance_calls.rs b/crates/ruff/src/rules/pylint/rules/repeated_isinstance_calls.rs index 031eee89d2..c4038767cc 100644 --- a/crates/ruff/src/rules/pylint/rules/repeated_isinstance_calls.rs +++ b/crates/ruff/src/rules/pylint/rules/repeated_isinstance_calls.rs @@ -1,6 +1,6 @@ use itertools::Itertools; use rustc_hash::{FxHashMap, FxHashSet}; -use rustpython_parser::ast::{self, Boolop, Expr, Ranged}; +use rustpython_parser::ast::{self, BoolOp, Expr, Ranged}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; @@ -36,6 +36,9 @@ use crate::settings::types::PythonVersion; /// return isinstance(x, int | float | complex) /// ``` /// +/// ## Options +/// - `target-version` +/// /// ## References /// - [Python documentation: `isinstance`](https://docs.python.org/3/library/functions.html#isinstance) #[violation] @@ -60,7 +63,7 @@ impl AlwaysAutofixableViolation for RepeatedIsinstanceCalls { pub(crate) fn repeated_isinstance_calls( checker: &mut Checker, expr: &Expr, - op: Boolop, + op: BoolOp, values: &[Expr], ) { if !op.is_or() { @@ -79,7 +82,7 @@ pub(crate) fn repeated_isinstance_calls( let [obj, types] = &args[..] else { continue; }; - if !checker.semantic_model().is_builtin("isinstance") { + if !checker.semantic().is_builtin("isinstance") { return; } let (num_calls, matches) = obj_to_types diff --git a/crates/ruff/src/rules/pylint/rules/return_in_init.rs b/crates/ruff/src/rules/pylint/rules/return_in_init.rs index 0bd7cd7758..0a6a81ebdc 100644 --- a/crates/ruff/src/rules/pylint/rules/return_in_init.rs +++ b/crates/ruff/src/rules/pylint/rules/return_in_init.rs @@ -1,7 +1,8 @@ -use rustpython_parser::ast::{self, Constant, Expr, Ranged, Stmt}; +use rustpython_parser::ast::{self, Ranged, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers::is_const_none; use crate::checkers::ast::Checker; use crate::rules::pylint::helpers::in_dunder_init; @@ -46,13 +47,7 @@ impl Violation for ReturnInInit { pub(crate) fn return_in_init(checker: &mut Checker, stmt: &Stmt) { if let Stmt::Return(ast::StmtReturn { value, range: _ }) = stmt { if let Some(expr) = value { - if matches!( - expr.as_ref(), - Expr::Constant(ast::ExprConstant { - value: Constant::None, - .. - }) - ) { + if is_const_none(expr) { // Explicit `return None`. return; } @@ -62,7 +57,7 @@ pub(crate) fn return_in_init(checker: &mut Checker, stmt: &Stmt) { } } - if in_dunder_init(checker.semantic_model(), checker.settings) { + if in_dunder_init(checker.semantic(), checker.settings) { checker .diagnostics .push(Diagnostic::new(ReturnInInit, stmt.range())); diff --git a/crates/ruff/src/rules/pylint/rules/single_string_slots.rs b/crates/ruff/src/rules/pylint/rules/single_string_slots.rs new file mode 100644 index 0000000000..54ca42e694 --- /dev/null +++ b/crates/ruff/src/rules/pylint/rules/single_string_slots.rs @@ -0,0 +1,107 @@ +use rustpython_parser::ast::{self, Constant, Expr, Stmt, StmtClassDef}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::identifier::Identifier; + +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for single strings assigned to `__slots__`. +/// +/// ## Why is this bad? +/// In Python, the `__slots__` attribute allows you to explicitly define the +/// attributes (instance variables) that a class can have. By default, Python +/// uses a dictionary to store an object's attributes, which incurs some memory +/// overhead. However, when `__slots__` is defined, Python uses a more compact +/// internal structure to store the object's attributes, resulting in memory +/// savings. +/// +/// Any string iterable may be assigned to `__slots__` (most commonly, a +/// `tuple` of strings). If a string is assigned to `__slots__`, it is +/// interpreted as a single attribute name, rather than an iterable of attribute +/// names. This can cause confusion, as users that iterate over the `__slots__` +/// value may expect to iterate over a sequence of attributes, but would instead +/// iterate over the characters of the string. +/// +/// To use a single string attribute in `__slots__`, wrap the string in an +/// iterable container type, like a `tuple`. +/// +/// ## Example +/// ```python +/// class Person: +/// __slots__: str = "name" +/// +/// def __init__(self, name: str) -> None: +/// self.name = name +/// ``` +/// +/// Use instead: +/// ```python +/// class Person: +/// __slots__: tuple[str, ...] = ("name",) +/// +/// def __init__(self, name: str) -> None: +/// self.name = name +/// ``` +/// +/// ## References +/// - [Python documentation: `__slots__`](https://docs.python.org/3/reference/datamodel.html#slots) +#[violation] +pub struct SingleStringSlots; + +impl Violation for SingleStringSlots { + #[derive_message_formats] + fn message(&self) -> String { + format!("Class `__slots__` should be a non-string iterable") + } +} + +/// PLC0205 +pub(crate) fn single_string_slots(checker: &mut Checker, class: &StmtClassDef) { + for stmt in &class.body { + match stmt { + Stmt::Assign(ast::StmtAssign { targets, value, .. }) => { + for target in targets { + if let Expr::Name(ast::ExprName { id, .. }) = target { + if id.as_str() == "__slots__" { + if matches!( + value.as_ref(), + Expr::Constant(ast::ExprConstant { + value: Constant::Str(_), + .. + }) | Expr::JoinedStr(_) + ) { + checker + .diagnostics + .push(Diagnostic::new(SingleStringSlots, stmt.identifier())); + } + } + } + } + } + Stmt::AnnAssign(ast::StmtAnnAssign { + target, + value: Some(value), + .. + }) => { + if let Expr::Name(ast::ExprName { id, .. }) = target.as_ref() { + if id.as_str() == "__slots__" { + if matches!( + value.as_ref(), + Expr::Constant(ast::ExprConstant { + value: Constant::Str(_), + .. + }) | Expr::JoinedStr(_) + ) { + checker + .diagnostics + .push(Diagnostic::new(SingleStringSlots, stmt.identifier())); + } + } + } + } + _ => {} + } + } +} diff --git a/crates/ruff/src/rules/pylint/rules/sys_exit_alias.rs b/crates/ruff/src/rules/pylint/rules/sys_exit_alias.rs index e9dccdd1cd..2e07090bf4 100644 --- a/crates/ruff/src/rules/pylint/rules/sys_exit_alias.rs +++ b/crates/ruff/src/rules/pylint/rules/sys_exit_alias.rs @@ -35,7 +35,7 @@ use crate::registry::AsRule; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/constants.html#constants-added-by-the-site-module) +/// - [Python documentation: Constants added by the `site` module](https://docs.python.org/3/library/constants.html#constants-added-by-the-site-module) #[violation] pub struct SysExitAlias { name: String, @@ -61,31 +61,31 @@ pub(crate) fn sys_exit_alias(checker: &mut Checker, func: &Expr) { let Expr::Name(ast::ExprName { id, .. }) = func else { return; }; - for name in ["exit", "quit"] { - if id != name { - continue; - } - if !checker.semantic_model().is_builtin(name) { - continue; - } - let mut diagnostic = Diagnostic::new( - SysExitAlias { - name: name.to_string(), - }, - func.range(), - ); - if checker.patch(diagnostic.kind.rule()) { - diagnostic.try_set_fix(|| { - let (import_edit, binding) = checker.importer.get_or_import_symbol( - &ImportRequest::import("sys", "exit"), - func.start(), - checker.semantic_model(), - )?; - let reference_edit = Edit::range_replacement(binding, func.range()); - #[allow(deprecated)] - Ok(Fix::unspecified_edits(import_edit, [reference_edit])) - }); - } - checker.diagnostics.push(diagnostic); + + if !matches!(id.as_str(), "exit" | "quit") { + return; } + + if !checker.semantic().is_builtin(id.as_str()) { + return; + } + + let mut diagnostic = Diagnostic::new( + SysExitAlias { + name: id.to_string(), + }, + func.range(), + ); + if checker.patch(diagnostic.kind.rule()) { + diagnostic.try_set_fix(|| { + let (import_edit, binding) = checker.importer.get_or_import_symbol( + &ImportRequest::import("sys", "exit"), + func.start(), + checker.semantic(), + )?; + let reference_edit = Edit::range_replacement(binding, func.range()); + Ok(Fix::suggested_edits(import_edit, [reference_edit])) + }); + } + checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/pylint/rules/too_many_arguments.rs b/crates/ruff/src/rules/pylint/rules/too_many_arguments.rs index bd06b69c99..4db54ced09 100644 --- a/crates/ruff/src/rules/pylint/rules/too_many_arguments.rs +++ b/crates/ruff/src/rules/pylint/rules/too_many_arguments.rs @@ -2,7 +2,7 @@ use rustpython_parser::ast::{Arguments, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::identifier_range; +use ruff_python_ast::identifier::Identifier; use crate::checkers::ast::Checker; @@ -58,21 +58,21 @@ impl Violation for TooManyArguments { } /// PLR0913 -pub(crate) fn too_many_arguments(checker: &mut Checker, args: &Arguments, stmt: &Stmt) { - let num_args = args +pub(crate) fn too_many_arguments(checker: &mut Checker, arguments: &Arguments, stmt: &Stmt) { + let num_arguments = arguments .args .iter() - .chain(args.kwonlyargs.iter()) - .chain(args.posonlyargs.iter()) - .filter(|arg| !checker.settings.dummy_variable_rgx.is_match(&arg.arg)) + .chain(&arguments.kwonlyargs) + .chain(&arguments.posonlyargs) + .filter(|arg| !checker.settings.dummy_variable_rgx.is_match(&arg.def.arg)) .count(); - if num_args > checker.settings.pylint.max_args { + if num_arguments > checker.settings.pylint.max_args { checker.diagnostics.push(Diagnostic::new( TooManyArguments { - c_args: num_args, + c_args: num_arguments, max_args: checker.settings.pylint.max_args, }, - identifier_range(stmt, checker.locator), + stmt.identifier(), )); } } diff --git a/crates/ruff/src/rules/pylint/rules/too_many_branches.rs b/crates/ruff/src/rules/pylint/rules/too_many_branches.rs index 449a261d2b..5d997ab843 100644 --- a/crates/ruff/src/rules/pylint/rules/too_many_branches.rs +++ b/crates/ruff/src/rules/pylint/rules/too_many_branches.rs @@ -1,9 +1,8 @@ -use rustpython_parser::ast::{self, Excepthandler, Stmt}; +use rustpython_parser::ast::{self, ExceptHandler, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::identifier_range; -use ruff_python_ast::source_code::Locator; +use ruff_python_ast::identifier::Identifier; /// ## What it does /// Checks for functions or methods with too many branches. @@ -147,8 +146,8 @@ fn num_branches(stmts: &[Stmt]) -> usize { .iter() .map(|handler| { 1 + { - let Excepthandler::ExceptHandler( - ast::ExcepthandlerExceptHandler { body, .. }, + let ExceptHandler::ExceptHandler( + ast::ExceptHandlerExceptHandler { body, .. }, ) = handler; num_branches(body) } @@ -166,7 +165,6 @@ pub(crate) fn too_many_branches( stmt: &Stmt, body: &[Stmt], max_branches: usize, - locator: &Locator, ) -> Option { let branches = num_branches(body); if branches > max_branches { @@ -175,7 +173,7 @@ pub(crate) fn too_many_branches( branches, max_branches, }, - identifier_range(stmt, locator), + stmt.identifier(), )) } else { None diff --git a/crates/ruff/src/rules/pylint/rules/too_many_return_statements.rs b/crates/ruff/src/rules/pylint/rules/too_many_return_statements.rs index 2a48517067..6b7f7de10a 100644 --- a/crates/ruff/src/rules/pylint/rules/too_many_return_statements.rs +++ b/crates/ruff/src/rules/pylint/rules/too_many_return_statements.rs @@ -2,8 +2,8 @@ use rustpython_parser::ast::Stmt; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::{identifier_range, ReturnStatementVisitor}; -use ruff_python_ast::source_code::Locator; +use ruff_python_ast::helpers::ReturnStatementVisitor; +use ruff_python_ast::identifier::Identifier; use ruff_python_ast::statement_visitor::StatementVisitor; /// ## What it does @@ -80,7 +80,6 @@ pub(crate) fn too_many_return_statements( stmt: &Stmt, body: &[Stmt], max_returns: usize, - locator: &Locator, ) -> Option { let returns = num_returns(body); if returns > max_returns { @@ -89,7 +88,7 @@ pub(crate) fn too_many_return_statements( returns, max_returns, }, - identifier_range(stmt, locator), + stmt.identifier(), )) } else { None diff --git a/crates/ruff/src/rules/pylint/rules/too_many_statements.rs b/crates/ruff/src/rules/pylint/rules/too_many_statements.rs index 87f3d57e9f..b22bffdbd8 100644 --- a/crates/ruff/src/rules/pylint/rules/too_many_statements.rs +++ b/crates/ruff/src/rules/pylint/rules/too_many_statements.rs @@ -1,9 +1,8 @@ -use rustpython_parser::ast::{self, Excepthandler, Stmt}; +use rustpython_parser::ast::{self, ExceptHandler, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::identifier_range; -use ruff_python_ast::source_code::Locator; +use ruff_python_ast::identifier::Identifier; /// ## What it does /// Checks for functions or methods with too many statements. @@ -123,7 +122,7 @@ fn num_statements(stmts: &[Stmt]) -> usize { } for handler in handlers { count += 1; - let Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { + let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { body, .. }) = handler; count += num_statements(body); @@ -149,7 +148,6 @@ pub(crate) fn too_many_statements( stmt: &Stmt, body: &[Stmt], max_statements: usize, - locator: &Locator, ) -> Option { let statements = num_statements(body); if statements > max_statements { @@ -158,7 +156,7 @@ pub(crate) fn too_many_statements( statements, max_statements, }, - identifier_range(stmt, locator), + stmt.identifier(), )) } else { None diff --git a/crates/ruff/src/rules/pylint/rules/type_bivariance.rs b/crates/ruff/src/rules/pylint/rules/type_bivariance.rs new file mode 100644 index 0000000000..b8f3c98faa --- /dev/null +++ b/crates/ruff/src/rules/pylint/rules/type_bivariance.rs @@ -0,0 +1,159 @@ +use std::fmt; + +use rustpython_parser::ast::{self, Expr, Ranged}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers::is_const_true; + +use crate::checkers::ast::Checker; +use crate::rules::pylint::helpers::type_param_name; + +/// ## What it does +/// Checks for `TypeVar` and `ParamSpec` definitions in which the type is +/// both covariant and contravariant. +/// +/// ## Why is this bad? +/// By default, Python's generic types are invariant, but can be marked as +/// either covariant or contravariant via the `covariant` and `contravariant` +/// keyword arguments. While the API does allow you to mark a type as both +/// covariant and contravariant, this is not supported by the type system, +/// and should be avoided. +/// +/// Instead, change the variance of the type to be either covariant, +/// contravariant, or invariant. If you want to describe both covariance and +/// contravariance, consider using two separate type parameters. +/// +/// For context: an "invariant" generic type only accepts values that exactly +/// match the type parameter; for example, `list[Dog]` accepts only `list[Dog]`, +/// not `list[Animal]` (superclass) or `list[Bulldog]` (subclass). This is +/// the default behavior for Python's generic types. +/// +/// A "covariant" generic type accepts subclasses of the type parameter; for +/// example, `Sequence[Animal]` accepts `Sequence[Dog]`. A "contravariant" +/// generic type accepts superclasses of the type parameter; for example, +/// `Callable[Dog]` accepts `Callable[Animal]`. +/// +/// ## Example +/// ```python +/// from typing import TypeVar +/// +/// T = TypeVar("T", covariant=True, contravariant=True) +/// ``` +/// +/// Use instead: +/// ```python +/// from typing import TypeVar +/// +/// T_co = TypeVar("T_co", covariant=True) +/// T_contra = TypeVar("T_contra", contravariant=True) +/// ``` +/// +/// ## References +/// - [Python documentation: `typing` — Support for type hints](https://docs.python.org/3/library/typing.html) +/// - [PEP 483 – The Theory of Type Hints: Covariance and Contravariance](https://peps.python.org/pep-0483/#covariance-and-contravariance) +/// - [PEP 484 – Type Hints: Covariance and contravariance](https://peps.python.org/pep-0484/#covariance-and-contravariance) +#[violation] +pub struct TypeBivariance { + kind: VarKind, + param_name: Option, +} + +impl Violation for TypeBivariance { + #[derive_message_formats] + fn message(&self) -> String { + let TypeBivariance { kind, param_name } = self; + match param_name { + None => format!("`{kind}` cannot be both covariant and contravariant"), + Some(param_name) => { + format!("`{kind}` \"{param_name}\" cannot be both covariant and contravariant",) + } + } + } +} + +/// PLC0131 +pub(crate) fn type_bivariance(checker: &mut Checker, value: &Expr) { + let Expr::Call(ast::ExprCall { + func, + args, + keywords, + .. + }) = value + else { + return; + }; + + let Some(covariant) = keywords + .iter() + .find(|keyword| { + keyword + .arg + .as_ref() + .map_or(false, |keyword| keyword.as_str() == "covariant") + }) + .map(|keyword| &keyword.value) + else { + return; + }; + + let Some(contravariant) = keywords + .iter() + .find(|keyword| { + keyword + .arg + .as_ref() + .map_or(false, |keyword| keyword.as_str() == "contravariant") + }) + .map(|keyword| &keyword.value) + else { + return; + }; + + if is_const_true(covariant) && is_const_true(contravariant) { + let Some(kind) = checker + .semantic() + .resolve_call_path(func) + .and_then(|call_path| { + if checker + .semantic() + .match_typing_call_path(&call_path, "ParamSpec") + { + Some(VarKind::ParamSpec) + } else if checker + .semantic() + .match_typing_call_path(&call_path, "TypeVar") + { + Some(VarKind::TypeVar) + } else { + None + } + }) + else { + return; + }; + + checker.diagnostics.push(Diagnostic::new( + TypeBivariance { + kind, + param_name: type_param_name(args, keywords).map(ToString::to_string), + }, + func.range(), + )); + } +} + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +enum VarKind { + TypeVar, + ParamSpec, +} + +impl fmt::Display for VarKind { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match self { + VarKind::TypeVar => fmt.write_str("TypeVar"), + VarKind::ParamSpec => fmt.write_str("ParamSpec"), + } + } +} diff --git a/crates/ruff/src/rules/pylint/rules/type_name_incorrect_variance.rs b/crates/ruff/src/rules/pylint/rules/type_name_incorrect_variance.rs new file mode 100644 index 0000000000..45e9622584 --- /dev/null +++ b/crates/ruff/src/rules/pylint/rules/type_name_incorrect_variance.rs @@ -0,0 +1,206 @@ +use std::fmt; + +use rustpython_parser::ast::{self, Expr, Ranged}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers::is_const_true; + +use crate::checkers::ast::Checker; +use crate::rules::pylint::helpers::type_param_name; + +/// ## What it does +/// Checks for type names that do not match the variance of their associated +/// type parameter. +/// +/// ## Why is this bad? +/// [PEP 484] recommends the use of the `_co` and `_contra` suffixes for +/// covariant and contravariant type parameters, respectively (while invariant +/// type parameters should not have any such suffix). +/// +/// ## Example +/// ```python +/// from typing import TypeVar +/// +/// T = TypeVar("T", covariant=True) +/// U = TypeVar("U", contravariant=True) +/// V_co = TypeVar("V_co") +/// ``` +/// +/// Use instead: +/// ```python +/// from typing import TypeVar +/// +/// T_co = TypeVar("T_co", covariant=True) +/// U_contra = TypeVar("U_contra", contravariant=True) +/// V = TypeVar("V") +/// ``` +/// +/// ## References +/// - [Python documentation: `typing` — Support for type hints](https://docs.python.org/3/library/typing.html) +/// - [PEP 483 – The Theory of Type Hints: Covariance and Contravariance](https://peps.python.org/pep-0483/#covariance-and-contravariance) +/// - [PEP 484 – Type Hints: Covariance and contravariance](https://peps.python.org/pep-0484/#covariance-and-contravariance) +/// +/// [PEP 484]: https://www.python.org/dev/peps/pep-0484/ +#[violation] +pub struct TypeNameIncorrectVariance { + kind: VarKind, + param_name: String, + variance: VarVariance, + replacement_name: String, +} + +impl Violation for TypeNameIncorrectVariance { + #[derive_message_formats] + fn message(&self) -> String { + let TypeNameIncorrectVariance { + kind, + param_name, + variance, + replacement_name, + } = self; + format!("`{kind}` name \"{param_name}\" does not reflect its {variance}; consider renaming it to \"{replacement_name}\"") + } +} + +/// PLC0105 +pub(crate) fn type_name_incorrect_variance(checker: &mut Checker, value: &Expr) { + let Expr::Call(ast::ExprCall { + func, + args, + keywords, + .. + }) = value + else { + return; + }; + + let Some(param_name) = type_param_name(args, keywords) else { + return; + }; + + let covariant = keywords + .iter() + .find(|keyword| { + keyword + .arg + .as_ref() + .map_or(false, |keyword| keyword.as_str() == "covariant") + }) + .map(|keyword| &keyword.value); + + let contravariant = keywords + .iter() + .find(|keyword| { + keyword + .arg + .as_ref() + .map_or(false, |keyword| keyword.as_str() == "contravariant") + }) + .map(|keyword| &keyword.value); + + if !mismatch(param_name, covariant, contravariant) { + return; + } + + let Some(kind) = checker + .semantic() + .resolve_call_path(func) + .and_then(|call_path| { + if checker + .semantic() + .match_typing_call_path(&call_path, "ParamSpec") + { + Some(VarKind::ParamSpec) + } else if checker + .semantic() + .match_typing_call_path(&call_path, "TypeVar") + { + Some(VarKind::TypeVar) + } else { + None + } + }) + else { + return; + }; + + let variance = variance(covariant, contravariant); + let name_root = param_name + .trim_end_matches("_co") + .trim_end_matches("_contra"); + let replacement_name: String = match variance { + VarVariance::Bivariance => return, // Bivariate type are invalid, so ignore them for this rule. + VarVariance::Covariance => format!("{name_root}_co"), + VarVariance::Contravariance => format!("{name_root}_contra"), + VarVariance::Invariance => name_root.to_string(), + }; + + checker.diagnostics.push(Diagnostic::new( + TypeNameIncorrectVariance { + kind, + param_name: param_name.to_string(), + variance, + replacement_name, + }, + func.range(), + )); +} + +/// Returns `true` if the parameter name does not match its type variance. +fn mismatch(param_name: &str, covariant: Option<&Expr>, contravariant: Option<&Expr>) -> bool { + if param_name.ends_with("_co") { + covariant.map_or(true, |covariant| !is_const_true(covariant)) + } else if param_name.ends_with("_contra") { + contravariant.map_or(true, |contravariant| !is_const_true(contravariant)) + } else { + covariant.map_or(false, is_const_true) || contravariant.map_or(false, is_const_true) + } +} + +/// Return the variance of the type parameter. +fn variance(covariant: Option<&Expr>, contravariant: Option<&Expr>) -> VarVariance { + match ( + covariant.map(is_const_true), + contravariant.map(is_const_true), + ) { + (Some(true), Some(true)) => VarVariance::Bivariance, + (Some(true), _) => VarVariance::Covariance, + (_, Some(true)) => VarVariance::Contravariance, + _ => VarVariance::Invariance, + } +} + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +enum VarKind { + TypeVar, + ParamSpec, +} + +impl fmt::Display for VarKind { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match self { + VarKind::TypeVar => fmt.write_str("TypeVar"), + VarKind::ParamSpec => fmt.write_str("ParamSpec"), + } + } +} + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +enum VarVariance { + Bivariance, + Covariance, + Contravariance, + Invariance, +} + +impl fmt::Display for VarVariance { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match self { + VarVariance::Bivariance => fmt.write_str("bivariance"), + VarVariance::Covariance => fmt.write_str("covariance"), + VarVariance::Contravariance => fmt.write_str("contravariance"), + VarVariance::Invariance => fmt.write_str("invariance"), + } + } +} diff --git a/crates/ruff/src/rules/pylint/rules/type_param_name_mismatch.rs b/crates/ruff/src/rules/pylint/rules/type_param_name_mismatch.rs new file mode 100644 index 0000000000..e7bbc7ae04 --- /dev/null +++ b/crates/ruff/src/rules/pylint/rules/type_param_name_mismatch.rs @@ -0,0 +1,147 @@ +use std::fmt; + +use rustpython_parser::ast::{self, Expr, Ranged}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; + +use crate::checkers::ast::Checker; +use crate::rules::pylint::helpers::type_param_name; + +/// ## What it does +/// Checks for `TypeVar`, `TypeVarTuple`, `ParamSpec`, and `NewType` +/// definitions in which the name of the type parameter does not match the name +/// of the variable to which it is assigned. +/// +/// ## Why is this bad? +/// When defining a `TypeVar` or a related type parameter, Python allows you to +/// provide a name for the type parameter. According to [PEP 484], the name +/// provided to the `TypeVar` constructor must be equal to the name of the +/// variable to which it is assigned. +/// +/// ## Example +/// ```python +/// from typing import TypeVar +/// +/// T = TypeVar("U") +/// ``` +/// +/// Use instead: +/// ```python +/// from typing import TypeVar +/// +/// T = TypeVar("T") +/// ``` +/// +/// ## References +/// - [Python documentation: `typing` — Support for type hints](https://docs.python.org/3/library/typing.html) +/// - [PEP 484 – Type Hints: Generics](https://peps.python.org/pep-0484/#generics) +/// +/// [PEP 484]:https://peps.python.org/pep-0484/#generics +#[violation] +pub struct TypeParamNameMismatch { + kind: VarKind, + var_name: String, + param_name: String, +} + +impl Violation for TypeParamNameMismatch { + #[derive_message_formats] + fn message(&self) -> String { + let TypeParamNameMismatch { + kind, + var_name, + param_name, + } = self; + format!("`{kind}` name `{param_name}` does not match assigned variable name `{var_name}`") + } +} + +/// PLC0132 +pub(crate) fn type_param_name_mismatch(checker: &mut Checker, value: &Expr, targets: &[Expr]) { + let [target] = targets else { + return; + }; + + let Expr::Name(ast::ExprName { id: var_name, .. }) = &target else { + return; + }; + + let Expr::Call(ast::ExprCall { + func, + args, + keywords, + .. + }) = value + else { + return; + }; + + let Some(param_name) = type_param_name(args, keywords) else { + return; + }; + + if var_name == param_name { + return; + } + + let Some(kind) = checker + .semantic() + .resolve_call_path(func) + .and_then(|call_path| { + if checker + .semantic() + .match_typing_call_path(&call_path, "ParamSpec") + { + Some(VarKind::ParamSpec) + } else if checker + .semantic() + .match_typing_call_path(&call_path, "TypeVar") + { + Some(VarKind::TypeVar) + } else if checker + .semantic() + .match_typing_call_path(&call_path, "TypeVarTuple") + { + Some(VarKind::TypeVarTuple) + } else if checker + .semantic() + .match_typing_call_path(&call_path, "NewType") + { + Some(VarKind::NewType) + } else { + None + } + }) + else { + return; + }; + + checker.diagnostics.push(Diagnostic::new( + TypeParamNameMismatch { + kind, + var_name: var_name.to_string(), + param_name: param_name.to_string(), + }, + value.range(), + )); +} + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +enum VarKind { + TypeVar, + ParamSpec, + TypeVarTuple, + NewType, +} + +impl fmt::Display for VarKind { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match self { + VarKind::TypeVar => fmt.write_str("TypeVar"), + VarKind::ParamSpec => fmt.write_str("ParamSpec"), + VarKind::TypeVarTuple => fmt.write_str("TypeVarTuple"), + VarKind::NewType => fmt.write_str("NewType"), + } + } +} diff --git a/crates/ruff/src/rules/pylint/rules/unexpected_special_method_signature.rs b/crates/ruff/src/rules/pylint/rules/unexpected_special_method_signature.rs index e22a2a2655..e77b612130 100644 --- a/crates/ruff/src/rules/pylint/rules/unexpected_special_method_signature.rs +++ b/crates/ruff/src/rules/pylint/rules/unexpected_special_method_signature.rs @@ -4,8 +4,7 @@ use rustpython_parser::ast::{Arguments, Decorator, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::identifier_range; -use ruff_python_ast::source_code::Locator; +use ruff_python_ast::identifier::Identifier; use ruff_python_semantic::analyze::visibility::is_staticmethod; use crate::checkers::ast::Checker; @@ -110,7 +109,7 @@ impl ExpectedParams { /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/reference/datamodel.html) +/// - [Python documentation: Data model](https://docs.python.org/3/reference/datamodel.html) #[violation] pub struct UnexpectedSpecialMethodSignature { method_name: String, @@ -143,9 +142,8 @@ pub(crate) fn unexpected_special_method_signature( name: &str, decorator_list: &[Decorator], args: &Arguments, - locator: &Locator, ) { - if !checker.semantic_model().scope().kind.is_class() { + if !checker.semantic().scope().kind.is_class() { return; } @@ -160,10 +158,11 @@ pub(crate) fn unexpected_special_method_signature( } let actual_params = args.args.len(); - let optional_params = args.defaults.len(); - let mandatory_params = actual_params - optional_params; + let mandatory_params = args.args.iter().filter(|arg| arg.default.is_none()).count(); - let Some(expected_params) = ExpectedParams::from_method(name, is_staticmethod(checker.semantic_model(), decorator_list)) else { + let Some(expected_params) = + ExpectedParams::from_method(name, is_staticmethod(decorator_list, checker.semantic())) + else { return; }; @@ -189,7 +188,7 @@ pub(crate) fn unexpected_special_method_signature( expected_params, actual_params, }, - identifier_range(stmt, locator), + stmt.identifier(), )); } } diff --git a/crates/ruff/src/rules/pylint/rules/unnecessary_direct_lambda_call.rs b/crates/ruff/src/rules/pylint/rules/unnecessary_direct_lambda_call.rs index 32207ad885..528cbee097 100644 --- a/crates/ruff/src/rules/pylint/rules/unnecessary_direct_lambda_call.rs +++ b/crates/ruff/src/rules/pylint/rules/unnecessary_direct_lambda_call.rs @@ -23,7 +23,7 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/reference/expressions.html#lambda) +/// - [Python documentation: Lambdas](https://docs.python.org/3/reference/expressions.html#lambda) #[violation] pub struct UnnecessaryDirectLambdaCall; diff --git a/crates/ruff/src/rules/pylint/rules/useless_else_on_loop.rs b/crates/ruff/src/rules/pylint/rules/useless_else_on_loop.rs index 79404a5bca..32fb1b8b59 100644 --- a/crates/ruff/src/rules/pylint/rules/useless_else_on_loop.rs +++ b/crates/ruff/src/rules/pylint/rules/useless_else_on_loop.rs @@ -1,8 +1,8 @@ -use rustpython_parser::ast::{self, Excepthandler, MatchCase, Stmt}; +use rustpython_parser::ast::{self, ExceptHandler, MatchCase, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers; +use ruff_python_ast::identifier; use crate::checkers::ast::Checker; @@ -37,7 +37,7 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/tutorial/controlflow.html#break-and-continue-statements-and-else-clauses-on-loops) +/// - [Python documentation: `break` and `continue` Statements, and `else` Clauses on Loops](https://docs.python.org/3/tutorial/controlflow.html#break-and-continue-statements-and-else-clauses-on-loops) #[violation] pub struct UselessElseOnLoop; @@ -79,7 +79,7 @@ fn loop_exits_early(body: &[Stmt]) -> bool { || loop_exits_early(orelse) || loop_exits_early(finalbody) || handlers.iter().any(|handler| match handler { - Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { + ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { body, .. }) => loop_exits_early(body), }) @@ -102,7 +102,7 @@ pub(crate) fn useless_else_on_loop( if !orelse.is_empty() && !loop_exits_early(body) { checker.diagnostics.push(Diagnostic::new( UselessElseOnLoop, - helpers::else_range(stmt, checker.locator).unwrap(), + identifier::else_(stmt, checker.locator).unwrap(), )); } } diff --git a/crates/ruff/src/rules/pylint/rules/useless_import_alias.rs b/crates/ruff/src/rules/pylint/rules/useless_import_alias.rs index 7c657a5840..481be6265a 100644 --- a/crates/ruff/src/rules/pylint/rules/useless_import_alias.rs +++ b/crates/ruff/src/rules/pylint/rules/useless_import_alias.rs @@ -43,14 +43,13 @@ pub(crate) fn useless_import_alias(checker: &mut Checker, alias: &Alias) { if alias.name.contains('.') { return; } - if &alias.name != asname { + if alias.name.as_str() != asname.as_str() { return; } let mut diagnostic = Diagnostic::new(UselessImportAlias, alias.range()); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( asname.to_string(), alias.range(), ))); diff --git a/crates/ruff/src/rules/pylint/rules/useless_return.rs b/crates/ruff/src/rules/pylint/rules/useless_return.rs index 82436e976e..28e7c689f4 100644 --- a/crates/ruff/src/rules/pylint/rules/useless_return.rs +++ b/crates/ruff/src/rules/pylint/rules/useless_return.rs @@ -85,7 +85,7 @@ pub(crate) fn useless_return<'a>( } // Verify that the last statement is a return statement. - let Stmt::Return(ast::StmtReturn { value, range: _}) = &last_stmt else { + let Stmt::Return(ast::StmtReturn { value, range: _ }) = &last_stmt else { return; }; @@ -103,13 +103,8 @@ pub(crate) fn useless_return<'a>( let mut diagnostic = Diagnostic::new(UselessReturn, last_stmt.range()); if checker.patch(diagnostic.kind.rule()) { - let edit = autofix::edits::delete_stmt( - last_stmt, - Some(stmt), - checker.locator, - checker.indexer, - checker.stylist, - ); + let edit = + autofix::edits::delete_stmt(last_stmt, Some(stmt), checker.locator, checker.indexer); diagnostic.set_fix(Fix::automatic(edit).isolate(checker.isolation(Some(stmt)))); } checker.diagnostics.push(diagnostic); diff --git a/crates/ruff/src/rules/pylint/rules/yield_from_in_async_function.rs b/crates/ruff/src/rules/pylint/rules/yield_from_in_async_function.rs index 4895faa12a..9b329b3a19 100644 --- a/crates/ruff/src/rules/pylint/rules/yield_from_in_async_function.rs +++ b/crates/ruff/src/rules/pylint/rules/yield_from_in_async_function.rs @@ -38,7 +38,7 @@ impl Violation for YieldFromInAsyncFunction { /// PLE1700 pub(crate) fn yield_from_in_async_function(checker: &mut Checker, expr: &ExprYieldFrom) { - let scope = checker.semantic_model().scope(); + let scope = checker.semantic().scope(); if scope.kind.is_async_function() { checker .diagnostics diff --git a/crates/ruff/src/rules/pylint/rules/yield_in_init.rs b/crates/ruff/src/rules/pylint/rules/yield_in_init.rs index 9ee87e70a7..2fabc842cc 100644 --- a/crates/ruff/src/rules/pylint/rules/yield_in_init.rs +++ b/crates/ruff/src/rules/pylint/rules/yield_in_init.rs @@ -39,7 +39,7 @@ impl Violation for YieldInInit { /// PLE0100 pub(crate) fn yield_in_init(checker: &mut Checker, expr: &Expr) { - if in_dunder_init(checker.semantic_model(), checker.settings) { + if in_dunder_init(checker.semantic(), checker.settings) { checker .diagnostics .push(Diagnostic::new(YieldInInit, expr.range())); diff --git a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLC0105_type_name_incorrect_variance.py.snap b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLC0105_type_name_incorrect_variance.py.snap new file mode 100644 index 0000000000..675130c6ea --- /dev/null +++ b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLC0105_type_name_incorrect_variance.py.snap @@ -0,0 +1,318 @@ +--- +source: crates/ruff/src/rules/pylint/mod.rs +--- +type_name_incorrect_variance.py:5:5: PLC0105 `TypeVar` name "T" does not reflect its covariance; consider renaming it to "T_co" + | +3 | # Errors. +4 | +5 | T = TypeVar("T", covariant=True) + | ^^^^^^^ PLC0105 +6 | T = TypeVar("T", covariant=True, contravariant=False) +7 | T = TypeVar("T", contravariant=True) + | + +type_name_incorrect_variance.py:6:5: PLC0105 `TypeVar` name "T" does not reflect its covariance; consider renaming it to "T_co" + | +5 | T = TypeVar("T", covariant=True) +6 | T = TypeVar("T", covariant=True, contravariant=False) + | ^^^^^^^ PLC0105 +7 | T = TypeVar("T", contravariant=True) +8 | T = TypeVar("T", covariant=False, contravariant=True) + | + +type_name_incorrect_variance.py:7:5: PLC0105 `TypeVar` name "T" does not reflect its contravariance; consider renaming it to "T_contra" + | +5 | T = TypeVar("T", covariant=True) +6 | T = TypeVar("T", covariant=True, contravariant=False) +7 | T = TypeVar("T", contravariant=True) + | ^^^^^^^ PLC0105 +8 | T = TypeVar("T", covariant=False, contravariant=True) +9 | P = ParamSpec("P", covariant=True) + | + +type_name_incorrect_variance.py:8:5: PLC0105 `TypeVar` name "T" does not reflect its contravariance; consider renaming it to "T_contra" + | + 6 | T = TypeVar("T", covariant=True, contravariant=False) + 7 | T = TypeVar("T", contravariant=True) + 8 | T = TypeVar("T", covariant=False, contravariant=True) + | ^^^^^^^ PLC0105 + 9 | P = ParamSpec("P", covariant=True) +10 | P = ParamSpec("P", covariant=True, contravariant=False) + | + +type_name_incorrect_variance.py:9:5: PLC0105 `ParamSpec` name "P" does not reflect its covariance; consider renaming it to "P_co" + | + 7 | T = TypeVar("T", contravariant=True) + 8 | T = TypeVar("T", covariant=False, contravariant=True) + 9 | P = ParamSpec("P", covariant=True) + | ^^^^^^^^^ PLC0105 +10 | P = ParamSpec("P", covariant=True, contravariant=False) +11 | P = ParamSpec("P", contravariant=True) + | + +type_name_incorrect_variance.py:10:5: PLC0105 `ParamSpec` name "P" does not reflect its covariance; consider renaming it to "P_co" + | + 8 | T = TypeVar("T", covariant=False, contravariant=True) + 9 | P = ParamSpec("P", covariant=True) +10 | P = ParamSpec("P", covariant=True, contravariant=False) + | ^^^^^^^^^ PLC0105 +11 | P = ParamSpec("P", contravariant=True) +12 | P = ParamSpec("P", covariant=False, contravariant=True) + | + +type_name_incorrect_variance.py:11:5: PLC0105 `ParamSpec` name "P" does not reflect its contravariance; consider renaming it to "P_contra" + | + 9 | P = ParamSpec("P", covariant=True) +10 | P = ParamSpec("P", covariant=True, contravariant=False) +11 | P = ParamSpec("P", contravariant=True) + | ^^^^^^^^^ PLC0105 +12 | P = ParamSpec("P", covariant=False, contravariant=True) + | + +type_name_incorrect_variance.py:12:5: PLC0105 `ParamSpec` name "P" does not reflect its contravariance; consider renaming it to "P_contra" + | +10 | P = ParamSpec("P", covariant=True, contravariant=False) +11 | P = ParamSpec("P", contravariant=True) +12 | P = ParamSpec("P", covariant=False, contravariant=True) + | ^^^^^^^^^ PLC0105 +13 | +14 | T_co = TypeVar("T_co") + | + +type_name_incorrect_variance.py:14:8: PLC0105 `TypeVar` name "T_co" does not reflect its invariance; consider renaming it to "T" + | +12 | P = ParamSpec("P", covariant=False, contravariant=True) +13 | +14 | T_co = TypeVar("T_co") + | ^^^^^^^ PLC0105 +15 | T_co = TypeVar("T_co", covariant=False) +16 | T_co = TypeVar("T_co", contravariant=False) + | + +type_name_incorrect_variance.py:15:8: PLC0105 `TypeVar` name "T_co" does not reflect its invariance; consider renaming it to "T" + | +14 | T_co = TypeVar("T_co") +15 | T_co = TypeVar("T_co", covariant=False) + | ^^^^^^^ PLC0105 +16 | T_co = TypeVar("T_co", contravariant=False) +17 | T_co = TypeVar("T_co", covariant=False, contravariant=False) + | + +type_name_incorrect_variance.py:16:8: PLC0105 `TypeVar` name "T_co" does not reflect its invariance; consider renaming it to "T" + | +14 | T_co = TypeVar("T_co") +15 | T_co = TypeVar("T_co", covariant=False) +16 | T_co = TypeVar("T_co", contravariant=False) + | ^^^^^^^ PLC0105 +17 | T_co = TypeVar("T_co", covariant=False, contravariant=False) +18 | T_co = TypeVar("T_co", contravariant=True) + | + +type_name_incorrect_variance.py:17:8: PLC0105 `TypeVar` name "T_co" does not reflect its invariance; consider renaming it to "T" + | +15 | T_co = TypeVar("T_co", covariant=False) +16 | T_co = TypeVar("T_co", contravariant=False) +17 | T_co = TypeVar("T_co", covariant=False, contravariant=False) + | ^^^^^^^ PLC0105 +18 | T_co = TypeVar("T_co", contravariant=True) +19 | T_co = TypeVar("T_co", covariant=False, contravariant=True) + | + +type_name_incorrect_variance.py:18:8: PLC0105 `TypeVar` name "T_co" does not reflect its contravariance; consider renaming it to "T_contra" + | +16 | T_co = TypeVar("T_co", contravariant=False) +17 | T_co = TypeVar("T_co", covariant=False, contravariant=False) +18 | T_co = TypeVar("T_co", contravariant=True) + | ^^^^^^^ PLC0105 +19 | T_co = TypeVar("T_co", covariant=False, contravariant=True) +20 | P_co = ParamSpec("P_co") + | + +type_name_incorrect_variance.py:19:8: PLC0105 `TypeVar` name "T_co" does not reflect its contravariance; consider renaming it to "T_contra" + | +17 | T_co = TypeVar("T_co", covariant=False, contravariant=False) +18 | T_co = TypeVar("T_co", contravariant=True) +19 | T_co = TypeVar("T_co", covariant=False, contravariant=True) + | ^^^^^^^ PLC0105 +20 | P_co = ParamSpec("P_co") +21 | P_co = ParamSpec("P_co", covariant=False) + | + +type_name_incorrect_variance.py:20:8: PLC0105 `ParamSpec` name "P_co" does not reflect its invariance; consider renaming it to "P" + | +18 | T_co = TypeVar("T_co", contravariant=True) +19 | T_co = TypeVar("T_co", covariant=False, contravariant=True) +20 | P_co = ParamSpec("P_co") + | ^^^^^^^^^ PLC0105 +21 | P_co = ParamSpec("P_co", covariant=False) +22 | P_co = ParamSpec("P_co", contravariant=False) + | + +type_name_incorrect_variance.py:21:8: PLC0105 `ParamSpec` name "P_co" does not reflect its invariance; consider renaming it to "P" + | +19 | T_co = TypeVar("T_co", covariant=False, contravariant=True) +20 | P_co = ParamSpec("P_co") +21 | P_co = ParamSpec("P_co", covariant=False) + | ^^^^^^^^^ PLC0105 +22 | P_co = ParamSpec("P_co", contravariant=False) +23 | P_co = ParamSpec("P_co", covariant=False, contravariant=False) + | + +type_name_incorrect_variance.py:22:8: PLC0105 `ParamSpec` name "P_co" does not reflect its invariance; consider renaming it to "P" + | +20 | P_co = ParamSpec("P_co") +21 | P_co = ParamSpec("P_co", covariant=False) +22 | P_co = ParamSpec("P_co", contravariant=False) + | ^^^^^^^^^ PLC0105 +23 | P_co = ParamSpec("P_co", covariant=False, contravariant=False) +24 | P_co = ParamSpec("P_co", contravariant=True) + | + +type_name_incorrect_variance.py:23:8: PLC0105 `ParamSpec` name "P_co" does not reflect its invariance; consider renaming it to "P" + | +21 | P_co = ParamSpec("P_co", covariant=False) +22 | P_co = ParamSpec("P_co", contravariant=False) +23 | P_co = ParamSpec("P_co", covariant=False, contravariant=False) + | ^^^^^^^^^ PLC0105 +24 | P_co = ParamSpec("P_co", contravariant=True) +25 | P_co = ParamSpec("P_co", covariant=False, contravariant=True) + | + +type_name_incorrect_variance.py:24:8: PLC0105 `ParamSpec` name "P_co" does not reflect its contravariance; consider renaming it to "P_contra" + | +22 | P_co = ParamSpec("P_co", contravariant=False) +23 | P_co = ParamSpec("P_co", covariant=False, contravariant=False) +24 | P_co = ParamSpec("P_co", contravariant=True) + | ^^^^^^^^^ PLC0105 +25 | P_co = ParamSpec("P_co", covariant=False, contravariant=True) + | + +type_name_incorrect_variance.py:25:8: PLC0105 `ParamSpec` name "P_co" does not reflect its contravariance; consider renaming it to "P_contra" + | +23 | P_co = ParamSpec("P_co", covariant=False, contravariant=False) +24 | P_co = ParamSpec("P_co", contravariant=True) +25 | P_co = ParamSpec("P_co", covariant=False, contravariant=True) + | ^^^^^^^^^ PLC0105 +26 | +27 | T_contra = TypeVar("T_contra") + | + +type_name_incorrect_variance.py:27:12: PLC0105 `TypeVar` name "T_contra" does not reflect its invariance; consider renaming it to "T" + | +25 | P_co = ParamSpec("P_co", covariant=False, contravariant=True) +26 | +27 | T_contra = TypeVar("T_contra") + | ^^^^^^^ PLC0105 +28 | T_contra = TypeVar("T_contra", covariant=False) +29 | T_contra = TypeVar("T_contra", contravariant=False) + | + +type_name_incorrect_variance.py:28:12: PLC0105 `TypeVar` name "T_contra" does not reflect its invariance; consider renaming it to "T" + | +27 | T_contra = TypeVar("T_contra") +28 | T_contra = TypeVar("T_contra", covariant=False) + | ^^^^^^^ PLC0105 +29 | T_contra = TypeVar("T_contra", contravariant=False) +30 | T_contra = TypeVar("T_contra", covariant=False, contravariant=False) + | + +type_name_incorrect_variance.py:29:12: PLC0105 `TypeVar` name "T_contra" does not reflect its invariance; consider renaming it to "T" + | +27 | T_contra = TypeVar("T_contra") +28 | T_contra = TypeVar("T_contra", covariant=False) +29 | T_contra = TypeVar("T_contra", contravariant=False) + | ^^^^^^^ PLC0105 +30 | T_contra = TypeVar("T_contra", covariant=False, contravariant=False) +31 | T_contra = TypeVar("T_contra", covariant=True) + | + +type_name_incorrect_variance.py:30:12: PLC0105 `TypeVar` name "T_contra" does not reflect its invariance; consider renaming it to "T" + | +28 | T_contra = TypeVar("T_contra", covariant=False) +29 | T_contra = TypeVar("T_contra", contravariant=False) +30 | T_contra = TypeVar("T_contra", covariant=False, contravariant=False) + | ^^^^^^^ PLC0105 +31 | T_contra = TypeVar("T_contra", covariant=True) +32 | T_contra = TypeVar("T_contra", covariant=True, contravariant=False) + | + +type_name_incorrect_variance.py:31:12: PLC0105 `TypeVar` name "T_contra" does not reflect its covariance; consider renaming it to "T_co" + | +29 | T_contra = TypeVar("T_contra", contravariant=False) +30 | T_contra = TypeVar("T_contra", covariant=False, contravariant=False) +31 | T_contra = TypeVar("T_contra", covariant=True) + | ^^^^^^^ PLC0105 +32 | T_contra = TypeVar("T_contra", covariant=True, contravariant=False) +33 | P_contra = ParamSpec("P_contra") + | + +type_name_incorrect_variance.py:32:12: PLC0105 `TypeVar` name "T_contra" does not reflect its covariance; consider renaming it to "T_co" + | +30 | T_contra = TypeVar("T_contra", covariant=False, contravariant=False) +31 | T_contra = TypeVar("T_contra", covariant=True) +32 | T_contra = TypeVar("T_contra", covariant=True, contravariant=False) + | ^^^^^^^ PLC0105 +33 | P_contra = ParamSpec("P_contra") +34 | P_contra = ParamSpec("P_contra", covariant=False) + | + +type_name_incorrect_variance.py:33:12: PLC0105 `ParamSpec` name "P_contra" does not reflect its invariance; consider renaming it to "P" + | +31 | T_contra = TypeVar("T_contra", covariant=True) +32 | T_contra = TypeVar("T_contra", covariant=True, contravariant=False) +33 | P_contra = ParamSpec("P_contra") + | ^^^^^^^^^ PLC0105 +34 | P_contra = ParamSpec("P_contra", covariant=False) +35 | P_contra = ParamSpec("P_contra", contravariant=False) + | + +type_name_incorrect_variance.py:34:12: PLC0105 `ParamSpec` name "P_contra" does not reflect its invariance; consider renaming it to "P" + | +32 | T_contra = TypeVar("T_contra", covariant=True, contravariant=False) +33 | P_contra = ParamSpec("P_contra") +34 | P_contra = ParamSpec("P_contra", covariant=False) + | ^^^^^^^^^ PLC0105 +35 | P_contra = ParamSpec("P_contra", contravariant=False) +36 | P_contra = ParamSpec("P_contra", covariant=False, contravariant=False) + | + +type_name_incorrect_variance.py:35:12: PLC0105 `ParamSpec` name "P_contra" does not reflect its invariance; consider renaming it to "P" + | +33 | P_contra = ParamSpec("P_contra") +34 | P_contra = ParamSpec("P_contra", covariant=False) +35 | P_contra = ParamSpec("P_contra", contravariant=False) + | ^^^^^^^^^ PLC0105 +36 | P_contra = ParamSpec("P_contra", covariant=False, contravariant=False) +37 | P_contra = ParamSpec("P_contra", covariant=True) + | + +type_name_incorrect_variance.py:36:12: PLC0105 `ParamSpec` name "P_contra" does not reflect its invariance; consider renaming it to "P" + | +34 | P_contra = ParamSpec("P_contra", covariant=False) +35 | P_contra = ParamSpec("P_contra", contravariant=False) +36 | P_contra = ParamSpec("P_contra", covariant=False, contravariant=False) + | ^^^^^^^^^ PLC0105 +37 | P_contra = ParamSpec("P_contra", covariant=True) +38 | P_contra = ParamSpec("P_contra", covariant=True, contravariant=False) + | + +type_name_incorrect_variance.py:37:12: PLC0105 `ParamSpec` name "P_contra" does not reflect its covariance; consider renaming it to "P_co" + | +35 | P_contra = ParamSpec("P_contra", contravariant=False) +36 | P_contra = ParamSpec("P_contra", covariant=False, contravariant=False) +37 | P_contra = ParamSpec("P_contra", covariant=True) + | ^^^^^^^^^ PLC0105 +38 | P_contra = ParamSpec("P_contra", covariant=True, contravariant=False) + | + +type_name_incorrect_variance.py:38:12: PLC0105 `ParamSpec` name "P_contra" does not reflect its covariance; consider renaming it to "P_co" + | +36 | P_contra = ParamSpec("P_contra", covariant=False, contravariant=False) +37 | P_contra = ParamSpec("P_contra", covariant=True) +38 | P_contra = ParamSpec("P_contra", covariant=True, contravariant=False) + | ^^^^^^^^^ PLC0105 +39 | +40 | # Non-errors. + | + + diff --git a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLC0131_type_bivariance.py.snap b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLC0131_type_bivariance.py.snap new file mode 100644 index 0000000000..01107aac91 --- /dev/null +++ b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLC0131_type_bivariance.py.snap @@ -0,0 +1,40 @@ +--- +source: crates/ruff/src/rules/pylint/mod.rs +--- +type_bivariance.py:5:5: PLC0131 `TypeVar` "T" cannot be both covariant and contravariant + | +3 | # Errors. +4 | +5 | T = TypeVar("T", covariant=True, contravariant=True) + | ^^^^^^^ PLC0131 +6 | T = TypeVar(name="T", covariant=True, contravariant=True) + | + +type_bivariance.py:6:5: PLC0131 `TypeVar` "T" cannot be both covariant and contravariant + | +5 | T = TypeVar("T", covariant=True, contravariant=True) +6 | T = TypeVar(name="T", covariant=True, contravariant=True) + | ^^^^^^^ PLC0131 +7 | +8 | T = ParamSpec("T", covariant=True, contravariant=True) + | + +type_bivariance.py:8:5: PLC0131 `ParamSpec` "T" cannot be both covariant and contravariant + | +6 | T = TypeVar(name="T", covariant=True, contravariant=True) +7 | +8 | T = ParamSpec("T", covariant=True, contravariant=True) + | ^^^^^^^^^ PLC0131 +9 | T = ParamSpec(name="T", covariant=True, contravariant=True) + | + +type_bivariance.py:9:5: PLC0131 `ParamSpec` "T" cannot be both covariant and contravariant + | + 8 | T = ParamSpec("T", covariant=True, contravariant=True) + 9 | T = ParamSpec(name="T", covariant=True, contravariant=True) + | ^^^^^^^^^ PLC0131 +10 | +11 | # Non-errors. + | + + diff --git a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLC0132_type_param_name_mismatch.py.snap b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLC0132_type_param_name_mismatch.py.snap new file mode 100644 index 0000000000..44084245c2 --- /dev/null +++ b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLC0132_type_param_name_mismatch.py.snap @@ -0,0 +1,76 @@ +--- +source: crates/ruff/src/rules/pylint/mod.rs +--- +type_param_name_mismatch.py:5:5: PLC0132 `TypeVar` name `T` does not match assigned variable name `X` + | +3 | # Errors. +4 | +5 | X = TypeVar("T") + | ^^^^^^^^^^^^ PLC0132 +6 | X = TypeVar(name="T") + | + +type_param_name_mismatch.py:6:5: PLC0132 `TypeVar` name `T` does not match assigned variable name `X` + | +5 | X = TypeVar("T") +6 | X = TypeVar(name="T") + | ^^^^^^^^^^^^^^^^^ PLC0132 +7 | +8 | Y = ParamSpec("T") + | + +type_param_name_mismatch.py:8:5: PLC0132 `ParamSpec` name `T` does not match assigned variable name `Y` + | +6 | X = TypeVar(name="T") +7 | +8 | Y = ParamSpec("T") + | ^^^^^^^^^^^^^^ PLC0132 +9 | Y = ParamSpec(name="T") + | + +type_param_name_mismatch.py:9:5: PLC0132 `ParamSpec` name `T` does not match assigned variable name `Y` + | + 8 | Y = ParamSpec("T") + 9 | Y = ParamSpec(name="T") + | ^^^^^^^^^^^^^^^^^^^ PLC0132 +10 | +11 | Z = NewType("T", int) + | + +type_param_name_mismatch.py:11:5: PLC0132 `NewType` name `T` does not match assigned variable name `Z` + | + 9 | Y = ParamSpec(name="T") +10 | +11 | Z = NewType("T", int) + | ^^^^^^^^^^^^^^^^^ PLC0132 +12 | Z = NewType(name="T", tp=int) + | + +type_param_name_mismatch.py:12:5: PLC0132 `NewType` name `T` does not match assigned variable name `Z` + | +11 | Z = NewType("T", int) +12 | Z = NewType(name="T", tp=int) + | ^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0132 +13 | +14 | Ws = TypeVarTuple("Ts") + | + +type_param_name_mismatch.py:14:6: PLC0132 `TypeVarTuple` name `Ts` does not match assigned variable name `Ws` + | +12 | Z = NewType(name="T", tp=int) +13 | +14 | Ws = TypeVarTuple("Ts") + | ^^^^^^^^^^^^^^^^^^ PLC0132 +15 | Ws = TypeVarTuple(name="Ts") + | + +type_param_name_mismatch.py:15:6: PLC0132 `TypeVarTuple` name `Ts` does not match assigned variable name `Ws` + | +14 | Ws = TypeVarTuple("Ts") +15 | Ws = TypeVarTuple(name="Ts") + | ^^^^^^^^^^^^^^^^^^^^^^^ PLC0132 +16 | +17 | # Non-errors. + | + + diff --git a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLC0205_single_string_slots.py.snap b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLC0205_single_string_slots.py.snap new file mode 100644 index 0000000000..b378d106c9 --- /dev/null +++ b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLC0205_single_string_slots.py.snap @@ -0,0 +1,32 @@ +--- +source: crates/ruff/src/rules/pylint/mod.rs +--- +single_string_slots.py:3:5: PLC0205 Class `__slots__` should be a non-string iterable + | +1 | # Errors. +2 | class Foo: +3 | __slots__ = "bar" + | ^^^^^^^^^^^^^^^^^ PLC0205 +4 | +5 | def __init__(self, bar): + | + +single_string_slots.py:10:5: PLC0205 Class `__slots__` should be a non-string iterable + | + 9 | class Foo: +10 | __slots__: str = "bar" + | ^^^^^^^^^^^^^^^^^^^^^^ PLC0205 +11 | +12 | def __init__(self, bar): + | + +single_string_slots.py:17:5: PLC0205 Class `__slots__` should be a non-string iterable + | +16 | class Foo: +17 | __slots__: str = f"bar" + | ^^^^^^^^^^^^^^^^^^^^^^^ PLC0205 +18 | +19 | def __init__(self, bar): + | + + diff --git a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE2510_invalid_characters.py.snap b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE2510_invalid_characters.py.snap index 5760228fe2..7cba4adf6d 100644 --- a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE2510_invalid_characters.py.snap +++ b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE2510_invalid_characters.py.snap @@ -12,7 +12,7 @@ invalid_characters.py:15:6: PLE2510 [*] Invalid unescaped character backspace, u | = help: Replace with escape sequence -ℹ Suggested fix +ℹ Fix 12 12 | # (Pylint, "C0414") => Rule::UselessImportAlias, 13 13 | # (Pylint, "C3002") => Rule::UnnecessaryDirectLambdaCall, 14 14 | #foo = 'hi' diff --git a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE2512_invalid_characters.py.snap b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE2512_invalid_characters.py.snap index 19aec0795a..dbde22f71b 100644 --- a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE2512_invalid_characters.py.snap +++ b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE2512_invalid_characters.py.snap @@ -12,7 +12,7 @@ invalid_characters.py:21:12: PLE2512 [*] Invalid unescaped character SUB, use "\ | = help: Replace with escape sequence -ℹ Suggested fix +ℹ Fix 18 18 | 19 19 | cr_ok = '\\r' 20 20 | diff --git a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE2513_invalid_characters.py.snap b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE2513_invalid_characters.py.snap index 2a080f617d..d8b61f4e13 100644 --- a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE2513_invalid_characters.py.snap +++ b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE2513_invalid_characters.py.snap @@ -12,7 +12,7 @@ invalid_characters.py:25:16: PLE2513 [*] Invalid unescaped character ESC, use "\ | = help: Replace with escape sequence -ℹ Suggested fix +ℹ Fix 22 22 | 23 23 | sub_ok = '\x1a' 24 24 | diff --git a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE2514_invalid_characters.py.snap b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE2514_invalid_characters.py.snap index 9b1f9ba80a..6adc278155 100644 Binary files a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE2514_invalid_characters.py.snap and b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE2514_invalid_characters.py.snap differ diff --git a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE2515_invalid_characters.py.snap b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE2515_invalid_characters.py.snap index f6bdbe5ec5..b12204f098 100644 --- a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE2515_invalid_characters.py.snap +++ b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE2515_invalid_characters.py.snap @@ -12,7 +12,7 @@ invalid_characters.py:34:13: PLE2515 [*] Invalid unescaped character zero-width- | = help: Replace with escape sequence -ℹ Suggested fix +ℹ Fix 31 31 | 32 32 | nul_ok = '\0' 33 33 | @@ -32,7 +32,7 @@ invalid_characters.py:38:36: PLE2515 [*] Invalid unescaped character zero-width- | = help: Replace with escape sequence -ℹ Suggested fix +ℹ Fix 35 35 | 36 36 | zwsp_ok = '\u200b' 37 37 | @@ -48,7 +48,7 @@ invalid_characters.py:39:60: PLE2515 [*] Invalid unescaped character zero-width- | = help: Replace with escape sequence -ℹ Suggested fix +ℹ Fix 36 36 | zwsp_ok = '\u200b' 37 37 | 38 38 | zwsp_after_multibyte_character = "ಫ​" @@ -63,7 +63,7 @@ invalid_characters.py:39:61: PLE2515 [*] Invalid unescaped character zero-width- | = help: Replace with escape sequence -ℹ Suggested fix +ℹ Fix 36 36 | zwsp_ok = '\u200b' 37 37 | 38 38 | zwsp_after_multibyte_character = "ಫ​" diff --git a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLR0402_import_aliasing.py.snap b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLR0402_import_aliasing.py.snap index d3b7da5cc1..2a7894af14 100644 --- a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLR0402_import_aliasing.py.snap +++ b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLR0402_import_aliasing.py.snap @@ -12,7 +12,7 @@ import_aliasing.py:9:8: PLR0402 [*] Use `from os import path` in lieu of alias | = help: Replace with `from os import path` -ℹ Suggested fix +ℹ Fix 6 6 | import collections as collections # [useless-import-alias] 7 7 | from collections import OrderedDict as OrderedDict # [useless-import-alias] 8 8 | from collections import OrderedDict as o_dict @@ -33,7 +33,7 @@ import_aliasing.py:11:8: PLR0402 [*] Use `from foo.bar import foobar` in lieu of | = help: Replace with `from foo.bar import foobar` -ℹ Suggested fix +ℹ Fix 8 8 | from collections import OrderedDict as o_dict 9 9 | import os.path as path # [consider-using-from-import] 10 10 | import os.path as p diff --git a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLR1714_repeated_equality_comparison_target.py.snap b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLR1714_repeated_equality_comparison_target.py.snap new file mode 100644 index 0000000000..9fa0ddc667 --- /dev/null +++ b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLR1714_repeated_equality_comparison_target.py.snap @@ -0,0 +1,53 @@ +--- +source: crates/ruff/src/rules/pylint/mod.rs +--- +repeated_equality_comparison_target.py:2:1: PLR1714 Consider merging multiple comparisons: `foo in ("a", "b")`. Use a `set` if the elements are hashable. + | +1 | # Errors. +2 | foo == "a" or foo == "b" + | ^^^^^^^^^^^^^^^^^^^^^^^^ PLR1714 +3 | +4 | foo != "a" and foo != "b" + | + +repeated_equality_comparison_target.py:4:1: PLR1714 Consider merging multiple comparisons: `foo not in ("a", "b")`. Use a `set` if the elements are hashable. + | +2 | foo == "a" or foo == "b" +3 | +4 | foo != "a" and foo != "b" + | ^^^^^^^^^^^^^^^^^^^^^^^^^ PLR1714 +5 | +6 | foo == "a" or foo == "b" or foo == "c" + | + +repeated_equality_comparison_target.py:6:1: PLR1714 Consider merging multiple comparisons: `foo in ("a", "b", "c")`. Use a `set` if the elements are hashable. + | +4 | foo != "a" and foo != "b" +5 | +6 | foo == "a" or foo == "b" or foo == "c" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLR1714 +7 | +8 | foo != "a" and foo != "b" and foo != "c" + | + +repeated_equality_comparison_target.py:8:1: PLR1714 Consider merging multiple comparisons: `foo not in ("a", "b", "c")`. Use a `set` if the elements are hashable. + | + 6 | foo == "a" or foo == "b" or foo == "c" + 7 | + 8 | foo != "a" and foo != "b" and foo != "c" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLR1714 + 9 | +10 | foo == a or foo == "b" or foo == 3 # Mixed types. + | + +repeated_equality_comparison_target.py:10:1: PLR1714 Consider merging multiple comparisons: `foo in (a, "b", 3)`. Use a `set` if the elements are hashable. + | + 8 | foo != "a" and foo != "b" and foo != "c" + 9 | +10 | foo == a or foo == "b" or foo == 3 # Mixed types. + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLR1714 +11 | +12 | # False negatives (the current implementation doesn't support Yoda conditions). + | + + diff --git a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLW0603_global_statement.py.snap b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLW0603_global_statement.py.snap index 5228b0e568..3d858431c5 100644 --- a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLW0603_global_statement.py.snap +++ b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLW0603_global_statement.py.snap @@ -79,4 +79,23 @@ global_statement.py:70:5: PLW0603 Using the global statement to update `CLASS` i 72 | class CLASS: | +global_statement.py:80:5: PLW0603 Using the global statement to update `CONSTANT` is discouraged + | +78 | def multiple_assignment(): +79 | """Should warn on every assignment.""" +80 | global CONSTANT # [global-statement] + | ^^^^^^^^^^^^^^^ PLW0603 +81 | CONSTANT = 1 +82 | CONSTANT = 2 + | + +global_statement.py:81:5: PLW0603 Using the global statement to update `CONSTANT` is discouraged + | +79 | """Should warn on every assignment.""" +80 | global CONSTANT # [global-statement] +81 | CONSTANT = 1 + | ^^^^^^^^^^^^ PLW0603 +82 | CONSTANT = 2 + | + diff --git a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLW2901_redefined_loop_name.py.snap b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLW2901_redefined_loop_name.py.snap index 10ef840c63..7ca951e257 100644 --- a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLW2901_redefined_loop_name.py.snap +++ b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLW2901_redefined_loop_name.py.snap @@ -37,197 +37,215 @@ redefined_loop_name.py:21:18: PLW2901 Outer `with` statement variable `i` overwr 22 | pass | -redefined_loop_name.py:37:13: PLW2901 Outer `for` loop variable `i` overwritten by inner `for` loop target +redefined_loop_name.py:36:18: PLW2901 Outer `with` statement variable `i` overwritten by inner `with` statement target | -35 | for i in []: -36 | for j in []: -37 | for i in []: # error - | ^ PLW2901 -38 | pass +34 | # Async with -> with, variable reused +35 | async with None as i: +36 | with None as i: # error + | ^ PLW2901 +37 | pass | -redefined_loop_name.py:43:13: PLW2901 Outer `for` loop variable `i` overwritten by inner `for` loop target +redefined_loop_name.py:46:9: PLW2901 Outer `for` loop variable `i` overwritten by inner `for` loop target | -41 | for i in []: -42 | for j in []: -43 | for i in []: # error - | ^ PLW2901 -44 | for j in []: # error -45 | pass +44 | # Async for -> for, variable reused +45 | async for i in []: +46 | for i in []: # error + | ^ PLW2901 +47 | pass | -redefined_loop_name.py:44:17: PLW2901 Outer `for` loop variable `j` overwritten by inner `for` loop target +redefined_loop_name.py:52:13: PLW2901 Outer `for` loop variable `i` overwritten by inner `for` loop target | -42 | for j in []: -43 | for i in []: # error -44 | for j in []: # error +50 | for i in []: +51 | for j in []: +52 | for i in []: # error + | ^ PLW2901 +53 | pass + | + +redefined_loop_name.py:58:13: PLW2901 Outer `for` loop variable `i` overwritten by inner `for` loop target + | +56 | for i in []: +57 | for j in []: +58 | for i in []: # error + | ^ PLW2901 +59 | for j in []: # error +60 | pass + | + +redefined_loop_name.py:59:17: PLW2901 Outer `for` loop variable `j` overwritten by inner `for` loop target + | +57 | for j in []: +58 | for i in []: # error +59 | for j in []: # error | ^ PLW2901 -45 | pass +60 | pass | -redefined_loop_name.py:52:5: PLW2901 `for` loop variable `i` overwritten by assignment target +redefined_loop_name.py:67:5: PLW2901 `for` loop variable `i` overwritten by assignment target | -50 | i = cast(int, i) -51 | i = typing.cast(int, i) -52 | i = 5 # error +65 | i = cast(int, i) +66 | i = typing.cast(int, i) +67 | i = 5 # error | ^ PLW2901 -53 | -54 | # For -> augmented assignment +68 | +69 | # For -> augmented assignment | -redefined_loop_name.py:56:5: PLW2901 `for` loop variable `i` overwritten by assignment target +redefined_loop_name.py:71:5: PLW2901 `for` loop variable `i` overwritten by assignment target | -54 | # For -> augmented assignment -55 | for i in []: -56 | i += 5 # error +69 | # For -> augmented assignment +70 | for i in []: +71 | i += 5 # error | ^ PLW2901 -57 | -58 | # For -> annotated assignment +72 | +73 | # For -> annotated assignment | -redefined_loop_name.py:60:5: PLW2901 `for` loop variable `i` overwritten by assignment target +redefined_loop_name.py:75:5: PLW2901 `for` loop variable `i` overwritten by assignment target | -58 | # For -> annotated assignment -59 | for i in []: -60 | i: int = 5 # error +73 | # For -> annotated assignment +74 | for i in []: +75 | i: int = 5 # error | ^ PLW2901 -61 | -62 | # For -> annotated assignment without value - | - -redefined_loop_name.py:68:9: PLW2901 Outer `for` loop variable `i` overwritten by inner `for` loop target - | -66 | # Async for -> for, variable reused -67 | async for i in []: -68 | for i in []: # error - | ^ PLW2901 -69 | pass - | - -redefined_loop_name.py:73:15: PLW2901 Outer `for` loop variable `i` overwritten by inner `for` loop target - | -71 | # For -> async for, variable reused -72 | for i in []: -73 | async for i in []: # error - | ^ PLW2901 -74 | pass - | - -redefined_loop_name.py:78:9: PLW2901 Outer `for` loop variable `i` overwritten by inner `for` loop target - | -76 | # For -> for, outer loop unpacks tuple -77 | for i, j in enumerate([]): -78 | for i in []: # error - | ^ PLW2901 -79 | pass +76 | +77 | # For -> annotated assignment without value | redefined_loop_name.py:83:9: PLW2901 Outer `for` loop variable `i` overwritten by inner `for` loop target | -81 | # For -> for, inner loop unpacks tuple -82 | for i in []: -83 | for i, j in enumerate([]): # error +81 | # Async for -> for, variable reused +82 | async for i in []: +83 | for i in []: # error | ^ PLW2901 84 | pass | -redefined_loop_name.py:88:9: PLW2901 Outer `for` loop variable `i` overwritten by inner `for` loop target +redefined_loop_name.py:88:15: PLW2901 Outer `for` loop variable `i` overwritten by inner `for` loop target | -86 | # For -> for, both loops unpack tuple -87 | for (i, (j, k)) in []: -88 | for i, j in enumerate([]): # two errors +86 | # For -> async for, variable reused +87 | for i in []: +88 | async for i in []: # error + | ^ PLW2901 +89 | pass + | + +redefined_loop_name.py:93:9: PLW2901 Outer `for` loop variable `i` overwritten by inner `for` loop target + | +91 | # For -> for, outer loop unpacks tuple +92 | for i, j in enumerate([]): +93 | for i in []: # error | ^ PLW2901 -89 | pass +94 | pass | -redefined_loop_name.py:88:12: PLW2901 Outer `for` loop variable `j` overwritten by inner `for` loop target +redefined_loop_name.py:98:9: PLW2901 Outer `for` loop variable `i` overwritten by inner `for` loop target | -86 | # For -> for, both loops unpack tuple -87 | for (i, (j, k)) in []: -88 | for i, j in enumerate([]): # two errors - | ^ PLW2901 -89 | pass +96 | # For -> for, inner loop unpacks tuple +97 | for i in []: +98 | for i, j in enumerate([]): # error + | ^ PLW2901 +99 | pass | -redefined_loop_name.py:105:9: PLW2901 Outer `for` loop variable `j` overwritten by inner `for` loop target +redefined_loop_name.py:103:9: PLW2901 Outer `for` loop variable `i` overwritten by inner `for` loop target | -103 | # For -> for, outer loop unpacks with asterisk -104 | for i, *j in []: -105 | for j in []: # error +101 | # For -> for, both loops unpack tuple +102 | for (i, (j, k)) in []: +103 | for i, j in enumerate([]): # two errors | ^ PLW2901 -106 | pass +104 | pass | -redefined_loop_name.py:122:13: PLW2901 `for` loop variable `i` overwritten by assignment target +redefined_loop_name.py:103:12: PLW2901 Outer `for` loop variable `j` overwritten by inner `for` loop target | -120 | def f(): -121 | for i in []: # no error -122 | i = 2 # error +101 | # For -> for, both loops unpack tuple +102 | for (i, (j, k)) in []: +103 | for i, j in enumerate([]): # two errors + | ^ PLW2901 +104 | pass + | + +redefined_loop_name.py:120:9: PLW2901 Outer `for` loop variable `j` overwritten by inner `for` loop target + | +118 | # For -> for, outer loop unpacks with asterisk +119 | for i, *j in []: +120 | for j in []: # error + | ^ PLW2901 +121 | pass + | + +redefined_loop_name.py:137:13: PLW2901 `for` loop variable `i` overwritten by assignment target + | +135 | def f(): +136 | for i in []: # no error +137 | i = 2 # error | ^ PLW2901 -123 | -124 | # For -> class definition -> for -> for +138 | +139 | # For -> class definition -> for -> for | -redefined_loop_name.py:128:17: PLW2901 Outer `for` loop variable `i` overwritten by inner `for` loop target +redefined_loop_name.py:143:17: PLW2901 Outer `for` loop variable `i` overwritten by inner `for` loop target | -126 | class A: -127 | for i in []: # no error -128 | for i in []: # error +141 | class A: +142 | for i in []: # no error +143 | for i in []: # error | ^ PLW2901 -129 | pass +144 | pass | -redefined_loop_name.py:143:5: PLW2901 `for` loop variable `a[0]` overwritten by assignment target +redefined_loop_name.py:158:5: PLW2901 `for` loop variable `a[0]` overwritten by assignment target | -141 | # For target with subscript -> assignment -142 | for a[0] in []: -143 | a[0] = 2 # error +156 | # For target with subscript -> assignment +157 | for a[0] in []: +158 | a[0] = 2 # error | ^^^^ PLW2901 -144 | a[1] = 2 # no error +159 | a[1] = 2 # no error | -redefined_loop_name.py:148:5: PLW2901 `for` loop variable `a['i']` overwritten by assignment target +redefined_loop_name.py:163:5: PLW2901 `for` loop variable `a['i']` overwritten by assignment target | -146 | # For target with subscript -> assignment -147 | for a['i'] in []: -148 | a['i'] = 2 # error +161 | # For target with subscript -> assignment +162 | for a['i'] in []: +163 | a['i'] = 2 # error | ^^^^^^ PLW2901 -149 | a['j'] = 2 # no error +164 | a['j'] = 2 # no error | -redefined_loop_name.py:153:5: PLW2901 `for` loop variable `a.i` overwritten by assignment target +redefined_loop_name.py:168:5: PLW2901 `for` loop variable `a.i` overwritten by assignment target | -151 | # For target with attribute -> assignment -152 | for a.i in []: -153 | a.i = 2 # error +166 | # For target with attribute -> assignment +167 | for a.i in []: +168 | a.i = 2 # error | ^^^ PLW2901 -154 | a.j = 2 # no error +169 | a.j = 2 # no error | -redefined_loop_name.py:158:5: PLW2901 `for` loop variable `a.i.j` overwritten by assignment target +redefined_loop_name.py:173:5: PLW2901 `for` loop variable `a.i.j` overwritten by assignment target | -156 | # For target with double nested attribute -> assignment -157 | for a.i.j in []: -158 | a.i.j = 2 # error +171 | # For target with double nested attribute -> assignment +172 | for a.i.j in []: +173 | a.i.j = 2 # error | ^^^^^ PLW2901 -159 | a.j.i = 2 # no error +174 | a.j.i = 2 # no error | -redefined_loop_name.py:163:5: PLW2901 `for` loop variable `a.i` overwritten by assignment target +redefined_loop_name.py:178:5: PLW2901 `for` loop variable `a.i` overwritten by assignment target | -161 | # For target with attribute -> assignment with different spacing -162 | for a.i in []: -163 | a. i = 2 # error +176 | # For target with attribute -> assignment with different spacing +177 | for a.i in []: +178 | a. i = 2 # error | ^^^^ PLW2901 -164 | for a. i in []: -165 | a.i = 2 # error +179 | for a. i in []: +180 | a.i = 2 # error | -redefined_loop_name.py:165:5: PLW2901 `for` loop variable `a.i` overwritten by assignment target +redefined_loop_name.py:180:5: PLW2901 `for` loop variable `a.i` overwritten by assignment target | -163 | a. i = 2 # error -164 | for a. i in []: -165 | a.i = 2 # error +178 | a. i = 2 # error +179 | for a. i in []: +180 | a.i = 2 # error | ^^^ PLW2901 | diff --git a/crates/ruff/src/rules/pyupgrade/fixes.rs b/crates/ruff/src/rules/pyupgrade/fixes.rs index 5f37034a2d..b20e351635 100644 --- a/crates/ruff/src/rules/pyupgrade/fixes.rs +++ b/crates/ruff/src/rules/pyupgrade/fixes.rs @@ -126,7 +126,7 @@ pub(crate) fn remove_import_members(contents: &str, members: &[&str]) -> String } #[cfg(test)] -mod test { +mod tests { use crate::rules::pyupgrade::fixes::remove_import_members; #[test] diff --git a/crates/ruff/src/rules/pyupgrade/mod.rs b/crates/ruff/src/rules/pyupgrade/mod.rs index 331194a03d..f65042ebc9 100644 --- a/crates/ruff/src/rules/pyupgrade/mod.rs +++ b/crates/ruff/src/rules/pyupgrade/mod.rs @@ -2,6 +2,7 @@ mod fixes; mod helpers; pub(crate) mod rules; +pub mod settings; pub(crate) mod types; #[cfg(test)] @@ -12,68 +13,70 @@ mod tests { use test_case::test_case; use crate::registry::Rule; + use crate::rules::pyupgrade; use crate::settings::types::PythonVersion; use crate::test::test_path; use crate::{assert_messages, settings}; - #[test_case(Rule::UselessMetaclassType, Path::new("UP001.py"))] - #[test_case(Rule::TypeOfPrimitive, Path::new("UP003.py"))] - #[test_case(Rule::UselessObjectInheritance, Path::new("UP004.py"))] + #[test_case(Rule::ConvertNamedTupleFunctionalToClass, Path::new("UP014.py"))] + #[test_case(Rule::ConvertTypedDictFunctionalToClass, Path::new("UP013.py"))] + #[test_case(Rule::DeprecatedCElementTree, Path::new("UP023.py"))] + #[test_case(Rule::DeprecatedImport, Path::new("UP035.py"))] + #[test_case(Rule::DeprecatedMockImport, Path::new("UP026.py"))] #[test_case(Rule::DeprecatedUnittestAlias, Path::new("UP005.py"))] + #[test_case(Rule::ExtraneousParentheses, Path::new("UP034.py"))] + #[test_case(Rule::FString, Path::new("UP032_0.py"))] + #[test_case(Rule::FString, Path::new("UP032_1.py"))] + #[test_case(Rule::FString, Path::new("UP032_2.py"))] + #[test_case(Rule::FormatLiterals, Path::new("UP030_0.py"))] + #[test_case(Rule::FormatLiterals, Path::new("UP030_1.py"))] + #[test_case(Rule::FormatLiterals, Path::new("UP030_2.py"))] + #[test_case(Rule::LRUCacheWithMaxsizeNone, Path::new("UP033_0.py"))] + #[test_case(Rule::LRUCacheWithMaxsizeNone, Path::new("UP033_1.py"))] + #[test_case(Rule::LRUCacheWithoutParameters, Path::new("UP011.py"))] + #[test_case(Rule::NativeLiterals, Path::new("UP018.py"))] #[test_case(Rule::NonPEP585Annotation, Path::new("UP006_0.py"))] #[test_case(Rule::NonPEP585Annotation, Path::new("UP006_1.py"))] #[test_case(Rule::NonPEP585Annotation, Path::new("UP006_2.py"))] #[test_case(Rule::NonPEP585Annotation, Path::new("UP006_3.py"))] #[test_case(Rule::NonPEP604Annotation, Path::new("UP007.py"))] - #[test_case(Rule::SuperCallWithParameters, Path::new("UP008.py"))] - #[test_case(Rule::UTF8EncodingDeclaration, Path::new("UP009_0.py"))] - #[test_case(Rule::UTF8EncodingDeclaration, Path::new("UP009_1.py"))] - #[test_case(Rule::UTF8EncodingDeclaration, Path::new("UP009_2.py"))] - #[test_case(Rule::UTF8EncodingDeclaration, Path::new("UP009_3.py"))] - #[test_case(Rule::UTF8EncodingDeclaration, Path::new("UP009_4.py"))] - #[test_case(Rule::UnnecessaryFutureImport, Path::new("UP010.py"))] - #[test_case(Rule::LRUCacheWithoutParameters, Path::new("UP011.py"))] - #[test_case(Rule::UnnecessaryEncodeUTF8, Path::new("UP012.py"))] - #[test_case(Rule::ConvertTypedDictFunctionalToClass, Path::new("UP013.py"))] - #[test_case(Rule::ConvertNamedTupleFunctionalToClass, Path::new("UP014.py"))] - #[test_case(Rule::RedundantOpenModes, Path::new("UP015.py"))] - #[test_case(Rule::NativeLiterals, Path::new("UP018.py"))] - #[test_case(Rule::TypingTextStrAlias, Path::new("UP019.py"))] - #[test_case(Rule::OpenAlias, Path::new("UP020.py"))] - #[test_case(Rule::ReplaceUniversalNewlines, Path::new("UP021.py"))] - #[test_case(Rule::ReplaceStdoutStderr, Path::new("UP022.py"))] - #[test_case(Rule::DeprecatedCElementTree, Path::new("UP023.py"))] + #[test_case(Rule::NonPEP604Isinstance, Path::new("UP038.py"))] #[test_case(Rule::OSErrorAlias, Path::new("UP024_0.py"))] #[test_case(Rule::OSErrorAlias, Path::new("UP024_1.py"))] #[test_case(Rule::OSErrorAlias, Path::new("UP024_2.py"))] #[test_case(Rule::OSErrorAlias, Path::new("UP024_3.py"))] #[test_case(Rule::OSErrorAlias, Path::new("UP024_4.py"))] - #[test_case(Rule::UnicodeKindPrefix, Path::new("UP025.py"))] - #[test_case(Rule::DeprecatedMockImport, Path::new("UP026.py"))] - #[test_case(Rule::UnpackedListComprehension, Path::new("UP027.py"))] - #[test_case(Rule::YieldInForLoop, Path::new("UP028_0.py"))] - #[test_case(Rule::YieldInForLoop, Path::new("UP028_1.py"))] - #[test_case(Rule::UnnecessaryBuiltinImport, Path::new("UP029.py"))] - #[test_case(Rule::FormatLiterals, Path::new("UP030_0.py"))] - #[test_case(Rule::FormatLiterals, Path::new("UP030_1.py"))] - #[test_case(Rule::FormatLiterals, Path::new("UP030_2.py"))] - #[test_case(Rule::PrintfStringFormatting, Path::new("UP031_0.py"))] - #[test_case(Rule::PrintfStringFormatting, Path::new("UP031_1.py"))] - #[test_case(Rule::FString, Path::new("UP032_0.py"))] - #[test_case(Rule::FString, Path::new("UP032_1.py"))] - #[test_case(Rule::FString, Path::new("UP032_2.py"))] - #[test_case(Rule::LRUCacheWithMaxsizeNone, Path::new("UP033_0.py"))] - #[test_case(Rule::LRUCacheWithMaxsizeNone, Path::new("UP033_1.py"))] - #[test_case(Rule::ExtraneousParentheses, Path::new("UP034.py"))] - #[test_case(Rule::DeprecatedImport, Path::new("UP035.py"))] + #[test_case(Rule::OpenAlias, Path::new("UP020.py"))] #[test_case(Rule::OutdatedVersionBlock, Path::new("UP036_0.py"))] #[test_case(Rule::OutdatedVersionBlock, Path::new("UP036_1.py"))] #[test_case(Rule::OutdatedVersionBlock, Path::new("UP036_2.py"))] #[test_case(Rule::OutdatedVersionBlock, Path::new("UP036_3.py"))] #[test_case(Rule::OutdatedVersionBlock, Path::new("UP036_4.py"))] #[test_case(Rule::OutdatedVersionBlock, Path::new("UP036_5.py"))] + #[test_case(Rule::PrintfStringFormatting, Path::new("UP031_0.py"))] + #[test_case(Rule::PrintfStringFormatting, Path::new("UP031_1.py"))] #[test_case(Rule::QuotedAnnotation, Path::new("UP037.py"))] - #[test_case(Rule::NonPEP604Isinstance, Path::new("UP038.py"))] + #[test_case(Rule::RedundantOpenModes, Path::new("UP015.py"))] + #[test_case(Rule::ReplaceStdoutStderr, Path::new("UP022.py"))] + #[test_case(Rule::ReplaceUniversalNewlines, Path::new("UP021.py"))] + #[test_case(Rule::SuperCallWithParameters, Path::new("UP008.py"))] + #[test_case(Rule::TypeOfPrimitive, Path::new("UP003.py"))] + #[test_case(Rule::TypingTextStrAlias, Path::new("UP019.py"))] + #[test_case(Rule::UTF8EncodingDeclaration, Path::new("UP009_0.py"))] + #[test_case(Rule::UTF8EncodingDeclaration, Path::new("UP009_1.py"))] + #[test_case(Rule::UTF8EncodingDeclaration, Path::new("UP009_2.py"))] + #[test_case(Rule::UTF8EncodingDeclaration, Path::new("UP009_3.py"))] + #[test_case(Rule::UTF8EncodingDeclaration, Path::new("UP009_4.py"))] + #[test_case(Rule::UnicodeKindPrefix, Path::new("UP025.py"))] + #[test_case(Rule::UnnecessaryBuiltinImport, Path::new("UP029.py"))] + #[test_case(Rule::UnnecessaryClassParentheses, Path::new("UP039.py"))] + #[test_case(Rule::UnnecessaryEncodeUTF8, Path::new("UP012.py"))] + #[test_case(Rule::UnnecessaryFutureImport, Path::new("UP010.py"))] + #[test_case(Rule::UnpackedListComprehension, Path::new("UP027.py"))] + #[test_case(Rule::UselessMetaclassType, Path::new("UP001.py"))] + #[test_case(Rule::UselessObjectInheritance, Path::new("UP004.py"))] + #[test_case(Rule::YieldInForLoop, Path::new("UP028_0.py"))] + #[test_case(Rule::YieldInForLoop, Path::new("UP028_1.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = path.to_string_lossy().to_string(); let diagnostics = test_path( @@ -84,6 +87,38 @@ mod tests { Ok(()) } + #[test] + fn future_annotations_keep_runtime_typing_p37() -> Result<()> { + let diagnostics = test_path( + Path::new("pyupgrade/future_annotations.py"), + &settings::Settings { + pyupgrade: pyupgrade::settings::Settings { + keep_runtime_typing: true, + }, + target_version: PythonVersion::Py37, + ..settings::Settings::for_rule(Rule::NonPEP585Annotation) + }, + )?; + assert_messages!(diagnostics); + Ok(()) + } + + #[test] + fn future_annotations_keep_runtime_typing_p310() -> Result<()> { + let diagnostics = test_path( + Path::new("pyupgrade/future_annotations.py"), + &settings::Settings { + pyupgrade: pyupgrade::settings::Settings { + keep_runtime_typing: true, + }, + target_version: PythonVersion::Py310, + ..settings::Settings::for_rule(Rule::NonPEP585Annotation) + }, + )?; + assert_messages!(diagnostics); + Ok(()) + } + #[test] fn future_annotations_pep_585_p37() -> Result<()> { let diagnostics = test_path( diff --git a/crates/ruff/src/rules/pyupgrade/rules/convert_named_tuple_functional_to_class.rs b/crates/ruff/src/rules/pyupgrade/rules/convert_named_tuple_functional_to_class.rs index d2de8fb2b8..63298260cf 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/convert_named_tuple_functional_to_class.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/convert_named_tuple_functional_to_class.rs @@ -1,17 +1,50 @@ use anyhow::{bail, Result}; use log::debug; use ruff_text_size::TextRange; -use rustpython_parser::ast::{self, Constant, Expr, ExprContext, Keyword, Ranged, Stmt}; +use rustpython_parser::ast::{ + self, Constant, Expr, ExprContext, Identifier, Keyword, Ranged, Stmt, +}; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers::is_dunder; use ruff_python_ast::source_code::Generator; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; use ruff_python_stdlib::identifiers::is_identifier; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Checks for `NamedTuple` declarations that use functional syntax. +/// +/// ## Why is this bad? +/// `NamedTuple` subclasses can be defined either through a functional syntax +/// (`Foo = NamedTuple(...)`) or a class syntax (`class Foo(NamedTuple): ...`). +/// +/// The class syntax is more readable and generally preferred over the +/// functional syntax, which exists primarily for backwards compatibility +/// with `collections.namedtuple`. +/// +/// ## Example +/// ```python +/// from typing import NamedTuple +/// +/// Foo = NamedTuple("Foo", [("a", int), ("b", str)]) +/// ``` +/// +/// Use instead: +/// ```python +/// from typing import NamedTuple +/// +/// +/// class Foo(NamedTuple): +/// a: int +/// b: str +/// ``` +/// +/// ## References +/// - [Python documentation: `typing.NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple) #[violation] pub struct ConvertNamedTupleFunctionalToClass { name: String, @@ -35,9 +68,9 @@ impl Violation for ConvertNamedTupleFunctionalToClass { /// Return the typename, args, keywords, and base class. fn match_named_tuple_assign<'a>( - model: &SemanticModel, targets: &'a [Expr], value: &'a Expr, + semantic: &SemanticModel, ) -> Option<(&'a str, &'a [Expr], &'a [Keyword], &'a Expr)> { let target = targets.get(0)?; let Expr::Name(ast::ExprName { id: typename, .. }) = target else { @@ -48,12 +81,11 @@ fn match_named_tuple_assign<'a>( args, keywords, range: _, - }) = value else { + }) = value + else { return None; }; - if !model.resolve_call_path(func).map_or(false, |call_path| { - call_path.as_slice() == ["typing", "NamedTuple"] - }) { + if !semantic.match_typing_expr(func, "NamedTuple") { return None; } Some((typename, args, keywords, func)) @@ -66,19 +98,21 @@ fn create_property_assignment_stmt( annotation: &Expr, value: Option<&Expr>, ) -> Stmt { - let node = ast::ExprName { - id: property.into(), - ctx: ExprContext::Load, - range: TextRange::default(), - }; - let node1 = ast::StmtAnnAssign { - target: Box::new(node.into()), + ast::StmtAnnAssign { + target: Box::new( + ast::ExprName { + id: property.into(), + ctx: ExprContext::Load, + range: TextRange::default(), + } + .into(), + ), annotation: Box::new(annotation.clone()), value: value.map(|value| Box::new(value.clone())), simple: true, range: TextRange::default(), - }; - node1.into() + } + .into() } /// Match the `defaults` keyword in a `NamedTuple(...)` call. @@ -103,10 +137,12 @@ fn match_defaults(keywords: &[Keyword]) -> Result<&[Expr]> { /// Create a list of property assignments from the `NamedTuple` arguments. fn create_properties_from_args(args: &[Expr], defaults: &[Expr]) -> Result> { let Some(fields) = args.get(1) else { - let node = Stmt::Pass(ast::StmtPass { range: TextRange::default()}); + let node = Stmt::Pass(ast::StmtPass { + range: TextRange::default(), + }); return Ok(vec![node]); }; - let Expr::List(ast::ExprList { elts, .. } )= &fields else { + let Expr::List(ast::ExprList { elts, .. }) = &fields else { bail!("Expected argument to be `Expr::List`"); }; if elts.is_empty() { @@ -134,12 +170,16 @@ fn create_properties_from_args(args: &[Expr], defaults: &[Expr]) -> Result Result, base_class: &Expr) -> Stmt { - let node = ast::StmtClassDef { - name: typename.into(), + ast::StmtClassDef { + name: Identifier::new(typename.to_string(), TextRange::default()), bases: vec![base_class.clone()], keywords: vec![], body, decorator_list: vec![], range: TextRange::default(), - }; - node.into() + } + .into() } /// Generate a `Fix` to convert a `NamedTuple` assignment to a class definition. @@ -169,8 +209,7 @@ fn convert_to_class( base_class: &Expr, generator: Generator, ) -> Fix { - #[allow(deprecated)] - Fix::unspecified(Edit::range_replacement( + Fix::suggested(Edit::range_replacement( generator.stmt(&create_class_def_stmt(typename, body, base_class)), stmt.range(), )) @@ -184,8 +223,8 @@ pub(crate) fn convert_named_tuple_functional_to_class( value: &Expr, ) { let Some((typename, args, keywords, base_class)) = - match_named_tuple_assign(checker.semantic_model(), targets, value) else - { + match_named_tuple_assign(targets, value, checker.semantic()) + else { return; }; diff --git a/crates/ruff/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs b/crates/ruff/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs index e1906fa880..afce256896 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs @@ -1,17 +1,49 @@ use anyhow::{bail, Result}; use log::debug; use ruff_text_size::TextRange; -use rustpython_parser::ast::{self, Constant, Expr, ExprContext, Keyword, Ranged, Stmt}; +use rustpython_parser::ast::{ + self, Constant, Expr, ExprContext, Identifier, Keyword, Ranged, Stmt, +}; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers::is_dunder; use ruff_python_ast::source_code::Generator; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; use ruff_python_stdlib::identifiers::is_identifier; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Checks for `TypedDict` declarations that use functional syntax. +/// +/// ## Why is this bad? +/// `TypedDict` subclasses can be defined either through a functional syntax +/// (`Foo = TypedDict(...)`) or a class syntax (`class Foo(TypedDict): ...`). +/// +/// The class syntax is more readable and generally preferred over the +/// functional syntax. +/// +/// ## Example +/// ```python +/// from typing import TypedDict +/// +/// Foo = TypedDict("Foo", {"a": int, "b": str}) +/// ``` +/// +/// Use instead: +/// ```python +/// from typing import TypedDict +/// +/// +/// class Foo(TypedDict): +/// a: int +/// b: str +/// ``` +/// +/// ## References +/// - [Python documentation: `typing.TypedDict`](https://docs.python.org/3/library/typing.html#typing.TypedDict) #[violation] pub struct ConvertTypedDictFunctionalToClass { name: String, @@ -35,9 +67,9 @@ impl Violation for ConvertTypedDictFunctionalToClass { /// Return the class name, arguments, keywords and base class for a `TypedDict` /// assignment. fn match_typed_dict_assign<'a>( - model: &SemanticModel, targets: &'a [Expr], value: &'a Expr, + semantic: &SemanticModel, ) -> Option<(&'a str, &'a [Expr], &'a [Keyword], &'a Expr)> { let target = targets.get(0)?; let Expr::Name(ast::ExprName { id: class_name, .. }) = target else { @@ -47,13 +79,12 @@ fn match_typed_dict_assign<'a>( func, args, keywords, - range: _ - }) = value else { + range: _, + }) = value + else { return None; }; - if !model.resolve_call_path(func).map_or(false, |call_path| { - call_path.as_slice() == ["typing", "TypedDict"] - }) { + if !semantic.match_typing_expr(func, "TypedDict") { return None; } Some((class_name, args, keywords, func)) @@ -62,20 +93,21 @@ fn match_typed_dict_assign<'a>( /// Generate a `Stmt::AnnAssign` representing the provided property /// definition. fn create_property_assignment_stmt(property: &str, annotation: &Expr) -> Stmt { - let node = annotation.clone(); - let node1 = ast::ExprName { - id: property.into(), - ctx: ExprContext::Load, - range: TextRange::default(), - }; - let node2 = ast::StmtAnnAssign { - target: Box::new(node1.into()), - annotation: Box::new(node), + ast::StmtAnnAssign { + target: Box::new( + ast::ExprName { + id: property.into(), + ctx: ExprContext::Load, + range: TextRange::default(), + } + .into(), + ), + annotation: Box::new(annotation.clone()), value: None, simple: true, range: TextRange::default(), - }; - node2.into() + } + .into() } /// Generate a `StmtKind:ClassDef` statement based on the provided body, @@ -90,15 +122,15 @@ fn create_class_def_stmt( Some(keyword) => vec![keyword.clone()], None => vec![], }; - let node = ast::StmtClassDef { - name: class_name.into(), + ast::StmtClassDef { + name: Identifier::new(class_name.to_string(), TextRange::default()), bases: vec![base_class.clone()], keywords, body, decorator_list: vec![], range: TextRange::default(), - }; - node.into() + } + .into() } fn properties_from_dict_literal(keys: &[Option], values: &[Expr]) -> Result> { @@ -116,11 +148,13 @@ fn properties_from_dict_literal(keys: &[Option], values: &[Expr]) -> Resul value: Constant::Str(property), .. })) => { - if is_identifier(property) { - Ok(create_property_assignment_stmt(property, value)) - } else { - bail!("Property name is not valid identifier: {}", property) + if !is_identifier(property) { + bail!("Invalid property name: {}", property) } + if is_dunder(property) { + bail!("Cannot use dunder property name: {}", property) + } + Ok(create_property_assignment_stmt(property, value)) } _ => bail!("Expected `key` to be `Constant::Str`"), }) @@ -170,12 +204,12 @@ fn properties_from_keywords(keywords: &[Keyword]) -> Result> { // TypedDict('name', {'a': int}, total=True) // ``` fn match_total_from_only_keyword(keywords: &[Keyword]) -> Option<&Keyword> { - let keyword = keywords.get(0)?; - let arg = &keyword.arg.as_ref()?; - match arg.as_str() { - "total" => Some(keyword), - _ => None, - } + keywords.iter().find(|keyword| { + let Some(arg) = &keyword.arg else { + return false; + }; + arg.as_str() == "total" + }) } fn match_properties_and_total<'a>( @@ -219,8 +253,7 @@ fn convert_to_class( base_class: &Expr, generator: Generator, ) -> Fix { - #[allow(deprecated)] - Fix::unspecified(Edit::range_replacement( + Fix::suggested(Edit::range_replacement( generator.stmt(&create_class_def_stmt( class_name, body, @@ -239,8 +272,8 @@ pub(crate) fn convert_typed_dict_functional_to_class( value: &Expr, ) { let Some((class_name, args, keywords, base_class)) = - match_typed_dict_assign(checker.semantic_model(), targets, value) else - { + match_typed_dict_assign(targets, value, checker.semantic()) + else { return; }; diff --git a/crates/ruff/src/rules/pyupgrade/rules/datetime_utc_alias.rs b/crates/ruff/src/rules/pyupgrade/rules/datetime_utc_alias.rs index 16aa05a145..3f0e04cc29 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/datetime_utc_alias.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/datetime_utc_alias.rs @@ -2,15 +2,39 @@ use rustpython_parser::ast::{Expr, Ranged}; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::call_path::collect_call_path; use crate::checkers::ast::Checker; +use crate::importer::ImportRequest; use crate::registry::AsRule; +/// ## What it does +/// Checks for uses of `datetime.timezone.utc`. +/// +/// ## Why is this bad? +/// As of Python 3.11, `datetime.UTC` is an alias for `datetime.timezone.utc`. +/// The alias is more readable and generally preferred over the full path. +/// +/// ## Example +/// ```python +/// import datetime +/// +/// datetime.timezone.utc +/// ``` +/// +/// Use instead: +/// ```python +/// import datetime +/// +/// datetime.UTC +/// ``` +/// +/// ## Options +/// - `target-version` +/// +/// ## References +/// - [Python documentation: `datetime.UTC`](https://docs.python.org/3/library/datetime.html#datetime.UTC) #[violation] -pub struct DatetimeTimezoneUTC { - straight_import: bool, -} +pub struct DatetimeTimezoneUTC; impl Violation for DatetimeTimezoneUTC { const AUTOFIX: AutofixKind = AutofixKind::Sometimes; @@ -28,24 +52,23 @@ impl Violation for DatetimeTimezoneUTC { /// UP017 pub(crate) fn datetime_utc_alias(checker: &mut Checker, expr: &Expr) { if checker - .semantic_model() + .semantic() .resolve_call_path(expr) .map_or(false, |call_path| { - call_path.as_slice() == ["datetime", "timezone", "utc"] + matches!(call_path.as_slice(), ["datetime", "timezone", "utc"]) }) { - let straight_import = collect_call_path(expr).map_or(false, |call_path| { - call_path.as_slice() == ["datetime", "timezone", "utc"] - }); - let mut diagnostic = Diagnostic::new(DatetimeTimezoneUTC { straight_import }, expr.range()); + let mut diagnostic = Diagnostic::new(DatetimeTimezoneUTC, expr.range()); if checker.patch(diagnostic.kind.rule()) { - if straight_import { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( - "datetime.UTC".to_string(), - expr.range(), - ))); - } + diagnostic.try_set_fix(|| { + let (import_edit, binding) = checker.importer.get_or_import_symbol( + &ImportRequest::import_from("datetime", "UTC"), + expr.start(), + checker.semantic(), + )?; + let reference_edit = Edit::range_replacement(binding, expr.range()); + Ok(Fix::suggested_edits(import_edit, [reference_edit])) + }); } checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/pyupgrade/rules/deprecated_c_element_tree.rs b/crates/ruff/src/rules/pyupgrade/rules/deprecated_c_element_tree.rs index dddc5b328f..fed0bda549 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/deprecated_c_element_tree.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/deprecated_c_element_tree.rs @@ -6,6 +6,25 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Checks for uses of the `xml.etree.cElementTree` module. +/// +/// ## Why is this bad? +/// In Python 3.3, `xml.etree.cElementTree` was deprecated in favor of +/// `xml.etree.ElementTree`. +/// +/// ## Example +/// ```python +/// from xml.etree import cElementTree +/// ``` +/// +/// Use instead: +/// ```python +/// from xml.etree import ElementTree +/// ``` +/// +/// ## References +/// - [Python documentation: `xml.etree.ElementTree`](https://docs.python.org/3/library/xml.etree.elementtree.html) #[violation] pub struct DeprecatedCElementTree; @@ -27,8 +46,7 @@ where let mut diagnostic = Diagnostic::new(DeprecatedCElementTree, node.range()); if checker.patch(diagnostic.kind.rule()) { let contents = checker.locator.slice(node.range()); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( contents.replacen("cElementTree", "ElementTree", 1), node.range(), ))); diff --git a/crates/ruff/src/rules/pyupgrade/rules/deprecated_import.rs b/crates/ruff/src/rules/pyupgrade/rules/deprecated_import.rs index b2c5c62394..4dac80b3e4 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/deprecated_import.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/deprecated_import.rs @@ -35,6 +35,29 @@ enum Deprecation { WithoutRename(WithoutRename), } +/// ## What it does +/// Checks for uses of deprecated imports based on the minimum supported +/// Python version. +/// +/// ## Why is this bad? +/// Deprecated imports may be removed in future versions of Python, and +/// should be replaced with their new equivalents. +/// +/// Note that, in some cases, it may be preferable to continue importing +/// members from `typing_extensions` even after they're added to the Python +/// standard library, as `typing_extensions` can backport bugfixes and +/// optimizations from later Python versions. This rule thus avoids flagging +/// imports from `typing_extensions` in such cases. +/// +/// ## Example +/// ```python +/// from collections import Sequence +/// ``` +/// +/// Use instead: +/// ```python +/// from collections.abc import Sequence +/// ``` #[violation] pub struct DeprecatedImport { deprecation: Deprecation, @@ -71,15 +94,13 @@ impl Violation for DeprecatedImport { } } -// A list of modules that may involve import rewrites. -const RELEVANT_MODULES: &[&str] = &[ - "collections", - "pipes", - "mypy_extensions", - "typing_extensions", - "typing", - "typing.re", -]; +/// Returns `true` if the module may contain deprecated imports. +fn is_relevant_module(module: &str) -> bool { + matches!( + module, + "collections" | "pipes" | "mypy_extensions" | "typing_extensions" | "typing" | "typing.re" + ) +} // Members of `collections` that were moved to `collections.abc`. const COLLECTIONS_TO_ABC: &[&str] = &[ @@ -122,10 +143,12 @@ const TYPING_EXTENSIONS_TO_TYPING: &[&str] = &[ "ContextManager", "Coroutine", "DefaultDict", - "NewType", "TYPE_CHECKING", "Text", "Type", + // Introduced in Python 3.5.2, but `typing_extensions` contains backported bugfixes and + // optimizations, + // "NewType", ]; // Python 3.7+ @@ -151,11 +174,13 @@ const MYPY_EXTENSIONS_TO_TYPING_38: &[&str] = &["TypedDict"]; // Members of `typing_extensions` that were moved to `typing`. const TYPING_EXTENSIONS_TO_TYPING_38: &[&str] = &[ "Final", - "Literal", "OrderedDict", - "Protocol", - "SupportsIndex", "runtime_checkable", + // Introduced in Python 3.8, but `typing_extensions` contains backported bugfixes and + // optimizations. + // "Literal", + // "Protocol", + // "SupportsIndex", ]; // Python 3.9+ @@ -226,6 +251,8 @@ const TYPING_TO_COLLECTIONS_ABC_310: &[&str] = &["Callable"]; // Members of `typing_extensions` that were moved to `typing`. const TYPING_EXTENSIONS_TO_TYPING_310: &[&str] = &[ "Concatenate", + "Literal", + "NewType", "ParamSpecArgs", "ParamSpecKwargs", "TypeAlias", @@ -241,23 +268,28 @@ const TYPING_EXTENSIONS_TO_TYPING_310: &[&str] = &[ const TYPING_EXTENSIONS_TO_TYPING_311: &[&str] = &[ "Any", "LiteralString", - "NamedTuple", "Never", "NotRequired", "Required", "Self", - "TypedDict", - "Unpack", "assert_never", "assert_type", "clear_overloads", - "dataclass_transform", "final", "get_overloads", "overload", "reveal_type", ]; +// Python 3.12+ + +// Members of `typing_extensions` that were moved to `typing`. +const TYPING_EXTENSIONS_TO_TYPING_312: &[&str] = &[ + // Introduced in Python 3.11, but `typing_extensions` backports the `frozen_default` argument, + // which was introduced in Python 3.12. + "dataclass_transform", +]; + struct ImportReplacer<'a> { stmt: &'a Stmt, module: &'a str, @@ -342,6 +374,9 @@ impl<'a> ImportReplacer<'a> { if self.version >= PythonVersion::Py311 { typing_extensions_to_typing.extend(TYPING_EXTENSIONS_TO_TYPING_311); } + if self.version >= PythonVersion::Py312 { + typing_extensions_to_typing.extend(TYPING_EXTENSIONS_TO_TYPING_312); + } if let Some(operation) = self.try_replace(&typing_extensions_to_typing, "typing") { operations.push(operation); } @@ -436,7 +471,7 @@ impl<'a> ImportReplacer<'a> { // line, we can't add a statement after it. For example, if we have // `if True: import foo`, we can't add a statement to the next line. let Some(indentation) = indentation else { - let operation = WithoutRename { + let operation = WithoutRename { target: target.to_string(), members: matched_names .iter() @@ -523,7 +558,7 @@ pub(crate) fn deprecated_import( return; }; - if !RELEVANT_MODULES.contains(&module) { + if !is_relevant_module(module) { return; } @@ -546,8 +581,7 @@ pub(crate) fn deprecated_import( ); if checker.patch(Rule::DeprecatedImport) { if let Some(content) = fix { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( content, stmt.range(), ))); diff --git a/crates/ruff/src/rules/pyupgrade/rules/deprecated_mock_import.rs b/crates/ruff/src/rules/pyupgrade/rules/deprecated_mock_import.rs index 9680aa8998..30ecd488e5 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/deprecated_mock_import.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/deprecated_mock_import.rs @@ -23,6 +23,28 @@ pub(crate) enum MockReference { Attribute, } +/// ## What it does +/// Checks for imports of the `mock` module that should be replaced with +/// `unittest.mock`. +/// +/// ## Why is this bad? +/// Since Python 3.3, `mock` has been a part of the standard library as +/// `unittest.mock`. The `mock` package is deprecated; use `unittest.mock` +/// instead. +/// +/// ## Example +/// ```python +/// import mock +/// ``` +/// +/// Use instead: +/// ```python +/// from unittest import mock +/// ``` +/// +/// ## References +/// - [Python documentation: `unittest.mock`](https://docs.python.org/3/library/unittest.mock.html) +/// - [PyPI: `mock`](https://pypi.org/project/mock/) #[violation] pub struct DeprecatedMockImport { reference_type: MockReference, @@ -228,9 +250,9 @@ fn format_import_from( /// UP026 pub(crate) fn deprecated_mock_attribute(checker: &mut Checker, expr: &Expr) { if let Expr::Attribute(ast::ExprAttribute { value, .. }) = expr { - if collect_call_path(value) - .map_or(false, |call_path| call_path.as_slice() == ["mock", "mock"]) - { + if collect_call_path(value).map_or(false, |call_path| { + matches!(call_path.as_slice(), ["mock", "mock"]) + }) { let mut diagnostic = Diagnostic::new( DeprecatedMockImport { reference_type: MockReference::Attribute, @@ -238,8 +260,7 @@ pub(crate) fn deprecated_mock_attribute(checker: &mut Checker, expr: &Expr) { value.range(), ); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( "mock".to_string(), value.range(), ))); @@ -285,8 +306,7 @@ pub(crate) fn deprecated_mock_import(checker: &mut Checker, stmt: &Stmt) { name.range(), ); if let Some(content) = content.as_ref() { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( content.clone(), stmt.range(), ))); diff --git a/crates/ruff/src/rules/pyupgrade/rules/deprecated_unittest_alias.rs b/crates/ruff/src/rules/pyupgrade/rules/deprecated_unittest_alias.rs index e73b6a1efc..80bd2aecb6 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/deprecated_unittest_alias.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/deprecated_unittest_alias.rs @@ -8,6 +8,36 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Checks for uses of deprecated methods from the `unittest` module. +/// +/// ## Why is this bad? +/// The `unittest` module has deprecated aliases for some of its methods. +/// The aliases may be removed in future versions of Python. Instead, +/// use their non-deprecated counterparts. +/// +/// ## Example +/// ```python +/// from unittest import TestCase +/// +/// +/// class SomeTest(TestCase): +/// def test_something(self): +/// self.assertEquals(1, 1) +/// ``` +/// +/// Use instead: +/// ```python +/// from unittest import TestCase +/// +/// +/// class SomeTest(TestCase): +/// def test_something(self): +/// self.assertEqual(1, 1) +/// ``` +/// +/// ## References +/// - [Python documentation: Deprecated aliases](https://docs.python.org/3/library/unittest.html#deprecated-aliases) #[violation] pub struct DeprecatedUnittestAlias { alias: String, @@ -69,8 +99,7 @@ pub(crate) fn deprecated_unittest_alias(checker: &mut Checker, expr: &Expr) { expr.range(), ); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( format!("self.{target}"), expr.range(), ))); diff --git a/crates/ruff/src/rules/pyupgrade/rules/extraneous_parentheses.rs b/crates/ruff/src/rules/pyupgrade/rules/extraneous_parentheses.rs index 25884db60b..185932b8eb 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/extraneous_parentheses.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/extraneous_parentheses.rs @@ -9,6 +9,22 @@ use ruff_python_ast::source_code::Locator; use crate::registry::Rule; use crate::settings::Settings; +/// ## What it does +/// Checks for extraneous parentheses. +/// +/// ## Why is this bad? +/// Extraneous parentheses are redundant, and can be removed to improve +/// readability while retaining identical semantics. +/// +/// ## Example +/// ```python +/// print(("Hello, world")) +/// ``` +/// +/// Use instead: +/// ```python +/// print("Hello, world") +/// ``` #[violation] pub struct ExtraneousParentheses; @@ -90,7 +106,7 @@ fn match_extraneous_parentheses(tokens: &[LexResult], mut i: usize) -> Option<(u if i >= tokens.len() { return None; } - let Ok(( tok, _)) = &tokens[i] else { + let Ok((tok, _)) = &tokens[i] else { return None; }; match tok { @@ -106,7 +122,7 @@ fn match_extraneous_parentheses(tokens: &[LexResult], mut i: usize) -> Option<(u if i >= tokens.len() { return None; } - let Ok(( tok, _)) = &tokens[i] else { + let Ok((tok, _)) = &tokens[i] else { return None; }; if matches!(tok, Tok::Rpar) { @@ -118,21 +134,21 @@ fn match_extraneous_parentheses(tokens: &[LexResult], mut i: usize) -> Option<(u /// UP034 pub(crate) fn extraneous_parentheses( + diagnostics: &mut Vec, tokens: &[LexResult], locator: &Locator, settings: &Settings, -) -> Vec { - let mut diagnostics = vec![]; +) { let mut i = 0; while i < tokens.len() { if matches!(tokens[i], Ok((Tok::Lpar, _))) { if let Some((start, end)) = match_extraneous_parentheses(tokens, i) { i = end + 1; let Ok((_, start_range)) = &tokens[start] else { - return diagnostics; + return; }; let Ok((.., end_range)) = &tokens[end] else { - return diagnostics; + return; }; let mut diagnostic = Diagnostic::new( ExtraneousParentheses, @@ -141,8 +157,7 @@ pub(crate) fn extraneous_parentheses( if settings.rules.should_fix(Rule::ExtraneousParentheses) { let contents = locator.slice(TextRange::new(start_range.start(), end_range.end())); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::replacement( + diagnostic.set_fix(Fix::automatic(Edit::replacement( contents[1..contents.len() - 1].to_string(), start_range.start(), end_range.end(), @@ -156,5 +171,4 @@ pub(crate) fn extraneous_parentheses( i += 1; } } - diagnostics } diff --git a/crates/ruff/src/rules/pyupgrade/rules/f_strings.rs b/crates/ruff/src/rules/pyupgrade/rules/f_strings.rs index 2c23330d2d..494274b3a3 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/f_strings.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/f_strings.rs @@ -13,10 +13,30 @@ use ruff_python_ast::source_code::Locator; use ruff_python_ast::str::{is_implicit_concatenation, leading_quote, trailing_quote}; use crate::checkers::ast::Checker; +use crate::line_width::LineLength; use crate::registry::AsRule; use crate::rules::pyflakes::format::FormatSummary; use crate::rules::pyupgrade::helpers::curly_escape; +/// ## What it does +/// Checks for `str#format` calls that can be replaced with f-strings. +/// +/// ## Why is this bad? +/// f-strings are more readable and generally preferred over `str#format` +/// calls. +/// +/// ## Example +/// ```python +/// "{}".format(foo) +/// ``` +/// +/// Use instead: +/// ```python +/// f"{foo}" +/// ``` +/// +/// ## References +/// - [Python documentation: f-strings](https://docs.python.org/3/reference/lexical_analysis.html#f-strings) #[violation] pub struct FString; @@ -178,7 +198,7 @@ fn try_convert_to_f_string(expr: &Expr, locator: &Locator) -> Option { return None; }; - let Some(mut summary) = FormatSummaryValues::try_from_expr( expr, locator) else { + let Some(mut summary) = FormatSummaryValues::try_from_expr(expr, locator) else { return None; }; @@ -294,25 +314,31 @@ fn try_convert_to_f_string(expr: &Expr, locator: &Locator) -> Option { } /// UP032 -pub(crate) fn f_strings(checker: &mut Checker, summary: &FormatSummary, expr: &Expr) { +pub(crate) fn f_strings( + checker: &mut Checker, + summary: &FormatSummary, + expr: &Expr, + template: &Expr, + line_length: LineLength, +) { if summary.has_nested_parts { return; } // Avoid refactoring multi-line strings. - if checker.locator.contains_line_break(expr.range()) { + if checker.locator.contains_line_break(template.range()) { return; } // Currently, the only issue we know of is in LibCST: // https://github.com/Instagram/LibCST/issues/846 - let Some(mut contents) = try_convert_to_f_string( expr, checker.locator) else { + let Some(mut contents) = try_convert_to_f_string(expr, checker.locator) else { return; }; - // Avoid refactors that increase the resulting string length. - let existing = checker.locator.slice(expr.range()); - if contents.len() > existing.len() { + // Avoid refactors that exceed the line length limit. + let col_offset = template.start() - checker.locator.line_start(template.start()); + if col_offset.to_usize() + contents.len() > line_length.get() { return; } @@ -329,8 +355,7 @@ pub(crate) fn f_strings(checker: &mut Checker, summary: &FormatSummary, expr: &E let mut diagnostic = Diagnostic::new(FString, expr.range()); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( contents, expr.range(), ))); diff --git a/crates/ruff/src/rules/pyupgrade/rules/format_literals.rs b/crates/ruff/src/rules/pyupgrade/rules/format_literals.rs index dde4279aab..a15974eb94 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/format_literals.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/format_literals.rs @@ -14,6 +14,30 @@ use crate::cst::matchers::{match_attribute, match_call_mut, match_expression}; use crate::registry::AsRule; use crate::rules::pyflakes::format::FormatSummary; +/// ## What it does +/// Checks for unnecessary positional indices in format strings. +/// +/// ## Why is this bad? +/// In Python 3.1 and later, format strings can use implicit positional +/// references. For example, `"{0}, {1}".format("Hello", "World")` can be +/// rewritten as `"{}, {}".format("Hello", "World")`. +/// +/// If the positional indices appear exactly in-order, they can be omitted +/// in favor of automatic indices to improve readability. +/// +/// ## Example +/// ```python +/// "{0}, {1}".format("Hello", "World") # "Hello, World" +/// ``` +/// +/// Use instead: +/// ```python +/// "{}, {}".format("Hello", "World") # "Hello, World" +/// ``` +/// +/// ## References +/// - [Python documentation: Format String Syntax](https://docs.python.org/3/library/string.html#format-string-syntax) +/// - [Python documentation: `str.format`](https://docs.python.org/3/library/stdtypes.html#str.format) #[violation] pub struct FormatLiterals; diff --git a/crates/ruff/src/rules/pyupgrade/rules/lru_cache_with_maxsize_none.rs b/crates/ruff/src/rules/pyupgrade/rules/lru_cache_with_maxsize_none.rs index 9ecf931f2c..f02c6543d2 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/lru_cache_with_maxsize_none.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/lru_cache_with_maxsize_none.rs @@ -1,13 +1,47 @@ use ruff_text_size::TextRange; -use rustpython_parser::ast::{self, Constant, Decorator, Expr, Keyword, Ranged}; +use rustpython_parser::ast::{self, Decorator, Expr, Keyword, Ranged}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers::is_const_none; use crate::checkers::ast::Checker; use crate::importer::ImportRequest; use crate::registry::AsRule; +/// ## What it does +/// Checks for uses of `functools.lru_cache` that set `maxsize=None`. +/// +/// ## Why is this bad? +/// Since Python 3.9, `functools.cache` can be used as a drop-in replacement +/// for `functools.lru_cache(maxsize=None)`. When possible, prefer +/// `functools.cache` as it is more readable and idiomatic. +/// +/// ## Example +/// ```python +/// import functools +/// +/// +/// @functools.lru_cache(maxsize=None) +/// def foo(): +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// import functools +/// +/// +/// @functools.cache +/// def foo(): +/// ... +/// ``` +/// +/// ## Options +/// - `target-version` +/// +/// ## References +/// - [Python documentation: `@functools.cache`](https://docs.python.org/3/library/functools.html#functools.cache) #[violation] pub struct LRUCacheWithMaxsizeNone; @@ -24,13 +58,14 @@ impl AlwaysAutofixableViolation for LRUCacheWithMaxsizeNone { /// UP033 pub(crate) fn lru_cache_with_maxsize_none(checker: &mut Checker, decorator_list: &[Decorator]) { - for decorator in decorator_list.iter() { + for decorator in decorator_list { let Expr::Call(ast::ExprCall { func, args, keywords, range: _, - }) = &decorator.expression else { + }) = &decorator.expression + else { continue; }; @@ -38,10 +73,10 @@ pub(crate) fn lru_cache_with_maxsize_none(checker: &mut Checker, decorator_list: if args.is_empty() && keywords.len() == 1 && checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["functools", "lru_cache"] + matches!(call_path.as_slice(), ["functools", "lru_cache"]) }) { let Keyword { @@ -49,16 +84,7 @@ pub(crate) fn lru_cache_with_maxsize_none(checker: &mut Checker, decorator_list: value, range: _, } = &keywords[0]; - if arg.as_ref().map_or(false, |arg| arg == "maxsize") - && matches!( - value, - Expr::Constant(ast::ExprConstant { - value: Constant::None, - kind: None, - range: _, - }) - ) - { + if arg.as_ref().map_or(false, |arg| arg == "maxsize") && is_const_none(value) { let mut diagnostic = Diagnostic::new( LRUCacheWithMaxsizeNone, TextRange::new(func.end(), decorator.end()), @@ -68,12 +94,11 @@ pub(crate) fn lru_cache_with_maxsize_none(checker: &mut Checker, decorator_list: let (import_edit, binding) = checker.importer.get_or_import_symbol( &ImportRequest::import("functools", "cache"), decorator.start(), - checker.semantic_model(), + checker.semantic(), )?; let reference_edit = Edit::range_replacement(binding, decorator.expression.range()); - #[allow(deprecated)] - Ok(Fix::unspecified_edits(import_edit, [reference_edit])) + Ok(Fix::automatic_edits(import_edit, [reference_edit])) }); } checker.diagnostics.push(diagnostic); diff --git a/crates/ruff/src/rules/pyupgrade/rules/lru_cache_without_parameters.rs b/crates/ruff/src/rules/pyupgrade/rules/lru_cache_without_parameters.rs index 0f482afb58..594025e287 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/lru_cache_without_parameters.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/lru_cache_without_parameters.rs @@ -7,6 +7,39 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Checks for unnecessary parentheses on `functools.lru_cache` decorators. +/// +/// ## Why is this bad? +/// Since Python 3.8, `functools.lru_cache` can be used as a decorator without +/// trailing parentheses, as long as no arguments are passed to it. +/// +/// ## Example +/// ```python +/// import functools +/// +/// +/// @functools.lru_cache() +/// def foo(): +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// import functools +/// +/// +/// @functools.lru_cache +/// def foo(): +/// ... +/// ``` +/// +/// ## Options +/// - `target-version` +/// +/// ## References +/// - [Python documentation: `@functools.lru_cache`](https://docs.python.org/3/library/functools.html#functools.lru_cache) +/// - [Let lru_cache be used as a decorator with no arguments](https://github.com/python/cpython/issues/80953) #[violation] pub struct LRUCacheWithoutParameters; @@ -23,13 +56,14 @@ impl AlwaysAutofixableViolation for LRUCacheWithoutParameters { /// UP011 pub(crate) fn lru_cache_without_parameters(checker: &mut Checker, decorator_list: &[Decorator]) { - for decorator in decorator_list.iter() { + for decorator in decorator_list { let Expr::Call(ast::ExprCall { func, args, keywords, range: _, - }) = &decorator.expression else { + }) = &decorator.expression + else { continue; }; @@ -37,10 +71,10 @@ pub(crate) fn lru_cache_without_parameters(checker: &mut Checker, decorator_list if args.is_empty() && keywords.is_empty() && checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["functools", "lru_cache"] + matches!(call_path.as_slice(), ["functools", "lru_cache"]) }) { let mut diagnostic = Diagnostic::new( @@ -48,8 +82,7 @@ pub(crate) fn lru_cache_without_parameters(checker: &mut Checker, decorator_list TextRange::new(func.end(), decorator.end()), ); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( checker.generator().expr(func), decorator.expression.range(), ))); diff --git a/crates/ruff/src/rules/pyupgrade/rules/mod.rs b/crates/ruff/src/rules/pyupgrade/rules/mod.rs index 2a09b35d65..16ca52a57b 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/mod.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/mod.rs @@ -1,53 +1,40 @@ -pub(crate) use convert_named_tuple_functional_to_class::{ - convert_named_tuple_functional_to_class, ConvertNamedTupleFunctionalToClass, -}; -pub(crate) use convert_typed_dict_functional_to_class::{ - convert_typed_dict_functional_to_class, ConvertTypedDictFunctionalToClass, -}; -pub(crate) use datetime_utc_alias::{datetime_utc_alias, DatetimeTimezoneUTC}; -pub(crate) use deprecated_c_element_tree::{deprecated_c_element_tree, DeprecatedCElementTree}; -pub(crate) use deprecated_import::{deprecated_import, DeprecatedImport}; -pub(crate) use deprecated_mock_import::{ - deprecated_mock_attribute, deprecated_mock_import, DeprecatedMockImport, -}; -pub(crate) use deprecated_unittest_alias::{deprecated_unittest_alias, DeprecatedUnittestAlias}; -pub(crate) use extraneous_parentheses::{extraneous_parentheses, ExtraneousParentheses}; -pub(crate) use f_strings::{f_strings, FString}; -pub(crate) use format_literals::{format_literals, FormatLiterals}; -pub(crate) use lru_cache_with_maxsize_none::{ - lru_cache_with_maxsize_none, LRUCacheWithMaxsizeNone, -}; -pub(crate) use lru_cache_without_parameters::{ - lru_cache_without_parameters, LRUCacheWithoutParameters, -}; -pub(crate) use native_literals::{native_literals, NativeLiterals}; -pub(crate) use open_alias::{open_alias, OpenAlias}; -pub(crate) use os_error_alias::{ - os_error_alias_call, os_error_alias_handlers, os_error_alias_raise, OSErrorAlias, -}; -pub(crate) use outdated_version_block::{outdated_version_block, OutdatedVersionBlock}; -pub(crate) use printf_string_formatting::{printf_string_formatting, PrintfStringFormatting}; -pub(crate) use quoted_annotation::{quoted_annotation, QuotedAnnotation}; -pub(crate) use redundant_open_modes::{redundant_open_modes, RedundantOpenModes}; -pub(crate) use replace_stdout_stderr::{replace_stdout_stderr, ReplaceStdoutStderr}; -pub(crate) use replace_universal_newlines::{replace_universal_newlines, ReplaceUniversalNewlines}; -pub(crate) use super_call_with_parameters::{super_call_with_parameters, SuperCallWithParameters}; -pub(crate) use type_of_primitive::{type_of_primitive, TypeOfPrimitive}; -pub(crate) use typing_text_str_alias::{typing_text_str_alias, TypingTextStrAlias}; -pub(crate) use unicode_kind_prefix::{unicode_kind_prefix, UnicodeKindPrefix}; -pub(crate) use unnecessary_builtin_import::{unnecessary_builtin_import, UnnecessaryBuiltinImport}; -pub(crate) use unnecessary_coding_comment::{unnecessary_coding_comment, UTF8EncodingDeclaration}; -pub(crate) use unnecessary_encode_utf8::{unnecessary_encode_utf8, UnnecessaryEncodeUTF8}; -pub(crate) use unnecessary_future_import::{unnecessary_future_import, UnnecessaryFutureImport}; -pub(crate) use unpacked_list_comprehension::{ - unpacked_list_comprehension, UnpackedListComprehension, -}; -pub(crate) use use_pep585_annotation::{use_pep585_annotation, NonPEP585Annotation}; -pub(crate) use use_pep604_annotation::{use_pep604_annotation, NonPEP604Annotation}; -pub(crate) use use_pep604_isinstance::{use_pep604_isinstance, NonPEP604Isinstance}; -pub(crate) use useless_metaclass_type::{useless_metaclass_type, UselessMetaclassType}; -pub(crate) use useless_object_inheritance::{useless_object_inheritance, UselessObjectInheritance}; -pub(crate) use yield_in_for_loop::{yield_in_for_loop, YieldInForLoop}; +pub(crate) use convert_named_tuple_functional_to_class::*; +pub(crate) use convert_typed_dict_functional_to_class::*; +pub(crate) use datetime_utc_alias::*; +pub(crate) use deprecated_c_element_tree::*; +pub(crate) use deprecated_import::*; +pub(crate) use deprecated_mock_import::*; +pub(crate) use deprecated_unittest_alias::*; +pub(crate) use extraneous_parentheses::*; +pub(crate) use f_strings::*; +pub(crate) use format_literals::*; +pub(crate) use lru_cache_with_maxsize_none::*; +pub(crate) use lru_cache_without_parameters::*; +pub(crate) use native_literals::*; +pub(crate) use open_alias::*; +pub(crate) use os_error_alias::*; +pub(crate) use outdated_version_block::*; +pub(crate) use printf_string_formatting::*; +pub(crate) use quoted_annotation::*; +pub(crate) use redundant_open_modes::*; +pub(crate) use replace_stdout_stderr::*; +pub(crate) use replace_universal_newlines::*; +pub(crate) use super_call_with_parameters::*; +pub(crate) use type_of_primitive::*; +pub(crate) use typing_text_str_alias::*; +pub(crate) use unicode_kind_prefix::*; +pub(crate) use unnecessary_builtin_import::*; +pub(crate) use unnecessary_class_parentheses::*; +pub(crate) use unnecessary_coding_comment::*; +pub(crate) use unnecessary_encode_utf8::*; +pub(crate) use unnecessary_future_import::*; +pub(crate) use unpacked_list_comprehension::*; +pub(crate) use use_pep585_annotation::*; +pub(crate) use use_pep604_annotation::*; +pub(crate) use use_pep604_isinstance::*; +pub(crate) use useless_metaclass_type::*; +pub(crate) use useless_object_inheritance::*; +pub(crate) use yield_in_for_loop::*; mod convert_named_tuple_functional_to_class; mod convert_typed_dict_functional_to_class; @@ -75,6 +62,7 @@ mod type_of_primitive; mod typing_text_str_alias; mod unicode_kind_prefix; mod unnecessary_builtin_import; +mod unnecessary_class_parentheses; mod unnecessary_coding_comment; mod unnecessary_encode_utf8; mod unnecessary_future_import; diff --git a/crates/ruff/src/rules/pyupgrade/rules/native_literals.rs b/crates/ruff/src/rules/pyupgrade/rules/native_literals.rs index a7849d2c4f..14d7c5298b 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/native_literals.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/native_literals.rs @@ -24,6 +24,26 @@ impl fmt::Display for LiteralType { } } +/// ## What it does +/// Checks for unnecessary calls to `str` and `bytes`. +/// +/// ## Why is this bad? +/// The `str` and `bytes` constructors can be replaced with string and bytes +/// literals, which are more readable and idiomatic. +/// +/// ## Example +/// ```python +/// str("foo") +/// ``` +/// +/// Use instead: +/// ```python +/// "foo" +/// ``` +/// +/// ## References +/// - [Python documentation: `str`](https://docs.python.org/3/library/stdtypes.html#str) +/// - [Python documentation: `bytes`](https://docs.python.org/3/library/stdtypes.html#bytes) #[violation] pub struct NativeLiterals { literal_type: LiteralType, @@ -62,66 +82,19 @@ pub(crate) fn native_literals( } // There's no way to rewrite, e.g., `f"{f'{str()}'}"` within a nested f-string. - if checker.semantic_model().in_nested_f_string() { + if checker.semantic().in_nested_f_string() { return; } - if (id == "str" || id == "bytes") && checker.semantic_model().is_builtin(id) { - let Some(arg) = args.get(0) else { - let mut diagnostic = Diagnostic::new(NativeLiterals{literal_type:if id == "str" { - LiteralType::Str - } else { - LiteralType::Bytes - }}, expr.range()); - if checker.patch(diagnostic.kind.rule()) { - let constant = if id == "bytes" { - Constant::Bytes(vec![]) - } else { - Constant::Str(String::new()) - }; - let content = checker.generator().constant(&constant); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( - content, - expr.range(), - ))); - } - checker.diagnostics.push(diagnostic); - return; - }; + if !matches!(id.as_str(), "str" | "bytes") { + return; + } - // Look for `str("")`. - if id == "str" - && !matches!( - &arg, - Expr::Constant(ast::ExprConstant { - value: Constant::Str(_), - .. - }), - ) - { - return; - } - - // Look for `bytes(b"")` - if id == "bytes" - && !matches!( - &arg, - Expr::Constant(ast::ExprConstant { - value: Constant::Bytes(_), - .. - }), - ) - { - return; - } - - // Skip implicit string concatenations. - let arg_code = checker.locator.slice(arg.range()); - if is_implicit_concatenation(arg_code) { - return; - } + if !checker.semantic().is_builtin(id) { + return; + } + let Some(arg) = args.get(0) else { let mut diagnostic = Diagnostic::new( NativeLiterals { literal_type: if id == "str" { @@ -133,12 +106,68 @@ pub(crate) fn native_literals( expr.range(), ); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( - arg_code.to_string(), + let constant = if id == "bytes" { + Constant::Bytes(vec![]) + } else { + Constant::Str(String::new()) + }; + let content = checker.generator().constant(&constant); + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( + content, expr.range(), ))); } checker.diagnostics.push(diagnostic); + return; + }; + + // Look for `str("")`. + if id == "str" + && !matches!( + &arg, + Expr::Constant(ast::ExprConstant { + value: Constant::Str(_), + .. + }), + ) + { + return; } + + // Look for `bytes(b"")` + if id == "bytes" + && !matches!( + &arg, + Expr::Constant(ast::ExprConstant { + value: Constant::Bytes(_), + .. + }), + ) + { + return; + } + + // Skip implicit string concatenations. + let arg_code = checker.locator.slice(arg.range()); + if is_implicit_concatenation(arg_code) { + return; + } + + let mut diagnostic = Diagnostic::new( + NativeLiterals { + literal_type: if id == "str" { + LiteralType::Str + } else { + LiteralType::Bytes + }, + }, + expr.range(), + ); + if checker.patch(diagnostic.kind.rule()) { + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( + arg_code.to_string(), + expr.range(), + ))); + } + checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/pyupgrade/rules/open_alias.rs b/crates/ruff/src/rules/pyupgrade/rules/open_alias.rs index c8e3809123..259615755a 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/open_alias.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/open_alias.rs @@ -6,6 +6,29 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Checks for uses of `io.open`. +/// +/// ## Why is this bad? +/// In Python 3, `io.open` is an alias for `open`. Prefer using `open` directly, +/// as it is more idiomatic. +/// +/// ## Example +/// ```python +/// import io +/// +/// with io.open("file.txt") as f: +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// with open("file.txt") as f: +/// ... +/// ``` +/// +/// ## References +/// - [Python documentation: `io.open`](https://docs.python.org/3/library/io.html#io.open) #[violation] pub struct OpenAlias; @@ -25,15 +48,16 @@ impl Violation for OpenAlias { /// UP020 pub(crate) fn open_alias(checker: &mut Checker, expr: &Expr, func: &Expr) { if checker - .semantic_model() + .semantic() .resolve_call_path(func) - .map_or(false, |call_path| call_path.as_slice() == ["io", "open"]) + .map_or(false, |call_path| { + matches!(call_path.as_slice(), ["io", "open"]) + }) { let mut diagnostic = Diagnostic::new(OpenAlias, expr.range()); if checker.patch(diagnostic.kind.rule()) { - if checker.semantic_model().is_unbound("open") { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + if checker.semantic().is_builtin("open") { + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( "open".to_string(), func.range(), ))); diff --git a/crates/ruff/src/rules/pyupgrade/rules/os_error_alias.rs b/crates/ruff/src/rules/pyupgrade/rules/os_error_alias.rs index 82a3da68ba..6739f5b680 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/os_error_alias.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/os_error_alias.rs @@ -1,14 +1,39 @@ use ruff_text_size::TextRange; -use rustpython_parser::ast::{self, Excepthandler, Expr, ExprContext, Ranged}; +use rustpython_parser::ast::{self, ExceptHandler, Expr, ExprContext, Ranged}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::call_path::compose_call_path; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Checks for uses of exceptions that alias `OSError`. +/// +/// ## Why is this bad? +/// `OSError` is the builtin error type used for exceptions that relate to the +/// operating system. +/// +/// In Python 3.3, a variety of other exceptions, like `WindowsError` were +/// aliased to `OSError`. These aliases remain in place for compatibility with +/// older versions of Python, but may be removed in future versions. +/// +/// Prefer using `OSError` directly, as it is more idiomatic and future-proof. +/// +/// ## Example +/// ```python +/// raise IOError +/// ``` +/// +/// Use instead: +/// ```python +/// raise OSError +/// ``` +/// +/// ## References +/// - [Python documentation: `OSError`](https://docs.python.org/3/library/exceptions.html#OSError) #[violation] pub struct OSErrorAlias { pub name: Option, @@ -29,29 +54,22 @@ impl AlwaysAutofixableViolation for OSErrorAlias { } } -const ALIASES: &[(&str, &str)] = &[ - ("", "EnvironmentError"), - ("", "IOError"), - ("", "WindowsError"), - ("mmap", "error"), - ("select", "error"), - ("socket", "error"), -]; - /// Return `true` if an [`Expr`] is an alias of `OSError`. -fn is_alias(model: &SemanticModel, expr: &Expr) -> bool { - model.resolve_call_path(expr).map_or(false, |call_path| { - ALIASES - .iter() - .any(|(module, member)| call_path.as_slice() == [*module, *member]) +fn is_alias(expr: &Expr, semantic: &SemanticModel) -> bool { + semantic.resolve_call_path(expr).map_or(false, |call_path| { + matches!( + call_path.as_slice(), + ["", "EnvironmentError" | "IOError" | "WindowsError"] + | ["mmap" | "select" | "socket", "error"] + ) }) } /// Return `true` if an [`Expr`] is `OSError`. -fn is_os_error(model: &SemanticModel, expr: &Expr) -> bool { - model - .resolve_call_path(expr) - .map_or(false, |call_path| call_path.as_slice() == ["", "OSError"]) +fn is_os_error(expr: &Expr, semantic: &SemanticModel) -> bool { + semantic.resolve_call_path(expr).map_or(false, |call_path| { + matches!(call_path.as_slice(), ["", "OSError"]) + }) } /// Create a [`Diagnostic`] for a single target, like an [`Expr::Name`]. @@ -63,11 +81,12 @@ fn atom_diagnostic(checker: &mut Checker, target: &Expr) { target.range(), ); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( - "OSError".to_string(), - target.range(), - ))); + if checker.semantic().is_builtin("OSError") { + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( + "OSError".to_string(), + target.range(), + ))); + } } checker.diagnostics.push(diagnostic); } @@ -76,67 +95,64 @@ fn atom_diagnostic(checker: &mut Checker, target: &Expr) { fn tuple_diagnostic(checker: &mut Checker, target: &Expr, aliases: &[&Expr]) { let mut diagnostic = Diagnostic::new(OSErrorAlias { name: None }, target.range()); if checker.patch(diagnostic.kind.rule()) { - let Expr::Tuple(ast::ExprTuple { elts, ..}) = target else { - panic!("Expected Expr::Tuple"); - }; - - // Filter out any `OSErrors` aliases. - let mut remaining: Vec = elts - .iter() - .filter_map(|elt| { - if aliases.contains(&elt) { - None - } else { - Some(elt.clone()) - } - }) - .collect(); - - // If `OSError` itself isn't already in the tuple, add it. - if elts - .iter() - .all(|elt| !is_os_error(checker.semantic_model(), elt)) - { - let node = ast::ExprName { - id: "OSError".into(), - ctx: ExprContext::Load, - range: TextRange::default(), + if checker.semantic().is_builtin("OSError") { + let Expr::Tuple(ast::ExprTuple { elts, .. }) = target else { + panic!("Expected Expr::Tuple"); }; - remaining.insert(0, node.into()); - } - if remaining.len() == 1 { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( - "OSError".to_string(), - target.range(), - ))); - } else { - let node = ast::ExprTuple { - elts: remaining, - ctx: ExprContext::Load, - range: TextRange::default(), - }; - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( - format!("({})", checker.generator().expr(&node.into())), - target.range(), - ))); + // Filter out any `OSErrors` aliases. + let mut remaining: Vec = elts + .iter() + .filter_map(|elt| { + if aliases.contains(&elt) { + None + } else { + Some(elt.clone()) + } + }) + .collect(); + + // If `OSError` itself isn't already in the tuple, add it. + if elts.iter().all(|elt| !is_os_error(elt, checker.semantic())) { + let node = ast::ExprName { + id: "OSError".into(), + ctx: ExprContext::Load, + range: TextRange::default(), + }; + remaining.insert(0, node.into()); + } + + if remaining.len() == 1 { + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( + "OSError".to_string(), + target.range(), + ))); + } else { + let node = ast::ExprTuple { + elts: remaining, + ctx: ExprContext::Load, + range: TextRange::default(), + }; + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( + format!("({})", checker.generator().expr(&node.into())), + target.range(), + ))); + } } } checker.diagnostics.push(diagnostic); } /// UP024 -pub(crate) fn os_error_alias_handlers(checker: &mut Checker, handlers: &[Excepthandler]) { +pub(crate) fn os_error_alias_handlers(checker: &mut Checker, handlers: &[ExceptHandler]) { for handler in handlers { - let Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { type_, .. }) = handler; + let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { type_, .. }) = handler; let Some(expr) = type_.as_ref() else { continue; }; match expr.as_ref() { Expr::Name(_) | Expr::Attribute(_) => { - if is_alias(checker.semantic_model(), expr) { + if is_alias(expr, checker.semantic()) { atom_diagnostic(checker, expr); } } @@ -144,7 +160,7 @@ pub(crate) fn os_error_alias_handlers(checker: &mut Checker, handlers: &[Excepth // List of aliases to replace with `OSError`. let mut aliases: Vec<&Expr> = vec![]; for elt in elts { - if is_alias(checker.semantic_model(), elt) { + if is_alias(elt, checker.semantic()) { aliases.push(elt); } } @@ -159,7 +175,7 @@ pub(crate) fn os_error_alias_handlers(checker: &mut Checker, handlers: &[Excepth /// UP024 pub(crate) fn os_error_alias_call(checker: &mut Checker, func: &Expr) { - if is_alias(checker.semantic_model(), func) { + if is_alias(func, checker.semantic()) { atom_diagnostic(checker, func); } } @@ -167,7 +183,7 @@ pub(crate) fn os_error_alias_call(checker: &mut Checker, func: &Expr) { /// UP024 pub(crate) fn os_error_alias_raise(checker: &mut Checker, expr: &Expr) { if matches!(expr, Expr::Name(_) | Expr::Attribute(_)) { - if is_alias(checker.semantic_model(), expr) { + if is_alias(expr, checker.semantic()) { atom_diagnostic(checker, expr); } } diff --git a/crates/ruff/src/rules/pyupgrade/rules/outdated_version_block.rs b/crates/ruff/src/rules/pyupgrade/rules/outdated_version_block.rs index 96e8d3ac0a..e84e0ab64b 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/outdated_version_block.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/outdated_version_block.rs @@ -2,7 +2,7 @@ use std::cmp::Ordering; use num_bigint::{BigInt, Sign}; use ruff_text_size::{TextRange, TextSize}; -use rustpython_parser::ast::{self, Cmpop, Constant, Expr, Ranged, Stmt}; +use rustpython_parser::ast::{self, CmpOp, Constant, Expr, Ranged, Stmt}; use rustpython_parser::{lexer, Mode, Tok}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; @@ -16,6 +16,37 @@ use crate::registry::AsRule; use crate::rules::pyupgrade::fixes::adjust_indentation; use crate::settings::types::PythonVersion; +/// ## What it does +/// Checks for conditional blocks gated on `sys.version_info` comparisons +/// that are outdated for the minimum supported Python version. +/// +/// ## Why is this bad? +/// In Python, code can be conditionally executed based on the active +/// Python version by comparing against the `sys.version_info` tuple. +/// +/// If a code block is only executed for Python versions older than the +/// minimum supported version, it should be removed. +/// +/// ## Example +/// ```python +/// import sys +/// +/// if sys.version_info < (3, 0): +/// print("py2") +/// else: +/// print("py3") +/// ``` +/// +/// Use instead: +/// ```python +/// print("py3") +/// ``` +/// +/// ## Options +/// - `target-version` +/// +/// ## References +/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info) #[violation] pub struct OutdatedVersionBlock; @@ -188,14 +219,17 @@ fn fix_py2_block( // Delete the entire statement. If this is an `elif`, know it's the only child // of its parent, so avoid passing in the parent at all. Otherwise, // `delete_stmt` will erroneously include a `pass`. - let stmt = checker.semantic_model().stmt(); - let parent = checker.semantic_model().stmt_parent(); + let stmt = checker.semantic().stmt(); + let parent = checker.semantic().stmt_parent(); let edit = delete_stmt( stmt, - if matches!(block.leading_token.tok, StartTok::If) { parent } else { None }, + if matches!(block.leading_token.tok, StartTok::If) { + parent + } else { + None + }, checker.locator, checker.indexer, - checker.stylist, ); return Some(Fix::suggested(edit)); }; @@ -207,8 +241,7 @@ fn fix_py2_block( let end = orelse.last()?; if indentation(checker.locator, start).is_none() { // Inline `else` block (e.g., `else: x = 1`). - #[allow(deprecated)] - Some(Fix::unspecified(Edit::range_replacement( + Some(Fix::suggested(Edit::range_replacement( checker .locator .slice(TextRange::new(start.start(), end.end())) @@ -227,8 +260,7 @@ fn fix_py2_block( .ok() }) .map(|contents| { - #[allow(deprecated)] - Fix::unspecified(Edit::replacement( + Fix::suggested(Edit::replacement( contents, checker.locator.line_start(stmt.start()), stmt.end(), @@ -240,21 +272,13 @@ fn fix_py2_block( // If we have an `if` and an `elif`, turn the `elif` into an `if`. let start_location = leading_token.range.start(); let end_location = trailing_token.range.start() + TextSize::from(2); - #[allow(deprecated)] - Some(Fix::unspecified(Edit::deletion( - start_location, - end_location, - ))) + Some(Fix::suggested(Edit::deletion(start_location, end_location))) } (StartTok::Elif, _) => { // If we have an `elif`, delete up to the `else` or the end of the statement. let start_location = leading_token.range.start(); let end_location = trailing_token.range.start(); - #[allow(deprecated)] - Some(Fix::unspecified(Edit::deletion( - start_location, - end_location, - ))) + Some(Fix::suggested(Edit::deletion(start_location, end_location))) } } } @@ -275,8 +299,7 @@ fn fix_py3_block( let end = body.last()?; if indentation(checker.locator, start).is_none() { // Inline `if` block (e.g., `if ...: x = 1`). - #[allow(deprecated)] - Some(Fix::unspecified(Edit::range_replacement( + Some(Fix::suggested(Edit::range_replacement( checker .locator .slice(TextRange::new(start.start(), end.end())) @@ -295,8 +318,7 @@ fn fix_py3_block( .ok() }) .map(|contents| { - #[allow(deprecated)] - Fix::unspecified(Edit::replacement( + Fix::suggested(Edit::replacement( contents, checker.locator.line_start(stmt.start()), stmt.end(), @@ -309,8 +331,7 @@ fn fix_py3_block( // the rest. let end = body.last()?; let text = checker.locator.slice(TextRange::new(test.end(), end.end())); - #[allow(deprecated)] - Some(Fix::unspecified(Edit::range_replacement( + Some(Fix::suggested(Edit::range_replacement( format!("else{text}"), stmt.range(), ))) @@ -331,15 +352,16 @@ pub(crate) fn outdated_version_block( ops, comparators, range: _, - }) = &test else { + }) = &test + else { return; }; if !checker - .semantic_model() + .semantic() .resolve_call_path(left) .map_or(false, |call_path| { - call_path.as_slice() == ["sys", "version_info"] + matches!(call_path.as_slice(), ["sys", "version_info"]) }) { return; @@ -352,8 +374,8 @@ pub(crate) fn outdated_version_block( Expr::Tuple(ast::ExprTuple { elts, .. }) => { let version = extract_version(elts); let target = checker.settings.target_version; - if op == &Cmpop::Lt || op == &Cmpop::LtE { - if compare_version(&version, target, op == &Cmpop::LtE) { + if op == &CmpOp::Lt || op == &CmpOp::LtE { + if compare_version(&version, target, op == &CmpOp::LtE) { let mut diagnostic = Diagnostic::new(OutdatedVersionBlock, stmt.range()); if checker.patch(diagnostic.kind.rule()) { if let Some(block) = metadata(checker.locator, stmt, body) { @@ -364,8 +386,8 @@ pub(crate) fn outdated_version_block( } checker.diagnostics.push(diagnostic); } - } else if op == &Cmpop::Gt || op == &Cmpop::GtE { - if compare_version(&version, target, op == &Cmpop::GtE) { + } else if op == &CmpOp::Gt || op == &CmpOp::GtE { + if compare_version(&version, target, op == &CmpOp::GtE) { let mut diagnostic = Diagnostic::new(OutdatedVersionBlock, stmt.range()); if checker.patch(diagnostic.kind.rule()) { if let Some(block) = metadata(checker.locator, stmt, body) { @@ -384,7 +406,7 @@ pub(crate) fn outdated_version_block( .. }) => { let version_number = bigint_to_u32(number); - if version_number == 2 && op == &Cmpop::Eq { + if version_number == 2 && op == &CmpOp::Eq { let mut diagnostic = Diagnostic::new(OutdatedVersionBlock, stmt.range()); if checker.patch(diagnostic.kind.rule()) { if let Some(block) = metadata(checker.locator, stmt, body) { @@ -394,7 +416,7 @@ pub(crate) fn outdated_version_block( } } checker.diagnostics.push(diagnostic); - } else if version_number == 3 && op == &Cmpop::Eq { + } else if version_number == 3 && op == &CmpOp::Eq { let mut diagnostic = Diagnostic::new(OutdatedVersionBlock, stmt.range()); if checker.patch(diagnostic.kind.rule()) { if let Some(block) = metadata(checker.locator, stmt, body) { diff --git a/crates/ruff/src/rules/pyupgrade/rules/printf_string_formatting.rs b/crates/ruff/src/rules/pyupgrade/rules/printf_string_formatting.rs index 5bcf6c764d..4477aa3cb6 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/printf_string_formatting.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/printf_string_formatting.rs @@ -18,6 +18,28 @@ use crate::checkers::ast::Checker; use crate::registry::AsRule; use crate::rules::pyupgrade::helpers::curly_escape; +/// ## What it does +/// Checks for `printf`-style string formatting. +/// +/// ## Why is this bad? +/// `printf`-style string formatting has a number of quirks, and leads to less +/// readable code than using `str.format` calls or f-strings. In general, prefer +/// the newer `str.format` and f-strings constructs over `printf`-style string +/// formatting. +/// +/// ## Example +/// ```python +/// "%s, %s" % ("Hello", "World") # "Hello, World" +/// ``` +/// +/// Use instead: +/// ```python +/// "{}, {}".format("Hello", "World") # "Hello, World" +/// ``` +/// +/// ## References +/// - [Python documentation: `printf`-style String Formatting](https://docs.python.org/3/library/stdtypes.html#old-string-formatting) +/// - [Python documentation: `str.format`](https://docs.python.org/3/library/stdtypes.html#str.format) #[violation] pub struct PrintfStringFormatting; @@ -322,7 +344,7 @@ pub(crate) fn printf_string_formatting( ) .flatten() { - if matches!(tok, Tok::String { .. }) { + if tok.is_string() { strings.push(range); } else if matches!(tok, Tok::Rpar) { // If we hit a right paren, we have to preserve it. @@ -446,8 +468,7 @@ pub(crate) fn printf_string_formatting( let mut diagnostic = Diagnostic::new(PrintfStringFormatting, expr.range()); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( contents, expr.range(), ))); @@ -456,7 +477,7 @@ pub(crate) fn printf_string_formatting( } #[cfg(test)] -mod test { +mod tests { use test_case::test_case; use super::*; diff --git a/crates/ruff/src/rules/pyupgrade/rules/quoted_annotation.rs b/crates/ruff/src/rules/pyupgrade/rules/quoted_annotation.rs index 1a8a29e0a6..749e3011f5 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/quoted_annotation.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/quoted_annotation.rs @@ -6,6 +6,36 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; use crate::registry::Rule; +/// ## What it does +/// Checks for the presence of unnecessary quotes in type annotations. +/// +/// ## Why is this bad? +/// In Python, type annotations can be quoted to avoid forward references. +/// However, if `from __future__ import annotations` is present, Python +/// will always evaluate type annotations in a deferred manner, making +/// the quotes unnecessary. +/// +/// ## Example +/// ```python +/// from __future__ import annotations +/// +/// +/// def foo(bar: "Bar") -> "Bar": +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// from __future__ import annotations +/// +/// +/// def foo(bar: Bar) -> Bar: +/// ... +/// ``` +/// +/// ## References +/// - [PEP 563](https://peps.python.org/pep-0563/) +/// - [Python documentation: `__future__`](https://docs.python.org/3/library/__future__.html#module-__future__) #[violation] pub struct QuotedAnnotation; @@ -24,8 +54,7 @@ impl AlwaysAutofixableViolation for QuotedAnnotation { pub(crate) fn quoted_annotation(checker: &mut Checker, annotation: &str, range: TextRange) { let mut diagnostic = Diagnostic::new(QuotedAnnotation, range); if checker.patch(Rule::QuotedAnnotation) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( annotation.to_string(), range, ))); diff --git a/crates/ruff/src/rules/pyupgrade/rules/redundant_open_modes.rs b/crates/ruff/src/rules/pyupgrade/rules/redundant_open_modes.rs index 9dabf61d24..0b7548498c 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/redundant_open_modes.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/redundant_open_modes.rs @@ -3,19 +3,41 @@ use std::str::FromStr; use anyhow::{anyhow, Result}; use ruff_text_size::TextSize; use rustpython_parser::ast::{self, Constant, Expr, Keyword, Ranged}; -use rustpython_parser::{lexer, Mode, Tok}; +use rustpython_parser::{lexer, Mode}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::find_keyword; use ruff_python_ast::source_code::Locator; +use ruff_python_semantic::SemanticModel; use crate::checkers::ast::Checker; use crate::registry::Rule; +/// ## What it does +/// Checks for redundant `open` mode parameters. +/// +/// ## Why is this bad? +/// Redundant `open` mode parameters are unnecessary and should be removed to +/// avoid confusion. +/// +/// ## Example +/// ```python +/// with open("foo.txt", "r") as f: +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// with open("foo.txt") as f: +/// ... +/// ``` +/// +/// ## References +/// - [Python documentation: `open`](https://docs.python.org/3/library/functions.html#open) #[violation] pub struct RedundantOpenModes { - pub replacement: Option, + replacement: Option, } impl AlwaysAutofixableViolation for RedundantOpenModes { @@ -41,10 +63,78 @@ impl AlwaysAutofixableViolation for RedundantOpenModes { } } +/// UP015 +pub(crate) fn redundant_open_modes(checker: &mut Checker, expr: &Expr) { + let Some((mode_param, keywords)) = match_open(expr, checker.semantic()) else { + return; + }; + match mode_param { + None => { + if !keywords.is_empty() { + if let Some(keyword) = find_keyword(keywords, MODE_KEYWORD_ARGUMENT) { + if let Expr::Constant(ast::ExprConstant { + value: Constant::Str(mode_param_value), + .. + }) = &keyword.value + { + if let Ok(mode) = OpenMode::from_str(mode_param_value.as_str()) { + checker.diagnostics.push(create_check( + expr, + &keyword.value, + mode.replacement_value(), + checker.locator, + checker.patch(Rule::RedundantOpenModes), + )); + } + } + } + } + } + Some(mode_param) => { + if let Expr::Constant(ast::ExprConstant { + value: Constant::Str(value), + .. + }) = &mode_param + { + if let Ok(mode) = OpenMode::from_str(value.as_str()) { + checker.diagnostics.push(create_check( + expr, + mode_param, + mode.replacement_value(), + checker.locator, + checker.patch(Rule::RedundantOpenModes), + )); + } + } + } + } +} + const OPEN_FUNC_NAME: &str = "open"; const MODE_KEYWORD_ARGUMENT: &str = "mode"; -#[derive(Copy, Clone)] +fn match_open<'a>( + expr: &'a Expr, + model: &SemanticModel, +) -> Option<(Option<&'a Expr>, &'a [Keyword])> { + let ast::ExprCall { + func, + args, + keywords, + range: _, + } = expr.as_call_expr()?; + + let ast::ExprName { id, .. } = func.as_name_expr()?; + + if id.as_str() == OPEN_FUNC_NAME && model.is_builtin(id) { + // Return the "open mode" parameter and keywords. + Some((args.get(1), keywords)) + } else { + None + } +} + +#[derive(Debug, Copy, Clone)] enum OpenMode { U, Ur, @@ -86,22 +176,6 @@ impl OpenMode { } } -fn match_open(expr: &Expr) -> (Option<&Expr>, Vec) { - if let Expr::Call(ast::ExprCall { - func, - args, - keywords, - range: _, - }) = expr - { - if matches!(func.as_ref(), Expr::Name(ast::ExprName {id, ..}) if id == OPEN_FUNC_NAME) { - // Return the "open mode" parameter and keywords. - return (args.get(1), keywords.clone()); - } - } - (None, vec![]) -} - fn create_check( expr: &Expr, mode_param: &Expr, @@ -117,14 +191,14 @@ fn create_check( ); if patch { if let Some(content) = replacement_value { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( content.to_string(), mode_param.range(), ))); } else { - #[allow(deprecated)] - diagnostic.try_set_fix_from_edit(|| create_remove_param_fix(locator, expr, mode_param)); + diagnostic.try_set_fix(|| { + create_remove_param_fix(locator, expr, mode_param).map(Fix::automatic) + }); } } diagnostic @@ -147,15 +221,15 @@ fn create_remove_param_fix(locator: &Locator, expr: &Expr, mode_param: &Expr) -> fix_end = Some(range.end()); break; } - if delete_first_arg && matches!(tok, Tok::Name { .. }) { + if delete_first_arg && tok.is_name() { fix_end = Some(range.start()); break; } - if matches!(tok, Tok::Lpar) { + if tok.is_lpar() { is_first_arg = true; fix_start = Some(range.end()); } - if matches!(tok, Tok::Comma) { + if tok.is_comma() { is_first_arg = false; if !delete_first_arg { fix_start = Some(range.start()); @@ -169,47 +243,3 @@ fn create_remove_param_fix(locator: &Locator, expr: &Expr, mode_param: &Expr) -> )), } } - -/// UP015 -pub(crate) fn redundant_open_modes(checker: &mut Checker, expr: &Expr) { - // If `open` has been rebound, skip this check entirely. - if !checker.semantic_model().is_builtin(OPEN_FUNC_NAME) { - return; - } - let (mode_param, keywords): (Option<&Expr>, Vec) = match_open(expr); - if mode_param.is_none() && !keywords.is_empty() { - if let Some(keyword) = find_keyword(&keywords, MODE_KEYWORD_ARGUMENT) { - if let Expr::Constant(ast::ExprConstant { - value: Constant::Str(mode_param_value), - .. - }) = &keyword.value - { - if let Ok(mode) = OpenMode::from_str(mode_param_value.as_str()) { - checker.diagnostics.push(create_check( - expr, - &keyword.value, - mode.replacement_value(), - checker.locator, - checker.patch(Rule::RedundantOpenModes), - )); - } - } - } - } else if let Some(mode_param) = mode_param { - if let Expr::Constant(ast::ExprConstant { - value: Constant::Str(mode_param_value), - .. - }) = &mode_param - { - if let Ok(mode) = OpenMode::from_str(mode_param_value.as_str()) { - checker.diagnostics.push(create_check( - expr, - mode_param, - mode.replacement_value(), - checker.locator, - checker.patch(Rule::RedundantOpenModes), - )); - } - } - } -} diff --git a/crates/ruff/src/rules/pyupgrade/rules/replace_stdout_stderr.rs b/crates/ruff/src/rules/pyupgrade/rules/replace_stdout_stderr.rs index b1aee43ed4..a94eed542e 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/replace_stdout_stderr.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/replace_stdout_stderr.rs @@ -10,13 +10,40 @@ use crate::autofix::edits::remove_argument; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Checks for uses of `subprocess.run` that send `stdout` and `stderr` to a +/// pipe. +/// +/// ## Why is this bad? +/// As of Python 3.7, `subprocess.run` has a `capture_output` keyword argument +/// that can be set to `True` to capture `stdout` and `stderr` outputs. This is +/// equivalent to setting `stdout` and `stderr` to `subprocess.PIPE`, but is +/// more explicit and readable. +/// +/// ## Example +/// ```python +/// import subprocess +/// +/// subprocess.run(["foo"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) +/// ``` +/// +/// Use instead: +/// ```python +/// import subprocess +/// +/// subprocess.run(["foo"], capture_output=True) +/// ``` +/// +/// ## References +/// - [Python 3.7 release notes](https://docs.python.org/3/whatsnew/3.7.html#subprocess) +/// - [Python documentation: `subprocess.run`](https://docs.python.org/3/library/subprocess.html#subprocess.run) #[violation] pub struct ReplaceStdoutStderr; impl AlwaysAutofixableViolation for ReplaceStdoutStderr { #[derive_message_formats] fn message(&self) -> String { - format!("Sending stdout and stderr to pipe is deprecated, use `capture_output`") + format!("Sending `stdout` and `stderr` to `PIPE` is deprecated, use `capture_output`") } fn autofix_title(&self) -> String { @@ -38,12 +65,11 @@ fn generate_fix( } else { (stderr, stdout) }; - #[allow(deprecated)] - Ok(Fix::unspecified_edits( + Ok(Fix::suggested_edits( Edit::range_replacement("capture_output=True".to_string(), first.range()), [remove_argument( locator, - func.start(), + func.end(), second.range(), args, keywords, @@ -61,10 +87,10 @@ pub(crate) fn replace_stdout_stderr( keywords: &[Keyword], ) { if checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["subprocess", "run"] + matches!(call_path.as_slice(), ["subprocess", "run"]) }) { // Find `stdout` and `stderr` kwargs. @@ -77,16 +103,16 @@ pub(crate) fn replace_stdout_stderr( // Verify that they're both set to `subprocess.PIPE`. if !checker - .semantic_model() + .semantic() .resolve_call_path(&stdout.value) .map_or(false, |call_path| { - call_path.as_slice() == ["subprocess", "PIPE"] + matches!(call_path.as_slice(), ["subprocess", "PIPE"]) }) || !checker - .semantic_model() + .semantic() .resolve_call_path(&stderr.value) .map_or(false, |call_path| { - call_path.as_slice() == ["subprocess", "PIPE"] + matches!(call_path.as_slice(), ["subprocess", "PIPE"]) }) { return; diff --git a/crates/ruff/src/rules/pyupgrade/rules/replace_universal_newlines.rs b/crates/ruff/src/rules/pyupgrade/rules/replace_universal_newlines.rs index 23c0e49de0..64b3e59aab 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/replace_universal_newlines.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/replace_universal_newlines.rs @@ -8,6 +8,33 @@ use ruff_python_ast::helpers::find_keyword; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Checks for uses of `subprocess.run` that set the `universal_newlines` +/// keyword argument. +/// +/// ## Why is this bad? +/// As of Python 3.7, the `universal_newlines` keyword argument has been +/// renamed to `text`, and now exists for backwards compatibility. The +/// `universal_newlines` keyword argument may be removed in a future version of +/// Python. Prefer `text`, which is more explicit and readable. +/// +/// ## Example +/// ```python +/// import subprocess +/// +/// subprocess.run(["foo"], universal_newlines=True) +/// ``` +/// +/// Use instead: +/// ```python +/// import subprocess +/// +/// subprocess.run(["foo"], text=True) +/// ``` +/// +/// ## References +/// - [Python 3.7 release notes](https://docs.python.org/3/whatsnew/3.7.html#subprocess) +/// - [Python documentation: `subprocess.run`](https://docs.python.org/3/library/subprocess.html#subprocess.run) #[violation] pub struct ReplaceUniversalNewlines; @@ -25,18 +52,19 @@ impl AlwaysAutofixableViolation for ReplaceUniversalNewlines { /// UP021 pub(crate) fn replace_universal_newlines(checker: &mut Checker, func: &Expr, kwargs: &[Keyword]) { if checker - .semantic_model() + .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - call_path.as_slice() == ["subprocess", "run"] + matches!(call_path.as_slice(), ["subprocess", "run"]) }) { - let Some(kwarg) = find_keyword(kwargs, "universal_newlines") else { return; }; + let Some(kwarg) = find_keyword(kwargs, "universal_newlines") else { + return; + }; let range = TextRange::at(kwarg.start(), "universal_newlines".text_len()); let mut diagnostic = Diagnostic::new(ReplaceUniversalNewlines, range); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( "text".to_string(), range, ))); diff --git a/crates/ruff/src/rules/pyupgrade/rules/super_call_with_parameters.rs b/crates/ruff/src/rules/pyupgrade/rules/super_call_with_parameters.rs index 63b4db3443..2362552fad 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/super_call_with_parameters.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/super_call_with_parameters.rs @@ -1,4 +1,4 @@ -use rustpython_parser::ast::{self, Arg, Expr, Ranged, Stmt}; +use rustpython_parser::ast::{self, Arg, ArgWithDefault, Expr, Ranged, Stmt}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix}; use ruff_macros::{derive_message_formats, violation}; @@ -7,6 +7,44 @@ use crate::checkers::ast::Checker; use crate::registry::AsRule; use crate::rules::pyupgrade::fixes; +/// ## What it does +/// Checks for `super` calls that pass redundant arguments. +/// +/// ## Why is this bad? +/// In Python 3, `super` can be invoked without any arguments when: (1) the +/// first argument is `__class__`, and (2) the second argument is equivalent to +/// the first argument of the enclosing method. +/// +/// When possible, omit the arguments to `super` to make the code more concise +/// and maintainable. +/// +/// ## Example +/// ```python +/// class A: +/// def foo(self): +/// pass +/// +/// +/// class B(A): +/// def bar(self): +/// super(B, self).foo() +/// ``` +/// +/// Use instead: +/// ```python +/// class A: +/// def foo(self): +/// pass +/// +/// +/// class B(A): +/// def bar(self): +/// super().foo() +/// ``` +/// +/// ## References +/// - [Python documentation: `super`](https://docs.python.org/3/library/functions.html#super) +/// - [super/MRO, Python's most misunderstood feature.](https://www.youtube.com/watch?v=X1PQ7zzltz4) #[violation] pub struct SuperCallWithParameters; @@ -42,14 +80,14 @@ pub(crate) fn super_call_with_parameters( if !is_super_call_with_arguments(func, args) { return; } - let scope = checker.semantic_model().scope(); + let scope = checker.semantic().scope(); // Check: are we in a Function scope? if !scope.kind.is_any_function() { return; } - let mut parents = checker.semantic_model().parents(); + let mut parents = checker.semantic().parents(); // For a `super` invocation to be unnecessary, the first argument needs to match // the enclosing class, and the second argument needs to match the first @@ -61,21 +99,27 @@ pub(crate) fn super_call_with_parameters( // Find the enclosing function definition (if any). let Some(Stmt::FunctionDef(ast::StmtFunctionDef { args: parent_args, .. - })) = parents.find(|stmt| stmt.is_function_def_stmt()) else { + })) = parents.find(|stmt| stmt.is_function_def_stmt()) + else { return; }; // Extract the name of the first argument to the enclosing function. - let Some(Arg { - arg: parent_arg, .. - }) = parent_args.args.first() else { + let Some(ArgWithDefault { + def: Arg { + arg: parent_arg, .. + }, + .. + }) = parent_args.args.first() + else { return; }; // Find the enclosing class definition (if any). let Some(Stmt::ClassDef(ast::StmtClassDef { name: parent_name, .. - })) = parents.find(|stmt| stmt.is_class_def_stmt()) else { + })) = parents.find(|stmt| stmt.is_class_def_stmt()) + else { return; }; @@ -86,11 +130,12 @@ pub(crate) fn super_call_with_parameters( Expr::Name(ast::ExprName { id: second_arg_id, .. }), - ) = (first_arg, second_arg) else { + ) = (first_arg, second_arg) + else { return; }; - if !(first_arg_id == parent_name && second_arg_id == parent_arg) { + if !(first_arg_id == parent_name.as_str() && second_arg_id == parent_arg.as_str()) { return; } diff --git a/crates/ruff/src/rules/pyupgrade/rules/type_of_primitive.rs b/crates/ruff/src/rules/pyupgrade/rules/type_of_primitive.rs index 7f2c9a5964..9be33ea2c0 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/type_of_primitive.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/type_of_primitive.rs @@ -1,6 +1,6 @@ use rustpython_parser::ast::{self, Expr, Ranged}; -use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; +use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; @@ -8,21 +8,47 @@ use crate::registry::AsRule; use super::super::types::Primitive; +/// ## What it does +/// Checks for uses of `type` that take a primitive as an argument. +/// +/// ## Why is this bad? +/// `type()` returns the type of a given object. A type of a primitive can +/// always be known in advance and accessed directly, which is more concise +/// and explicit than using `type()`. +/// +/// ## Example +/// ```python +/// type(1) +/// ``` +/// +/// Use instead: +/// ```python +/// int +/// ``` +/// +/// ## References +/// - [Python documentation: `type()`](https://docs.python.org/3/library/functions.html#type) +/// - [Python documentation: Built-in types](https://docs.python.org/3/library/stdtypes.html) #[violation] pub struct TypeOfPrimitive { primitive: Primitive, } -impl AlwaysAutofixableViolation for TypeOfPrimitive { +impl Violation for TypeOfPrimitive { + const AUTOFIX: AutofixKind = AutofixKind::Sometimes; + #[derive_message_formats] fn message(&self) -> String { let TypeOfPrimitive { primitive } = self; format!("Use `{}` instead of `type(...)`", primitive.builtin()) } - fn autofix_title(&self) -> String { + fn autofix_title(&self) -> Option { let TypeOfPrimitive { primitive } = self; - format!("Replace `type(...)` with `{}`", primitive.builtin()) + Some(format!( + "Replace `type(...)` with `{}`", + primitive.builtin() + )) } } @@ -32,13 +58,15 @@ pub(crate) fn type_of_primitive(checker: &mut Checker, expr: &Expr, func: &Expr, return; } if !checker - .semantic_model() + .semantic() .resolve_call_path(func) - .map_or(false, |call_path| call_path.as_slice() == ["", "type"]) + .map_or(false, |call_path| { + matches!(call_path.as_slice(), ["", "type"]) + }) { return; } - let Expr::Constant(ast::ExprConstant { value, .. } )= &args[0] else { + let Expr::Constant(ast::ExprConstant { value, .. }) = &args[0] else { return; }; let Some(primitive) = Primitive::from_constant(value) else { @@ -46,11 +74,13 @@ pub(crate) fn type_of_primitive(checker: &mut Checker, expr: &Expr, func: &Expr, }; let mut diagnostic = Diagnostic::new(TypeOfPrimitive { primitive }, expr.range()); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( - primitive.builtin(), - expr.range(), - ))); + let builtin = primitive.builtin(); + if checker.semantic().is_builtin(&builtin) { + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( + primitive.builtin(), + expr.range(), + ))); + } } checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/pyupgrade/rules/typing_text_str_alias.rs b/crates/ruff/src/rules/pyupgrade/rules/typing_text_str_alias.rs index 5b1a867cca..ea8d0955ce 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/typing_text_str_alias.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/typing_text_str_alias.rs @@ -1,41 +1,66 @@ use rustpython_parser::ast::{Expr, Ranged}; -use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; +use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Checks for uses of `typing.Text`. +/// +/// ## Why is this bad? +/// `typing.Text` is an alias for `str`, and only exists for Python 2 +/// compatibility. As of Python 3.11, `typing.Text` is deprecated. Use `str` +/// instead. +/// +/// ## Example +/// ```python +/// from typing import Text +/// +/// foo: Text = "bar" +/// ``` +/// +/// Use instead: +/// ```python +/// foo: str = "bar" +/// ``` +/// +/// ## References +/// - [Python documentation: `typing.Text`](https://docs.python.org/3/library/typing.html#typing.Text) #[violation] pub struct TypingTextStrAlias; -impl AlwaysAutofixableViolation for TypingTextStrAlias { +impl Violation for TypingTextStrAlias { + const AUTOFIX: AutofixKind = AutofixKind::Sometimes; + #[derive_message_formats] fn message(&self) -> String { format!("`typing.Text` is deprecated, use `str`") } - fn autofix_title(&self) -> String { - "Replace with `str`".to_string() + fn autofix_title(&self) -> Option { + Some("Replace with `str`".to_string()) } } /// UP019 pub(crate) fn typing_text_str_alias(checker: &mut Checker, expr: &Expr) { if checker - .semantic_model() + .semantic() .resolve_call_path(expr) .map_or(false, |call_path| { - call_path.as_slice() == ["typing", "Text"] + matches!(call_path.as_slice(), ["typing", "Text"]) }) { let mut diagnostic = Diagnostic::new(TypingTextStrAlias, expr.range()); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( - "str".to_string(), - expr.range(), - ))); + if checker.semantic().is_builtin("str") { + diagnostic.set_fix(Fix::automatic(Edit::range_replacement( + "str".to_string(), + expr.range(), + ))); + } } checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/pyupgrade/rules/unicode_kind_prefix.rs b/crates/ruff/src/rules/pyupgrade/rules/unicode_kind_prefix.rs index 45dbf962d5..4058be5de3 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/unicode_kind_prefix.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/unicode_kind_prefix.rs @@ -7,6 +7,25 @@ use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Checks for uses of the Unicode kind prefix (`u`) in strings. +/// +/// ## Why is this bad? +/// In Python 3, all strings are Unicode by default. The Unicode kind prefix is +/// unnecessary and should be removed to avoid confusion. +/// +/// ## Example +/// ```python +/// u"foo" +/// ``` +/// +/// Use instead: +/// ```python +/// "foo" +/// ``` +/// +/// ## References +/// - [Python documentation: Unicode HOWTO](https://docs.python.org/3/howto/unicode.html) #[violation] pub struct UnicodeKindPrefix; @@ -23,17 +42,14 @@ impl AlwaysAutofixableViolation for UnicodeKindPrefix { /// UP025 pub(crate) fn unicode_kind_prefix(checker: &mut Checker, expr: &Expr, kind: Option<&str>) { - if let Some(const_kind) = kind { - if const_kind.to_lowercase() == "u" { - let mut diagnostic = Diagnostic::new(UnicodeKindPrefix, expr.range()); - if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_deletion(TextRange::at( - expr.start(), - TextSize::from(1), - )))); - } - checker.diagnostics.push(diagnostic); + if matches!(kind, Some("u" | "U")) { + let mut diagnostic = Diagnostic::new(UnicodeKindPrefix, expr.range()); + if checker.patch(diagnostic.kind.rule()) { + diagnostic.set_fix(Fix::automatic(Edit::range_deletion(TextRange::at( + expr.start(), + TextSize::from(1), + )))); } + checker.diagnostics.push(diagnostic); } } diff --git a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_builtin_import.rs b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_builtin_import.rs index b604684c1c..60d856bec0 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_builtin_import.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_builtin_import.rs @@ -8,6 +8,27 @@ use crate::autofix; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Checks for unnecessary imports of builtins. +/// +/// ## Why is this bad? +/// Builtins are always available. Importing them is unnecessary and should be +/// removed to avoid confusion. +/// +/// ## Example +/// ```python +/// from builtins import str +/// +/// str(1) +/// ``` +/// +/// Use instead: +/// ```python +/// str(1) +/// ``` +/// +/// ## References +/// - [Python documentation: The Python Standard Library](https://docs.python.org/3/library/index.html) #[violation] pub struct UnnecessaryBuiltinImport { pub names: Vec, @@ -31,37 +52,6 @@ impl AlwaysAutofixableViolation for UnnecessaryBuiltinImport { } } -const BUILTINS: &[&str] = &[ - "*", - "ascii", - "bytes", - "chr", - "dict", - "filter", - "hex", - "input", - "int", - "isinstance", - "list", - "map", - "max", - "min", - "next", - "object", - "oct", - "open", - "pow", - "range", - "round", - "str", - "super", - "zip", -]; -const IO: &[&str] = &["open"]; -const SIX_MOVES_BUILTINS: &[&str] = BUILTINS; -const SIX: &[&str] = &["callable", "next"]; -const SIX_MOVES: &[&str] = &["filter", "input", "map", "range", "zip"]; - /// UP029 pub(crate) fn unnecessary_builtin_import( checker: &mut Checker, @@ -69,26 +59,53 @@ pub(crate) fn unnecessary_builtin_import( module: &str, names: &[Alias], ) { - let deprecated_names = match module { - "builtins" => BUILTINS, - "io" => IO, - "six" => SIX, - "six.moves" => SIX_MOVES, - "six.moves.builtins" => SIX_MOVES_BUILTINS, - _ => return, - }; - - // Do this with a filter? - let mut unused_imports: Vec<&Alias> = vec![]; - for alias in names { - if alias.asname.is_some() { - continue; - } - if deprecated_names.contains(&alias.name.as_str()) { - unused_imports.push(alias); - } + // Ignore irrelevant modules. + if !matches!( + module, + "builtins" | "io" | "six" | "six.moves" | "six.moves.builtins" + ) { + return; } + // Identify unaliased, builtin imports. + let unused_imports: Vec<&Alias> = names + .iter() + .filter(|alias| alias.asname.is_none()) + .filter(|alias| { + matches!( + (module, alias.name.as_str()), + ( + "builtins" | "six.moves.builtins", + "*" | "ascii" + | "bytes" + | "chr" + | "dict" + | "filter" + | "hex" + | "input" + | "int" + | "isinstance" + | "list" + | "map" + | "max" + | "min" + | "next" + | "object" + | "oct" + | "open" + | "pow" + | "range" + | "round" + | "str" + | "super" + | "zip" + ) | ("io", "open") + | ("six", "callable" | "next") + | ("six.moves", "filter" | "input" | "map" | "range" | "zip") + ) + }) + .collect(); + if unused_imports.is_empty() { return; } @@ -105,8 +122,8 @@ pub(crate) fn unnecessary_builtin_import( ); if checker.patch(diagnostic.kind.rule()) { diagnostic.try_set_fix(|| { - let stmt = checker.semantic_model().stmt(); - let parent = checker.semantic_model().stmt_parent(); + let stmt = checker.semantic().stmt(); + let parent = checker.semantic().stmt_parent(); let unused_imports: Vec = unused_imports .iter() .map(|alias| format!("{module}.{}", alias.name)) @@ -116,8 +133,8 @@ pub(crate) fn unnecessary_builtin_import( stmt, parent, checker.locator, - checker.indexer, checker.stylist, + checker.indexer, )?; Ok(Fix::suggested(edit).isolate(checker.isolation(parent))) }); diff --git a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_class_parentheses.rs b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_class_parentheses.rs new file mode 100644 index 0000000000..69a27b27b4 --- /dev/null +++ b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_class_parentheses.rs @@ -0,0 +1,97 @@ +use std::ops::Add; + +use ruff_text_size::{TextRange, TextSize}; +use rustpython_parser::ast::{self, Ranged}; + +use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; +use ruff_macros::{derive_message_formats, violation}; + +use crate::checkers::ast::Checker; +use crate::registry::AsRule; + +/// ## What it does +/// Checks for class definitions that include unnecessary parentheses after +/// the class name. +/// +/// ## Why is this bad? +/// If a class definition doesn't have any bases, the parentheses are +/// unnecessary. +/// +/// ## Examples +/// ```python +/// class Foo(): +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// class Foo: +/// ... +/// ``` +#[violation] +pub struct UnnecessaryClassParentheses; + +impl AlwaysAutofixableViolation for UnnecessaryClassParentheses { + #[derive_message_formats] + fn message(&self) -> String { + format!("Unnecessary parentheses after class definition") + } + + fn autofix_title(&self) -> String { + "Remove parentheses".to_string() + } +} + +/// UP039 +pub(crate) fn unnecessary_class_parentheses(checker: &mut Checker, class_def: &ast::StmtClassDef) { + if !class_def.bases.is_empty() || !class_def.keywords.is_empty() { + return; + } + + let offset = class_def.name.end(); + let contents = checker.locator.after(offset); + + // Find the open and closing parentheses between the class name and the colon, if they exist. + let mut depth = 0u32; + let mut start = None; + let mut end = None; + for (i, c) in contents.char_indices() { + match c { + '(' => { + if depth == 0 { + start = Some(i); + } + depth = depth.saturating_add(1); + } + ')' => { + depth = depth.saturating_sub(1); + if depth == 0 { + end = Some(i + c.len_utf8()); + } + } + ':' => { + if depth == 0 { + break; + } + } + _ => {} + } + } + let (Some(start), Some(end)) = (start, end) else { + return; + }; + + // Convert to `TextSize`. + let start = TextSize::try_from(start).unwrap(); + let end = TextSize::try_from(end).unwrap(); + + // Add initial offset. + let start = offset.add(start); + let end = offset.add(end); + + let mut diagnostic = Diagnostic::new(UnnecessaryClassParentheses, TextRange::new(start, end)); + if checker.patch(diagnostic.kind.rule()) { + diagnostic.set_fix(Fix::automatic(Edit::deletion(start, end))); + } + checker.diagnostics.push(diagnostic); +} diff --git a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_coding_comment.rs b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_coding_comment.rs index 7b474ab5f0..3b2040bdb9 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_coding_comment.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_coding_comment.rs @@ -5,7 +5,25 @@ use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_whitespace::Line; -// TODO: document referencing [PEP 3120]: https://peps.python.org/pep-3120/ +/// ## What it does +/// Checks for unnecessary UTF-8 encoding declarations. +/// +/// ## Why is this bad? +/// [PEP 3120] makes UTF-8 the default encoding, so a UTF-8 encoding +/// declaration is unnecessary. +/// +/// ## Example +/// ```python +/// # -*- coding: utf-8 -*- +/// print("Hello, world!") +/// ``` +/// +/// Use instead: +/// ```python +/// print("Hello, world!") +/// ``` +/// +/// [PEP 3120]: https://peps.python.org/pep-3120/ #[violation] pub struct UTF8EncodingDeclaration; @@ -30,8 +48,7 @@ pub(crate) fn unnecessary_coding_comment(line: &Line, autofix: bool) -> Option Option<&Expr> { value: variable, attr, .. - }) = func else { + }) = func + else { return None; }; if attr != "encode" { @@ -124,8 +144,8 @@ fn replace_with_bytes_literal(locator: &Locator, expr: &Expr) -> Fix { } prev = range.end(); } - #[allow(deprecated)] - Fix::unspecified(Edit::range_replacement(replacement, expr.range())) + + Fix::automatic(Edit::range_replacement(replacement, expr.range())) } /// UP012 @@ -168,16 +188,16 @@ pub(crate) fn unnecessary_encode_utf8( expr.range(), ); if checker.patch(Rule::UnnecessaryEncodeUTF8) { - #[allow(deprecated)] - diagnostic.try_set_fix_from_edit(|| { + diagnostic.try_set_fix(|| { remove_argument( checker.locator, - func.start(), + func.end(), kwarg.range(), args, kwargs, false, ) + .map(Fix::automatic) }); } checker.diagnostics.push(diagnostic); @@ -190,16 +210,16 @@ pub(crate) fn unnecessary_encode_utf8( expr.range(), ); if checker.patch(Rule::UnnecessaryEncodeUTF8) { - #[allow(deprecated)] - diagnostic.try_set_fix_from_edit(|| { + diagnostic.try_set_fix(|| { remove_argument( checker.locator, - func.start(), + func.end(), arg.range(), args, kwargs, false, ) + .map(Fix::automatic) }); } checker.diagnostics.push(diagnostic); @@ -219,16 +239,16 @@ pub(crate) fn unnecessary_encode_utf8( expr.range(), ); if checker.patch(Rule::UnnecessaryEncodeUTF8) { - #[allow(deprecated)] - diagnostic.try_set_fix_from_edit(|| { + diagnostic.try_set_fix(|| { remove_argument( checker.locator, - func.start(), + func.end(), kwarg.range(), args, kwargs, false, ) + .map(Fix::automatic) }); } checker.diagnostics.push(diagnostic); @@ -241,16 +261,16 @@ pub(crate) fn unnecessary_encode_utf8( expr.range(), ); if checker.patch(Rule::UnnecessaryEncodeUTF8) { - #[allow(deprecated)] - diagnostic.try_set_fix_from_edit(|| { + diagnostic.try_set_fix(|| { remove_argument( checker.locator, - func.start(), + func.end(), arg.range(), args, kwargs, false, ) + .map(Fix::automatic) }); } checker.diagnostics.push(diagnostic); diff --git a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_future_import.rs b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_future_import.rs index 34cd3e5ca5..14894f2626 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_future_import.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_future_import.rs @@ -8,6 +8,33 @@ use crate::autofix; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Checks for unnecessary `__future__` imports. +/// +/// ## Why is this bad? +/// The `__future__` module is used to enable features that are not yet +/// available in the current Python version. If a feature is already +/// available in the minimum supported Python version, importing it +/// from `__future__` is unnecessary and should be removed to avoid +/// confusion. +/// +/// ## Example +/// ```python +/// from __future__ import print_function +/// +/// print("Hello, world!") +/// ``` +/// +/// Use instead: +/// ```python +/// print("Hello, world!") +/// ``` +/// +/// ## Options +/// - `target-version` +/// +/// ## References +/// - [Python documentation: `__future__` — Future statement definitions](https://docs.python.org/3/library/__future__.html) #[violation] pub struct UnnecessaryFutureImport { pub names: Vec, @@ -88,15 +115,15 @@ pub(crate) fn unnecessary_future_import(checker: &mut Checker, stmt: &Stmt, name .iter() .map(|alias| format!("__future__.{}", alias.name)) .collect(); - let stmt = checker.semantic_model().stmt(); - let parent = checker.semantic_model().stmt_parent(); + let stmt = checker.semantic().stmt(); + let parent = checker.semantic().stmt_parent(); let edit = autofix::edits::remove_unused_imports( unused_imports.iter().map(String::as_str), stmt, parent, checker.locator, - checker.indexer, checker.stylist, + checker.indexer, )?; Ok(Fix::suggested(edit).isolate(checker.isolation(parent))) }); diff --git a/crates/ruff/src/rules/pyupgrade/rules/unpacked_list_comprehension.rs b/crates/ruff/src/rules/pyupgrade/rules/unpacked_list_comprehension.rs index 3ff0a14ffb..193fe237a2 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/unpacked_list_comprehension.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/unpacked_list_comprehension.rs @@ -2,10 +2,32 @@ use rustpython_parser::ast::{self, Expr, Ranged}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers::any_over_expr; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Checks for list comprehensions that are immediately unpacked. +/// +/// ## Why is this bad? +/// There is no reason to use a list comprehension if the result is immediately +/// unpacked. Instead, use a generator expression, which is more efficient as +/// it avoids allocating an intermediary list. +/// +/// ## Example +/// ```python +/// a, b, c = [foo(x) for x in items] +/// ``` +/// +/// Use instead: +/// ```python +/// a, b, c = (foo(x) for x in items) +/// ``` +/// +/// ## References +/// - [Python documentation: Generator expressions](https://docs.python.org/3/reference/expressions.html#generator-expressions) +/// - [Python documentation: List comprehensions](https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions) #[violation] pub struct UnpackedListComprehension; @@ -20,126 +42,46 @@ impl AlwaysAutofixableViolation for UnpackedListComprehension { } } -/// Returns `true` if `expr` contains an `Expr::Await`. -fn contains_await(expr: &Expr) -> bool { - match expr { - Expr::Await(_) => true, - Expr::BoolOp(ast::ExprBoolOp { values, .. }) => values.iter().any(contains_await), - Expr::NamedExpr(ast::ExprNamedExpr { - target, - value, - range: _, - }) => contains_await(target) || contains_await(value), - Expr::BinOp(ast::ExprBinOp { left, right, .. }) => { - contains_await(left) || contains_await(right) - } - Expr::UnaryOp(ast::ExprUnaryOp { operand, .. }) => contains_await(operand), - Expr::Lambda(ast::ExprLambda { body, .. }) => contains_await(body), - Expr::IfExp(ast::ExprIfExp { - test, - body, - orelse, - range: _, - }) => contains_await(test) || contains_await(body) || contains_await(orelse), - Expr::Dict(ast::ExprDict { - keys, - values, - range: _, - }) => keys - .iter() - .flatten() - .chain(values.iter()) - .any(contains_await), - Expr::Set(ast::ExprSet { elts, range: _ }) => elts.iter().any(contains_await), - Expr::ListComp(ast::ExprListComp { elt, .. }) => contains_await(elt), - Expr::SetComp(ast::ExprSetComp { elt, .. }) => contains_await(elt), - Expr::DictComp(ast::ExprDictComp { key, value, .. }) => { - contains_await(key) || contains_await(value) - } - Expr::GeneratorExp(ast::ExprGeneratorExp { elt, .. }) => contains_await(elt), - Expr::Yield(ast::ExprYield { value, range: _ }) => { - value.as_ref().map_or(false, |value| contains_await(value)) - } - Expr::YieldFrom(ast::ExprYieldFrom { value, range: _ }) => contains_await(value), - Expr::Compare(ast::ExprCompare { - left, comparators, .. - }) => contains_await(left) || comparators.iter().any(contains_await), - Expr::Call(ast::ExprCall { - func, - args, - keywords, - range: _, - }) => { - contains_await(func) - || args.iter().any(contains_await) - || keywords - .iter() - .any(|keyword| contains_await(&keyword.value)) - } - Expr::FormattedValue(ast::ExprFormattedValue { - value, format_spec, .. - }) => { - contains_await(value) - || format_spec - .as_ref() - .map_or(false, |value| contains_await(value)) - } - Expr::JoinedStr(ast::ExprJoinedStr { values, range: _ }) => { - values.iter().any(contains_await) - } - Expr::Constant(_) => false, - Expr::Attribute(ast::ExprAttribute { value, .. }) => contains_await(value), - Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => { - contains_await(value) || contains_await(slice) - } - Expr::Starred(ast::ExprStarred { value, .. }) => contains_await(value), - Expr::Name(_) => false, - Expr::List(ast::ExprList { elts, .. }) => elts.iter().any(contains_await), - Expr::Tuple(ast::ExprTuple { elts, .. }) => elts.iter().any(contains_await), - Expr::Slice(ast::ExprSlice { - lower, - upper, - step, - range: _, - }) => { - lower.as_ref().map_or(false, |value| contains_await(value)) - || upper.as_ref().map_or(false, |value| contains_await(value)) - || step.as_ref().map_or(false, |value| contains_await(value)) - } - } -} - /// UP027 pub(crate) fn unpacked_list_comprehension(checker: &mut Checker, targets: &[Expr], value: &Expr) { let Some(target) = targets.get(0) else { return; }; - if let Expr::Tuple(_) = target { - if let Expr::ListComp(ast::ExprListComp { - elt, - generators, - range: _, - }) = value - { - if generators.iter().any(|generator| generator.is_async) || contains_await(elt) { - return; - } - let mut diagnostic = Diagnostic::new(UnpackedListComprehension, value.range()); - if checker.patch(diagnostic.kind.rule()) { - let existing = checker.locator.slice(value.range()); - - let mut content = String::with_capacity(existing.len()); - content.push('('); - content.push_str(&existing[1..existing.len() - 1]); - content.push(')'); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( - content, - value.range(), - ))); - } - checker.diagnostics.push(diagnostic); - } + if !target.is_tuple_expr() { + return; } + + let Expr::ListComp(ast::ExprListComp { + elt, + generators, + range: _, + }) = value + else { + return; + }; + + if generators.iter().any(|generator| generator.is_async) || contains_await(elt) { + return; + } + + let mut diagnostic = Diagnostic::new(UnpackedListComprehension, value.range()); + if checker.patch(diagnostic.kind.rule()) { + let existing = checker.locator.slice(value.range()); + + let mut content = String::with_capacity(existing.len()); + content.push('('); + content.push_str(&existing[1..existing.len() - 1]); + content.push(')'); + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( + content, + value.range(), + ))); + } + checker.diagnostics.push(diagnostic); +} + +/// Return `true` if the [`Expr`] contains an `await` expression. +fn contains_await(expr: &Expr) -> bool { + any_over_expr(expr, &Expr::is_await_expr) } diff --git a/crates/ruff/src/rules/pyupgrade/rules/use_pep585_annotation.rs b/crates/ruff/src/rules/pyupgrade/rules/use_pep585_annotation.rs index 20e09a7528..b5ceee9e8a 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/use_pep585_annotation.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/use_pep585_annotation.rs @@ -9,6 +9,36 @@ use crate::checkers::ast::Checker; use crate::importer::ImportRequest; use crate::registry::AsRule; +/// ## What it does +/// Checks for the use of generics that can be replaced with standard library +/// variants based on [PEP 585]. +/// +/// ## Why is this bad? +/// [PEP 585] enabled collections in the Python standard library (like `list`) +/// to be used as generics directly, instead of importing analogous members +/// from the `typing` module (like `typing.List`). +/// +/// When available, the [PEP 585] syntax should be used instead of importing +/// members from the `typing` module, as it's more concise and readable. +/// Importing those members from `typing` is considered deprecated as of PEP +/// 585. +/// +/// ## Example +/// ```python +/// from typing import List +/// +/// foo: List[int] = [1, 2, 3] +/// ``` +/// +/// Use instead: +/// ```python +/// foo: list[int] = [1, 2, 3] +/// ``` +/// +/// ## Options +/// - `target-version` +/// +/// [PEP 585]: https://peps.python.org/pep-0585/ #[violation] pub struct NonPEP585Annotation { from: String, @@ -47,11 +77,11 @@ pub(crate) fn use_pep585_annotation( expr.range(), ); if checker.patch(diagnostic.kind.rule()) { - if !checker.semantic_model().in_complex_string_type_definition() { + if !checker.semantic().in_complex_string_type_definition() { match replacement { ModuleMember::BuiltIn(name) => { // Built-in type, like `list`. - if checker.semantic_model().is_builtin(name) { + if checker.semantic().is_builtin(name) { diagnostic.set_fix(Fix::automatic(Edit::range_replacement( (*name).to_string(), expr.range(), @@ -64,7 +94,7 @@ pub(crate) fn use_pep585_annotation( let (import_edit, binding) = checker.importer.get_or_import_symbol( &ImportRequest::import_from(module, member), expr.start(), - checker.semantic_model(), + checker.semantic(), )?; let reference_edit = Edit::range_replacement(binding, expr.range()); Ok(Fix::suggested_edits(import_edit, [reference_edit])) diff --git a/crates/ruff/src/rules/pyupgrade/rules/use_pep604_annotation.rs b/crates/ruff/src/rules/pyupgrade/rules/use_pep604_annotation.rs index 789e89b685..2f2fea0424 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/use_pep604_annotation.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/use_pep604_annotation.rs @@ -1,13 +1,39 @@ -use ruff_text_size::TextRange; -use rustpython_parser::ast::{self, Constant, Expr, Operator, Ranged}; +use itertools::Either::{Left, Right}; +use itertools::Itertools; +use rustpython_parser::ast::{self, Expr, Ranged}; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::source_code::Locator; use ruff_python_semantic::analyze::typing::Pep604Operator; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Check for type annotations that can be rewritten based on [PEP 604] syntax. +/// +/// ## Why is this bad? +/// [PEP 604] introduced a new syntax for union type annotations based on the +/// `|` operator. This syntax is more concise and readable than the previous +/// `typing.Union` and `typing.Optional` syntaxes. +/// +/// ## Example +/// ```python +/// from typing import Union +/// +/// foo: Union[int, str] = 1 +/// ``` +/// +/// Use instead: +/// ```python +/// foo: int | str = 1 +/// ``` +/// +/// ## Options +/// - `target-version` +/// +/// [PEP 604]: https://peps.python.org/pep-0604/ #[violation] pub struct NonPEP604Annotation; @@ -24,32 +50,6 @@ impl Violation for NonPEP604Annotation { } } -fn optional(expr: &Expr) -> Expr { - Expr::BinOp(ast::ExprBinOp { - left: Box::new(expr.clone()), - op: Operator::BitOr, - right: Box::new(Expr::Constant(ast::ExprConstant { - value: Constant::None, - kind: None, - range: TextRange::default(), - })), - range: TextRange::default(), - }) -} - -fn union(elts: &[Expr]) -> Expr { - if elts.len() == 1 { - elts[0].clone() - } else { - Expr::BinOp(ast::ExprBinOp { - left: Box::new(union(&elts[..elts.len() - 1])), - op: Operator::BitOr, - right: Box::new(elts[elts.len() - 1].clone()), - range: TextRange::default(), - }) - } -} - /// UP007 pub(crate) fn use_pep604_annotation( checker: &mut Checker, @@ -58,15 +58,15 @@ pub(crate) fn use_pep604_annotation( operator: Pep604Operator, ) { // Avoid fixing forward references, or types not in an annotation. - let fixable = checker.semantic_model().in_type_definition() - && !checker.semantic_model().in_complex_string_type_definition(); + let fixable = checker.semantic().in_type_definition() + && !checker.semantic().in_complex_string_type_definition(); + match operator { Pep604Operator::Optional => { let mut diagnostic = Diagnostic::new(NonPEP604Annotation, expr.range()); if fixable && checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( - checker.generator().expr(&optional(slice)), + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( + optional(slice, checker.locator), expr.range(), ))); } @@ -80,17 +80,15 @@ pub(crate) fn use_pep604_annotation( // Invalid type annotation. } Expr::Tuple(ast::ExprTuple { elts, .. }) => { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( - checker.generator().expr(&union(elts)), + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( + union(elts, checker.locator), expr.range(), ))); } _ => { // Single argument. - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( - checker.generator().expr(slice), + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( + checker.locator.slice(slice.range()).to_string(), expr.range(), ))); } @@ -100,3 +98,24 @@ pub(crate) fn use_pep604_annotation( } } } + +/// Format the expression as a PEP 604-style optional. +fn optional(expr: &Expr, locator: &Locator) -> String { + format!("{} | None", locator.slice(expr.range())) +} + +/// Format the expressions as a PEP 604-style union. +fn union(elts: &[Expr], locator: &Locator) -> String { + let mut elts = elts + .iter() + .flat_map(|expr| match expr { + Expr::Tuple(ast::ExprTuple { elts, .. }) => Left(elts.iter()), + _ => Right(std::iter::once(expr)), + }) + .peekable(); + if elts.peek().is_none() { + "()".to_string() + } else { + elts.map(|expr| locator.slice(expr.range())).join(" | ") + } +} diff --git a/crates/ruff/src/rules/pyupgrade/rules/use_pep604_isinstance.rs b/crates/ruff/src/rules/pyupgrade/rules/use_pep604_isinstance.rs index d0f8baaf92..7ffa04286d 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/use_pep604_isinstance.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/use_pep604_isinstance.rs @@ -34,6 +34,33 @@ impl CallKind { } } +/// ## What it does +/// Checks for uses of `isinstance` and `issubclass` that take a tuple +/// of types for comparison. +/// +/// ## Why is this bad? +/// Since Python 3.10, `isinstance` and `issubclass` can be passed a +/// `|`-separated union of types, which is more concise and consistent +/// with the union operator introduced in [PEP 604]. +/// +/// ## Example +/// ```python +/// isinstance(x, (int, float)) +/// ``` +/// +/// Use instead: +/// ```python +/// isinstance(x, int | float) +/// ``` +/// +/// ## Options +/// - `target-version` +/// +/// ## References +/// - [Python documentation: `isinstance`](https://docs.python.org/3/library/functions.html#isinstance) +/// - [Python documentation: `issubclass`](https://docs.python.org/3/library/functions.html#issubclass) +/// +/// [PEP 604]: https://peps.python.org/pep-0604/ #[violation] pub struct NonPEP604Isinstance { kind: CallKind, @@ -74,7 +101,7 @@ pub(crate) fn use_pep604_isinstance( let Some(kind) = CallKind::from_name(id) else { return; }; - if !checker.semantic_model().is_builtin(id) { + if !checker.semantic().is_builtin(id) { return; }; if let Some(types) = args.get(1) { @@ -85,14 +112,13 @@ pub(crate) fn use_pep604_isinstance( } // Ex) `(*args,)` - if elts.iter().any(|elt| matches!(elt, Expr::Starred(_))) { + if elts.iter().any(Expr::is_starred_expr) { return; } let mut diagnostic = Diagnostic::new(NonPEP604Isinstance { kind }, expr.range()); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( checker.generator().expr(&union(elts)), types.range(), ))); diff --git a/crates/ruff/src/rules/pyupgrade/rules/useless_metaclass_type.rs b/crates/ruff/src/rules/pyupgrade/rules/useless_metaclass_type.rs index 783bc520c8..286c4116f1 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/useless_metaclass_type.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/useless_metaclass_type.rs @@ -7,6 +7,26 @@ use crate::autofix; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Checks for the use of `__metaclass__ = type` in class definitions. +/// +/// ## Why is this bad? +/// Since Python 3, `__metaclass__ = type` is implied and can thus be omitted. +/// +/// ## Example +/// ```python +/// class Foo: +/// __metaclass__ = type +/// ``` +/// +/// Use instead: +/// ```python +/// class Foo: +/// ... +/// ``` +/// +/// ## References +/// - [PEP 3115](https://www.python.org/dev/peps/pep-3115/) #[violation] pub struct UselessMetaclassType; @@ -32,13 +52,13 @@ pub(crate) fn useless_metaclass_type( return; } let Expr::Name(ast::ExprName { id, .. }) = targets.first().unwrap() else { - return ; + return; }; if id != "__metaclass__" { return; } let Expr::Name(ast::ExprName { id, .. }) = value else { - return ; + return; }; if id != "type" { return; @@ -46,15 +66,9 @@ pub(crate) fn useless_metaclass_type( let mut diagnostic = Diagnostic::new(UselessMetaclassType, stmt.range()); if checker.patch(diagnostic.kind.rule()) { - let stmt = checker.semantic_model().stmt(); - let parent = checker.semantic_model().stmt_parent(); - let edit = autofix::edits::delete_stmt( - stmt, - parent, - checker.locator, - checker.indexer, - checker.stylist, - ); + let stmt = checker.semantic().stmt(); + let parent = checker.semantic().stmt_parent(); + let edit = autofix::edits::delete_stmt(stmt, parent, checker.locator, checker.indexer); diagnostic.set_fix(Fix::automatic(edit).isolate(checker.isolation(parent))); } checker.diagnostics.push(diagnostic); diff --git a/crates/ruff/src/rules/pyupgrade/rules/useless_object_inheritance.rs b/crates/ruff/src/rules/pyupgrade/rules/useless_object_inheritance.rs index 8a3bc3839d..b3ac4c2899 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/useless_object_inheritance.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/useless_object_inheritance.rs @@ -1,14 +1,33 @@ -use rustpython_parser::ast::{self, Expr, Keyword, Ranged, Stmt}; +use rustpython_parser::ast::{self, Expr, Ranged}; -use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic}; +use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::binding::{Binding, BindingKind, Bindings}; -use ruff_python_semantic::scope::Scope; use crate::autofix::edits::remove_argument; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Checks for classes that inherit from `object`. +/// +/// ## Why is this bad? +/// Since Python 3, all classes inherit from `object` by default, so `object` can +/// be omitted from the list of base classes. +/// +/// ## Example +/// ```python +/// class Foo(object): +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// class Foo: +/// ... +/// ``` +/// +/// ## References +/// - [PEP 3115](https://www.python.org/dev/peps/pep-3115/) #[violation] pub struct UselessObjectInheritance { name: String, @@ -26,62 +45,36 @@ impl AlwaysAutofixableViolation for UselessObjectInheritance { } } -fn rule(name: &str, bases: &[Expr], scope: &Scope, bindings: &Bindings) -> Option { - for expr in bases { +/// UP004 +pub(crate) fn useless_object_inheritance(checker: &mut Checker, class_def: &ast::StmtClassDef) { + for expr in &class_def.bases { let Expr::Name(ast::ExprName { id, .. }) = expr else { continue; }; if id != "object" { continue; } - if !matches!( - scope - .get(id.as_str()) - .map(|binding_id| &bindings[binding_id]), - None | Some(Binding { - kind: BindingKind::Builtin, - .. - }) - ) { + if !checker.semantic().is_builtin("object") { continue; } - return Some(Diagnostic::new( + + let mut diagnostic = Diagnostic::new( UselessObjectInheritance { - name: name.to_string(), + name: class_def.name.to_string(), }, expr.range(), - )); - } - - None -} - -/// UP004 -pub(crate) fn useless_object_inheritance( - checker: &mut Checker, - stmt: &Stmt, - name: &str, - bases: &[Expr], - keywords: &[Keyword], -) { - if let Some(mut diagnostic) = rule( - name, - bases, - checker.semantic_model().scope(), - &checker.semantic_model().bindings, - ) { + ); if checker.patch(diagnostic.kind.rule()) { - let expr_range = diagnostic.range(); - #[allow(deprecated)] - diagnostic.try_set_fix_from_edit(|| { - remove_argument( + diagnostic.try_set_fix(|| { + let edit = remove_argument( checker.locator, - stmt.start(), - expr_range, - bases, - keywords, + class_def.name.end(), + expr.range(), + &class_def.bases, + &class_def.keywords, true, - ) + )?; + Ok(Fix::automatic(edit)) }); } checker.diagnostics.push(diagnostic); diff --git a/crates/ruff/src/rules/pyupgrade/rules/yield_in_for_loop.rs b/crates/ruff/src/rules/pyupgrade/rules/yield_in_for_loop.rs index 999df0fd04..e41becee05 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/yield_in_for_loop.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/yield_in_for_loop.rs @@ -11,6 +11,27 @@ use ruff_python_ast::{statement_visitor, visitor}; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Checks for `for` loops that can be replaced with `yield from` expressions. +/// +/// ## Why is this bad? +/// If a `for` loop only contains a `yield` statement, it can be replaced with a +/// `yield from` expression, which is more concise and idiomatic. +/// +/// ## Example +/// ```python +/// for x in foo: +/// yield x +/// ``` +/// +/// Use instead: +/// ```python +/// yield from foo +/// ``` +/// +/// ## References +/// - [Python documentation: The `yield` statement](https://docs.python.org/3/reference/simple_stmts.html#the-yield-statement) +/// - [PEP 380](https://peps.python.org/pep-0380/) #[violation] pub struct YieldInForLoop; @@ -169,8 +190,7 @@ pub(crate) fn yield_in_for_loop(checker: &mut Checker, stmt: &Stmt) { if checker.patch(diagnostic.kind.rule()) { let contents = checker.locator.slice(item.iter.range()); let contents = format!("yield from {contents}"); - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( contents, item.stmt.range(), ))); diff --git a/crates/ruff/src/rules/pyupgrade/settings.rs b/crates/ruff/src/rules/pyupgrade/settings.rs new file mode 100644 index 0000000000..a5b2d78188 --- /dev/null +++ b/crates/ruff/src/rules/pyupgrade/settings.rs @@ -0,0 +1,76 @@ +//! Settings for the `pyupgrade` plugin. + +use ruff_macros::{CacheKey, CombineOptions, ConfigurationOptions}; +use serde::{Deserialize, Serialize}; + +#[derive( + Debug, PartialEq, Eq, Serialize, Deserialize, Default, ConfigurationOptions, CombineOptions, +)] +#[serde( + deny_unknown_fields, + rename_all = "kebab-case", + rename = "PyUpgradeOptions" +)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub struct Options { + #[option( + default = r#"false"#, + value_type = "bool", + example = r#" + # Preserve types, even if a file imports `from __future__ import annotations`. + keep-runtime-typing = true + "# + )] + /// Whether to avoid PEP 585 (`List[int]` -> `list[int]`) and PEP 604 + /// (`Union[str, int]` -> `str | int`) rewrites even if a file imports + /// `from __future__ import annotations`. + /// + /// This setting is only applicable when the target Python version is below + /// 3.9 and 3.10 respectively, and is most commonly used when working with + /// libraries like Pydantic and FastAPI, which rely on the ability to parse + /// type annotations at runtime. The use of `from __future__ import annotations` + /// causes Python to treat the type annotations as strings, which typically + /// allows for the use of language features that appear in later Python + /// versions but are not yet supported by the current version (e.g., `str | + /// int`). However, libraries that rely on runtime type annotations will + /// break if the annotations are incompatible with the current Python + /// version. + /// + /// For example, while the following is valid Python 3.8 code due to the + /// presence of `from __future__ import annotations`, the use of `str| int` + /// prior to Python 3.10 will cause Pydantic to raise a `TypeError` at + /// runtime: + /// + /// ```python + /// from __future__ import annotations + /// + /// import pydantic + /// + /// class Foo(pydantic.BaseModel): + /// bar: str | int + /// ``` + /// + /// + pub keep_runtime_typing: Option, +} + +#[derive(Debug, Default, CacheKey)] +pub struct Settings { + pub keep_runtime_typing: bool, +} + +impl From for Settings { + fn from(options: Options) -> Self { + Self { + keep_runtime_typing: options.keep_runtime_typing.unwrap_or_default(), + } + } +} + +impl From for Options { + fn from(settings: Settings) -> Self { + Self { + keep_runtime_typing: Some(settings.keep_runtime_typing), + } + } +} diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP003.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP003.py.snap index 5d7d50e18e..c461f5d92c 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP003.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP003.py.snap @@ -10,7 +10,7 @@ UP003.py:1:1: UP003 [*] Use `str` instead of `type(...)` | = help: Replace `type(...)` with `str` -ℹ Suggested fix +ℹ Fix 1 |-type("") 1 |+str 2 2 | type(b"") @@ -27,7 +27,7 @@ UP003.py:2:1: UP003 [*] Use `bytes` instead of `type(...)` | = help: Replace `type(...)` with `bytes` -ℹ Suggested fix +ℹ Fix 1 1 | type("") 2 |-type(b"") 2 |+bytes @@ -46,7 +46,7 @@ UP003.py:3:1: UP003 [*] Use `int` instead of `type(...)` | = help: Replace `type(...)` with `int` -ℹ Suggested fix +ℹ Fix 1 1 | type("") 2 2 | type(b"") 3 |-type(0) @@ -65,7 +65,7 @@ UP003.py:4:1: UP003 [*] Use `float` instead of `type(...)` | = help: Replace `type(...)` with `float` -ℹ Suggested fix +ℹ Fix 1 1 | type("") 2 2 | type(b"") 3 3 | type(0) @@ -86,7 +86,7 @@ UP003.py:5:1: UP003 [*] Use `complex` instead of `type(...)` | = help: Replace `type(...)` with `complex` -ℹ Suggested fix +ℹ Fix 2 2 | type(b"") 3 3 | type(0) 4 4 | type(0.0) diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP004.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP004.py.snap index f480d6b0df..e106675943 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP004.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP004.py.snap @@ -9,7 +9,7 @@ UP004.py:5:9: UP004 [*] Class `A` inherits from `object` | = help: Remove `object` inheritance -ℹ Suggested fix +ℹ Fix 2 2 | ... 3 3 | 4 4 | @@ -29,7 +29,7 @@ UP004.py:10:5: UP004 [*] Class `A` inherits from `object` | = help: Remove `object` inheritance -ℹ Suggested fix +ℹ Fix 6 6 | ... 7 7 | 8 8 | @@ -51,7 +51,7 @@ UP004.py:16:5: UP004 [*] Class `A` inherits from `object` | = help: Remove `object` inheritance -ℹ Suggested fix +ℹ Fix 12 12 | ... 13 13 | 14 14 | @@ -75,7 +75,7 @@ UP004.py:24:5: UP004 [*] Class `A` inherits from `object` | = help: Remove `object` inheritance -ℹ Suggested fix +ℹ Fix 19 19 | ... 20 20 | 21 21 | @@ -99,7 +99,7 @@ UP004.py:31:5: UP004 [*] Class `A` inherits from `object` | = help: Remove `object` inheritance -ℹ Suggested fix +ℹ Fix 26 26 | ... 27 27 | 28 28 | @@ -122,7 +122,7 @@ UP004.py:37:5: UP004 [*] Class `A` inherits from `object` | = help: Remove `object` inheritance -ℹ Suggested fix +ℹ Fix 33 33 | ... 34 34 | 35 35 | @@ -146,7 +146,7 @@ UP004.py:45:5: UP004 [*] Class `A` inherits from `object` | = help: Remove `object` inheritance -ℹ Suggested fix +ℹ Fix 40 40 | ... 41 41 | 42 42 | @@ -171,7 +171,7 @@ UP004.py:53:5: UP004 [*] Class `A` inherits from `object` | = help: Remove `object` inheritance -ℹ Suggested fix +ℹ Fix 48 48 | ... 49 49 | 50 50 | @@ -196,7 +196,7 @@ UP004.py:61:5: UP004 [*] Class `A` inherits from `object` | = help: Remove `object` inheritance -ℹ Suggested fix +ℹ Fix 56 56 | ... 57 57 | 58 58 | @@ -221,7 +221,7 @@ UP004.py:69:5: UP004 [*] Class `A` inherits from `object` | = help: Remove `object` inheritance -ℹ Suggested fix +ℹ Fix 64 64 | ... 65 65 | 66 66 | @@ -243,7 +243,7 @@ UP004.py:75:12: UP004 [*] Class `B` inherits from `object` | = help: Remove `object` inheritance -ℹ Suggested fix +ℹ Fix 72 72 | ... 73 73 | 74 74 | @@ -261,7 +261,7 @@ UP004.py:79:9: UP004 [*] Class `B` inherits from `object` | = help: Remove `object` inheritance -ℹ Suggested fix +ℹ Fix 76 76 | ... 77 77 | 78 78 | @@ -281,7 +281,7 @@ UP004.py:84:5: UP004 [*] Class `B` inherits from `object` | = help: Remove `object` inheritance -ℹ Suggested fix +ℹ Fix 81 81 | 82 82 | 83 83 | class B( @@ -301,7 +301,7 @@ UP004.py:92:5: UP004 [*] Class `B` inherits from `object` | = help: Remove `object` inheritance -ℹ Suggested fix +ℹ Fix 89 89 | 90 90 | class B( 91 91 | A, @@ -320,7 +320,7 @@ UP004.py:98:5: UP004 [*] Class `B` inherits from `object` | = help: Remove `object` inheritance -ℹ Suggested fix +ℹ Fix 95 95 | 96 96 | 97 97 | class B( @@ -340,7 +340,7 @@ UP004.py:108:5: UP004 [*] Class `B` inherits from `object` | = help: Remove `object` inheritance -ℹ Suggested fix +ℹ Fix 105 105 | class B( 106 106 | # Comment on A. 107 107 | A, @@ -349,25 +349,6 @@ UP004.py:108:5: UP004 [*] Class `B` inherits from `object` 110 109 | ... 111 110 | -UP004.py:114:13: UP004 [*] Class `A` inherits from `object` - | -113 | def f(): -114 | class A(object): - | ^^^^^^ UP004 -115 | ... - | - = help: Remove `object` inheritance - -ℹ Suggested fix -111 111 | -112 112 | -113 113 | def f(): -114 |- class A(object): - 114 |+ class A: -115 115 | ... -116 116 | -117 117 | - UP004.py:119:5: UP004 [*] Class `A` inherits from `object` | 118 | class A( @@ -378,7 +359,7 @@ UP004.py:119:5: UP004 [*] Class `A` inherits from `object` | = help: Remove `object` inheritance -ℹ Suggested fix +ℹ Fix 115 115 | ... 116 116 | 117 117 | @@ -400,7 +381,7 @@ UP004.py:125:5: UP004 [*] Class `A` inherits from `object` | = help: Remove `object` inheritance -ℹ Suggested fix +ℹ Fix 121 121 | ... 122 122 | 123 123 | @@ -422,7 +403,7 @@ UP004.py:131:5: UP004 [*] Class `A` inherits from `object` | = help: Remove `object` inheritance -ℹ Suggested fix +ℹ Fix 127 127 | ... 128 128 | 129 129 | @@ -435,4 +416,78 @@ UP004.py:131:5: UP004 [*] Class `A` inherits from `object` 135 132 | 136 133 | +UP004.py:137:9: UP004 [*] Class `A` inherits from `object` + | +137 | class A(object, object): + | ^^^^^^ UP004 +138 | ... + | + = help: Remove `object` inheritance + +ℹ Fix +134 134 | ... +135 135 | +136 136 | +137 |-class A(object, object): + 137 |+class A(object): +138 138 | ... +139 139 | +140 140 | + +UP004.py:137:17: UP004 [*] Class `A` inherits from `object` + | +137 | class A(object, object): + | ^^^^^^ UP004 +138 | ... + | + = help: Remove `object` inheritance + +ℹ Fix +134 134 | ... +135 135 | +136 136 | +137 |-class A(object, object): + 137 |+class A(object): +138 138 | ... +139 139 | +140 140 | + +UP004.py:142:9: UP004 [*] Class `A` inherits from `object` + | +141 | @decorator() +142 | class A(object): + | ^^^^^^ UP004 +143 | ... + | + = help: Remove `object` inheritance + +ℹ Fix +139 139 | +140 140 | +141 141 | @decorator() +142 |-class A(object): + 142 |+class A: +143 143 | ... +144 144 | +145 145 | @decorator() # class A(object): + +UP004.py:146:9: UP004 [*] Class `A` inherits from `object` + | +145 | @decorator() # class A(object): +146 | class A(object): + | ^^^^^^ UP004 +147 | ... + | + = help: Remove `object` inheritance + +ℹ Fix +143 143 | ... +144 144 | +145 145 | @decorator() # class A(object): +146 |-class A(object): + 146 |+class A: +147 147 | ... +148 148 | +149 149 | + diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP007.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP007.py.snap index 4f87a31ecf..250930ff96 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP007.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP007.py.snap @@ -122,15 +122,15 @@ UP007.py:26:10: UP007 [*] Use `X | Y` for type annotations 24 24 | 25 25 | 26 |-def f(x: typing.Union[(str, int), float]) -> None: - 26 |+def f(x: (str, int) | float) -> None: + 26 |+def f(x: str | int | float) -> None: 27 27 | ... 28 28 | 29 29 | -UP007.py:30:11: UP007 [*] Use `X | Y` for type annotations +UP007.py:30:10: UP007 [*] Use `X | Y` for type annotations | -30 | def f(x: "Union[str, int, Union[float, bytes]]") -> None: - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP007 +30 | def f(x: typing.Union[(int,)]) -> None: + | ^^^^^^^^^^^^^^^^^^^^ UP007 31 | ... | = help: Convert to `X | Y` @@ -139,34 +139,16 @@ UP007.py:30:11: UP007 [*] Use `X | Y` for type annotations 27 27 | ... 28 28 | 29 29 | -30 |-def f(x: "Union[str, int, Union[float, bytes]]") -> None: - 30 |+def f(x: "str | int | Union[float, bytes]") -> None: +30 |-def f(x: typing.Union[(int,)]) -> None: + 30 |+def f(x: int) -> None: 31 31 | ... 32 32 | 33 33 | -UP007.py:30:27: UP007 [*] Use `X | Y` for type annotations +UP007.py:34:10: UP007 [*] Use `X | Y` for type annotations | -30 | def f(x: "Union[str, int, Union[float, bytes]]") -> None: - | ^^^^^^^^^^^^^^^^^^^ UP007 -31 | ... - | - = help: Convert to `X | Y` - -ℹ Suggested fix -27 27 | ... -28 28 | -29 29 | -30 |-def f(x: "Union[str, int, Union[float, bytes]]") -> None: - 30 |+def f(x: "Union[str, int, float | bytes]") -> None: -31 31 | ... -32 32 | -33 33 | - -UP007.py:34:11: UP007 [*] Use `X | Y` for type annotations - | -34 | def f(x: "typing.Union[str, int]") -> None: - | ^^^^^^^^^^^^^^^^^^^^^^ UP007 +34 | def f(x: typing.Union[()]) -> None: + | ^^^^^^^^^^^^^^^^ UP007 35 | ... | = help: Convert to `X | Y` @@ -175,69 +157,139 @@ UP007.py:34:11: UP007 [*] Use `X | Y` for type annotations 31 31 | ... 32 32 | 33 33 | -34 |-def f(x: "typing.Union[str, int]") -> None: - 34 |+def f(x: "str | int") -> None: +34 |-def f(x: typing.Union[()]) -> None: + 34 |+def f(x: ()) -> None: 35 35 | ... 36 36 | 37 37 | -UP007.py:47:8: UP007 [*] Use `X | Y` for type annotations +UP007.py:38:11: UP007 [*] Use `X | Y` for type annotations | -46 | def f() -> None: -47 | x: Optional[str] - | ^^^^^^^^^^^^^ UP007 -48 | x = Optional[str] +38 | def f(x: "Union[str, int, Union[float, bytes]]") -> None: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP007 +39 | ... | = help: Convert to `X | Y` ℹ Suggested fix +35 35 | ... +36 36 | +37 37 | +38 |-def f(x: "Union[str, int, Union[float, bytes]]") -> None: + 38 |+def f(x: "str | int | Union[float, bytes]") -> None: +39 39 | ... +40 40 | +41 41 | + +UP007.py:38:27: UP007 [*] Use `X | Y` for type annotations + | +38 | def f(x: "Union[str, int, Union[float, bytes]]") -> None: + | ^^^^^^^^^^^^^^^^^^^ UP007 +39 | ... + | + = help: Convert to `X | Y` + +ℹ Suggested fix +35 35 | ... +36 36 | +37 37 | +38 |-def f(x: "Union[str, int, Union[float, bytes]]") -> None: + 38 |+def f(x: "Union[str, int, float | bytes]") -> None: +39 39 | ... +40 40 | +41 41 | + +UP007.py:42:11: UP007 [*] Use `X | Y` for type annotations + | +42 | def f(x: "typing.Union[str, int]") -> None: + | ^^^^^^^^^^^^^^^^^^^^^^ UP007 +43 | ... + | + = help: Convert to `X | Y` + +ℹ Suggested fix +39 39 | ... +40 40 | +41 41 | +42 |-def f(x: "typing.Union[str, int]") -> None: + 42 |+def f(x: "str | int") -> None: +43 43 | ... 44 44 | 45 45 | -46 46 | def f() -> None: -47 |- x: Optional[str] - 47 |+ x: str | None -48 48 | x = Optional[str] -49 49 | -50 50 | x = Union[str, int] -UP007.py:48:9: UP007 Use `X | Y` for type annotations +UP007.py:55:8: UP007 [*] Use `X | Y` for type annotations | -46 | def f() -> None: -47 | x: Optional[str] -48 | x = Optional[str] - | ^^^^^^^^^^^^^ UP007 -49 | -50 | x = Union[str, int] - | - = help: Convert to `X | Y` - -UP007.py:50:9: UP007 Use `X | Y` for type annotations - | -48 | x = Optional[str] -49 | -50 | x = Union[str, int] - | ^^^^^^^^^^^^^^^ UP007 -51 | x = Union["str", "int"] -52 | x: Union[str, int] - | - = help: Convert to `X | Y` - -UP007.py:52:8: UP007 [*] Use `X | Y` for type annotations - | -50 | x = Union[str, int] -51 | x = Union["str", "int"] -52 | x: Union[str, int] - | ^^^^^^^^^^^^^^^ UP007 -53 | x: Union["str", "int"] +54 | def f() -> None: +55 | x: Optional[str] + | ^^^^^^^^^^^^^ UP007 +56 | x = Optional[str] | = help: Convert to `X | Y` ℹ Suggested fix -49 49 | -50 50 | x = Union[str, int] -51 51 | x = Union["str", "int"] -52 |- x: Union[str, int] - 52 |+ x: str | int -53 53 | x: Union["str", "int"] +52 52 | +53 53 | +54 54 | def f() -> None: +55 |- x: Optional[str] + 55 |+ x: str | None +56 56 | x = Optional[str] +57 57 | +58 58 | x = Union[str, int] + +UP007.py:56:9: UP007 Use `X | Y` for type annotations + | +54 | def f() -> None: +55 | x: Optional[str] +56 | x = Optional[str] + | ^^^^^^^^^^^^^ UP007 +57 | +58 | x = Union[str, int] + | + = help: Convert to `X | Y` + +UP007.py:58:9: UP007 Use `X | Y` for type annotations + | +56 | x = Optional[str] +57 | +58 | x = Union[str, int] + | ^^^^^^^^^^^^^^^ UP007 +59 | x = Union["str", "int"] +60 | x: Union[str, int] + | + = help: Convert to `X | Y` + +UP007.py:60:8: UP007 [*] Use `X | Y` for type annotations + | +58 | x = Union[str, int] +59 | x = Union["str", "int"] +60 | x: Union[str, int] + | ^^^^^^^^^^^^^^^ UP007 +61 | x: Union["str", "int"] + | + = help: Convert to `X | Y` + +ℹ Suggested fix +57 57 | +58 58 | x = Union[str, int] +59 59 | x = Union["str", "int"] +60 |- x: Union[str, int] + 60 |+ x: str | int +61 61 | x: Union["str", "int"] + +UP007.py:61:8: UP007 [*] Use `X | Y` for type annotations + | +59 | x = Union["str", "int"] +60 | x: Union[str, int] +61 | x: Union["str", "int"] + | ^^^^^^^^^^^^^^^^^^^ UP007 + | + = help: Convert to `X | Y` + +ℹ Suggested fix +58 58 | x = Union[str, int] +59 59 | x = Union["str", "int"] +60 60 | x: Union[str, int] +61 |- x: Union["str", "int"] + 61 |+ x: "str" | "int" diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP009_0.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP009_0.py.snap index 42e1dbd19f..871f3d8580 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP009_0.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP009_0.py.snap @@ -10,7 +10,7 @@ UP009_0.py:1:1: UP009 [*] UTF-8 encoding declaration is unnecessary | = help: Remove unnecessary coding comment -ℹ Suggested fix +ℹ Fix 1 |-# coding=utf8 2 1 | 3 2 | print("Hello world") diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP009_1.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP009_1.py.snap index 9447bf10cc..3019e95616 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP009_1.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP009_1.py.snap @@ -11,7 +11,7 @@ UP009_1.py:2:1: UP009 [*] UTF-8 encoding declaration is unnecessary | = help: Remove unnecessary coding comment -ℹ Suggested fix +ℹ Fix 1 1 | #!/usr/bin/python 2 |-# -*- coding: utf-8 -*- 3 2 | diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP011.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP011.py.snap index 58b5775f91..a52adcc916 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP011.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP011.py.snap @@ -10,7 +10,7 @@ UP011.py:5:21: UP011 [*] Unnecessary parentheses to `functools.lru_cache` | = help: Remove unnecessary parentheses -ℹ Suggested fix +ℹ Fix 2 2 | from functools import lru_cache 3 3 | 4 4 | @@ -29,7 +29,7 @@ UP011.py:10:11: UP011 [*] Unnecessary parentheses to `functools.lru_cache` | = help: Remove unnecessary parentheses -ℹ Suggested fix +ℹ Fix 7 7 | pass 8 8 | 9 9 | @@ -49,7 +49,7 @@ UP011.py:16:21: UP011 [*] Unnecessary parentheses to `functools.lru_cache` | = help: Remove unnecessary parentheses -ℹ Suggested fix +ℹ Fix 13 13 | 14 14 | 15 15 | @other_decorator @@ -68,7 +68,7 @@ UP011.py:21:21: UP011 [*] Unnecessary parentheses to `functools.lru_cache` | = help: Remove unnecessary parentheses -ℹ Suggested fix +ℹ Fix 18 18 | pass 19 19 | 20 20 | diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP012.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP012.py.snap index b083c6c0ca..d25beae3d4 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP012.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP012.py.snap @@ -11,7 +11,7 @@ UP012.py:2:1: UP012 [*] Unnecessary call to `encode` as UTF-8 | = help: Rewrite as bytes literal -ℹ Suggested fix +ℹ Fix 1 1 | # ASCII literals should be replaced by a bytes literal 2 |-"foo".encode("utf-8") # b"foo" 2 |+b"foo" # b"foo" @@ -30,7 +30,7 @@ UP012.py:3:1: UP012 [*] Unnecessary call to `encode` as UTF-8 | = help: Rewrite as bytes literal -ℹ Suggested fix +ℹ Fix 1 1 | # ASCII literals should be replaced by a bytes literal 2 2 | "foo".encode("utf-8") # b"foo" 3 |-"foo".encode("u8") # b"foo" @@ -50,7 +50,7 @@ UP012.py:4:1: UP012 [*] Unnecessary call to `encode` as UTF-8 | = help: Rewrite as bytes literal -ℹ Suggested fix +ℹ Fix 1 1 | # ASCII literals should be replaced by a bytes literal 2 2 | "foo".encode("utf-8") # b"foo" 3 3 | "foo".encode("u8") # b"foo" @@ -71,7 +71,7 @@ UP012.py:5:1: UP012 [*] Unnecessary call to `encode` as UTF-8 | = help: Rewrite as bytes literal -ℹ Suggested fix +ℹ Fix 2 2 | "foo".encode("utf-8") # b"foo" 3 3 | "foo".encode("u8") # b"foo" 4 4 | "foo".encode() # b"foo" @@ -92,7 +92,7 @@ UP012.py:6:1: UP012 [*] Unnecessary call to `encode` as UTF-8 | = help: Rewrite as bytes literal -ℹ Suggested fix +ℹ Fix 3 3 | "foo".encode("u8") # b"foo" 4 4 | "foo".encode() # b"foo" 5 5 | "foo".encode("UTF8") # b"foo" @@ -113,7 +113,7 @@ UP012.py:7:1: UP012 [*] Unnecessary call to `encode` as UTF-8 | = help: Rewrite as bytes literal -ℹ Suggested fix +ℹ Fix 4 4 | "foo".encode() # b"foo" 5 5 | "foo".encode("UTF8") # b"foo" 6 6 | U"foo".encode("utf-8") # b"foo" @@ -140,7 +140,7 @@ UP012.py:8:1: UP012 [*] Unnecessary call to `encode` as UTF-8 | = help: Rewrite as bytes literal -ℹ Suggested fix +ℹ Fix 5 5 | "foo".encode("UTF8") # b"foo" 6 6 | U"foo".encode("utf-8") # b"foo" 7 7 | "foo".encode(encoding="utf-8") # b"foo" @@ -170,7 +170,7 @@ UP012.py:16:5: UP012 [*] Unnecessary call to `encode` as UTF-8 | = help: Rewrite as bytes literal -ℹ Suggested fix +ℹ Fix 13 13 | "utf-8" 14 14 | ) 15 15 | ( @@ -195,7 +195,7 @@ UP012.py:20:5: UP012 [*] Unnecessary call to `encode` as UTF-8 | = help: Rewrite as bytes literal -ℹ Suggested fix +ℹ Fix 17 17 | "Ipsum".encode() 18 18 | ) 19 19 | ( @@ -217,7 +217,7 @@ UP012.py:24:5: UP012 [*] Unnecessary call to `encode` as UTF-8 | = help: Rewrite as bytes literal -ℹ Suggested fix +ℹ Fix 21 21 | "Ipsum".encode() # Comment 22 22 | ) 23 23 | ( @@ -237,7 +237,7 @@ UP012.py:32:1: UP012 [*] Unnecessary call to `encode` as UTF-8 | = help: Remove unnecessary encoding argument -ℹ Suggested fix +ℹ Fix 29 29 | string.encode("utf-8") 30 30 | 31 31 | bar = "bar" @@ -260,7 +260,7 @@ UP012.py:36:1: UP012 [*] Unnecessary call to `encode` as UTF-8 | = help: Remove unnecessary encoding argument -ℹ Suggested fix +ℹ Fix 33 33 | encoding = "latin" 34 34 | "foo".encode(encoding) 35 35 | f"foo{bar}".encode(encoding) @@ -282,7 +282,7 @@ UP012.py:53:1: UP012 [*] Unnecessary call to `encode` as UTF-8 | = help: Remove unnecessary encoding argument -ℹ Suggested fix +ℹ Fix 50 50 | "unicode text©".encode(encoding="utf-8", errors="replace") 51 51 | 52 52 | # Unicode literals should only be stripped of default encoding. @@ -303,7 +303,7 @@ UP012.py:55:1: UP012 [*] Unnecessary call to `encode` as UTF-8 | = help: Remove unnecessary encoding argument -ℹ Suggested fix +ℹ Fix 52 52 | # Unicode literals should only be stripped of default encoding. 53 53 | "unicode text©".encode("utf-8") # "unicode text©".encode() 54 54 | "unicode text©".encode() @@ -324,7 +324,7 @@ UP012.py:57:1: UP012 [*] Unnecessary call to `encode` as UTF-8 | = help: Rewrite as bytes literal -ℹ Suggested fix +ℹ Fix 54 54 | "unicode text©".encode() 55 55 | "unicode text©".encode(encoding="UTF8") # "unicode text©".encode() 56 56 | @@ -344,7 +344,7 @@ UP012.py:58:1: UP012 [*] Unnecessary call to `encode` as UTF-8 | = help: Rewrite as bytes literal -ℹ Suggested fix +ℹ Fix 55 55 | "unicode text©".encode(encoding="UTF8") # "unicode text©".encode() 56 56 | 57 57 | r"foo\o".encode("utf-8") # br"foo\o" @@ -365,7 +365,7 @@ UP012.py:59:1: UP012 [*] Unnecessary call to `encode` as UTF-8 | = help: Rewrite as bytes literal -ℹ Suggested fix +ℹ Fix 56 56 | 57 57 | r"foo\o".encode("utf-8") # br"foo\o" 58 58 | u"foo".encode("utf-8") # b"foo" @@ -385,7 +385,7 @@ UP012.py:60:1: UP012 [*] Unnecessary call to `encode` as UTF-8 | = help: Rewrite as bytes literal -ℹ Suggested fix +ℹ Fix 57 57 | r"foo\o".encode("utf-8") # br"foo\o" 58 58 | u"foo".encode("utf-8") # b"foo" 59 59 | R"foo\o".encode("utf-8") # br"foo\o" @@ -406,7 +406,7 @@ UP012.py:61:7: UP012 [*] Unnecessary call to `encode` as UTF-8 | = help: Rewrite as bytes literal -ℹ Suggested fix +ℹ Fix 58 58 | u"foo".encode("utf-8") # b"foo" 59 59 | R"foo\o".encode("utf-8") # br"foo\o" 60 60 | U"foo".encode("utf-8") # b"foo" @@ -429,7 +429,7 @@ UP012.py:64:1: UP012 [*] Unnecessary call to `encode` as UTF-8 | = help: Rewrite as bytes literal -ℹ Suggested fix +ℹ Fix 62 62 | 63 63 | # `encode` on parenthesized strings. 64 64 | ( @@ -452,10 +452,12 @@ UP012.py:69:1: UP012 [*] Unnecessary call to `encode` as UTF-8 71 | | "def" 72 | | )).encode() | |___________^ UP012 +73 | +74 | (f"foo{bar}").encode("utf-8") | = help: Rewrite as bytes literal -ℹ Suggested fix +ℹ Fix 67 67 | ).encode() 68 68 | 69 69 | (( @@ -465,5 +467,82 @@ UP012.py:69:1: UP012 [*] Unnecessary call to `encode` as UTF-8 70 |+ b"abc" 71 |+ b"def" 72 |+)) +73 73 | +74 74 | (f"foo{bar}").encode("utf-8") +75 75 | (f"foo{bar}").encode(encoding="utf-8") + +UP012.py:74:1: UP012 [*] Unnecessary call to `encode` as UTF-8 + | +72 | )).encode() +73 | +74 | (f"foo{bar}").encode("utf-8") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP012 +75 | (f"foo{bar}").encode(encoding="utf-8") +76 | ("unicode text©").encode("utf-8") + | + = help: Remove unnecessary encoding argument + +ℹ Fix +71 71 | "def" +72 72 | )).encode() +73 73 | +74 |-(f"foo{bar}").encode("utf-8") + 74 |+(f"foo{bar}").encode() +75 75 | (f"foo{bar}").encode(encoding="utf-8") +76 76 | ("unicode text©").encode("utf-8") +77 77 | ("unicode text©").encode(encoding="utf-8") + +UP012.py:75:1: UP012 [*] Unnecessary call to `encode` as UTF-8 + | +74 | (f"foo{bar}").encode("utf-8") +75 | (f"foo{bar}").encode(encoding="utf-8") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP012 +76 | ("unicode text©").encode("utf-8") +77 | ("unicode text©").encode(encoding="utf-8") + | + = help: Remove unnecessary encoding argument + +ℹ Fix +72 72 | )).encode() +73 73 | +74 74 | (f"foo{bar}").encode("utf-8") +75 |-(f"foo{bar}").encode(encoding="utf-8") + 75 |+(f"foo{bar}").encode() +76 76 | ("unicode text©").encode("utf-8") +77 77 | ("unicode text©").encode(encoding="utf-8") + +UP012.py:76:1: UP012 [*] Unnecessary call to `encode` as UTF-8 + | +74 | (f"foo{bar}").encode("utf-8") +75 | (f"foo{bar}").encode(encoding="utf-8") +76 | ("unicode text©").encode("utf-8") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP012 +77 | ("unicode text©").encode(encoding="utf-8") + | + = help: Remove unnecessary encoding argument + +ℹ Fix +73 73 | +74 74 | (f"foo{bar}").encode("utf-8") +75 75 | (f"foo{bar}").encode(encoding="utf-8") +76 |-("unicode text©").encode("utf-8") + 76 |+("unicode text©").encode() +77 77 | ("unicode text©").encode(encoding="utf-8") + +UP012.py:77:1: UP012 [*] Unnecessary call to `encode` as UTF-8 + | +75 | (f"foo{bar}").encode(encoding="utf-8") +76 | ("unicode text©").encode("utf-8") +77 | ("unicode text©").encode(encoding="utf-8") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP012 + | + = help: Remove unnecessary encoding argument + +ℹ Fix +74 74 | (f"foo{bar}").encode("utf-8") +75 75 | (f"foo{bar}").encode(encoding="utf-8") +76 76 | ("unicode text©").encode("utf-8") +77 |-("unicode text©").encode(encoding="utf-8") + 77 |+("unicode text©").encode() diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP015.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP015.py.snap index e0e09a15a5..b227068992 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP015.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP015.py.snap @@ -10,7 +10,7 @@ UP015.py:1:1: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 1 |-open("foo", "U") 1 |+open("foo") 2 2 | open("foo", "Ur") @@ -27,7 +27,7 @@ UP015.py:2:1: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 1 1 | open("foo", "U") 2 |-open("foo", "Ur") 2 |+open("foo") @@ -46,7 +46,7 @@ UP015.py:3:1: UP015 [*] Unnecessary open mode parameters, use ""rb"" | = help: Replace with ""rb"" -ℹ Suggested fix +ℹ Fix 1 1 | open("foo", "U") 2 2 | open("foo", "Ur") 3 |-open("foo", "Ub") @@ -66,7 +66,7 @@ UP015.py:4:1: UP015 [*] Unnecessary open mode parameters, use ""rb"" | = help: Replace with ""rb"" -ℹ Suggested fix +ℹ Fix 1 1 | open("foo", "U") 2 2 | open("foo", "Ur") 3 3 | open("foo", "Ub") @@ -87,7 +87,7 @@ UP015.py:5:1: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 2 2 | open("foo", "Ur") 3 3 | open("foo", "Ub") 4 4 | open("foo", "rUb") @@ -108,7 +108,7 @@ UP015.py:6:1: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 3 3 | open("foo", "Ub") 4 4 | open("foo", "rUb") 5 5 | open("foo", "r") @@ -128,7 +128,7 @@ UP015.py:7:1: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 4 4 | open("foo", "rUb") 5 5 | open("foo", "r") 6 6 | open("foo", "rt") @@ -149,7 +149,7 @@ UP015.py:8:1: UP015 [*] Unnecessary open mode parameters, use ""w"" | = help: Replace with ""w"" -ℹ Suggested fix +ℹ Fix 5 5 | open("foo", "r") 6 6 | open("foo", "rt") 7 7 | open("f", "r", encoding="UTF-8") @@ -170,7 +170,7 @@ UP015.py:10:6: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 7 7 | open("f", "r", encoding="UTF-8") 8 8 | open("f", "wt") 9 9 | @@ -191,7 +191,7 @@ UP015.py:12:6: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 9 9 | 10 10 | with open("foo", "U") as f: 11 11 | pass @@ -212,7 +212,7 @@ UP015.py:14:6: UP015 [*] Unnecessary open mode parameters, use ""rb"" | = help: Replace with ""rb"" -ℹ Suggested fix +ℹ Fix 11 11 | pass 12 12 | with open("foo", "Ur") as f: 13 13 | pass @@ -233,7 +233,7 @@ UP015.py:16:6: UP015 [*] Unnecessary open mode parameters, use ""rb"" | = help: Replace with ""rb"" -ℹ Suggested fix +ℹ Fix 13 13 | pass 14 14 | with open("foo", "Ub") as f: 15 15 | pass @@ -254,7 +254,7 @@ UP015.py:18:6: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 15 15 | pass 16 16 | with open("foo", "rUb") as f: 17 17 | pass @@ -275,7 +275,7 @@ UP015.py:20:6: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 17 17 | pass 18 18 | with open("foo", "r") as f: 19 19 | pass @@ -296,7 +296,7 @@ UP015.py:22:6: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 19 19 | pass 20 20 | with open("foo", "rt") as f: 21 21 | pass @@ -316,7 +316,7 @@ UP015.py:24:6: UP015 [*] Unnecessary open mode parameters, use ""w"" | = help: Replace with ""w"" -ℹ Suggested fix +ℹ Fix 21 21 | pass 22 22 | with open("foo", "r", encoding="UTF-8") as f: 23 23 | pass @@ -336,7 +336,7 @@ UP015.py:27:1: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 24 24 | with open("foo", "wt") as f: 25 25 | pass 26 26 | @@ -356,7 +356,7 @@ UP015.py:28:1: UP015 [*] Unnecessary open mode parameters, use ""rb"" | = help: Replace with ""rb"" -ℹ Suggested fix +ℹ Fix 25 25 | pass 26 26 | 27 27 | open(f("a", "b", "c"), "U") @@ -377,7 +377,7 @@ UP015.py:30:6: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 27 27 | open(f("a", "b", "c"), "U") 28 28 | open(f("a", "b", "c"), "Ub") 29 29 | @@ -397,7 +397,7 @@ UP015.py:32:6: UP015 [*] Unnecessary open mode parameters, use ""rb"" | = help: Replace with ""rb"" -ℹ Suggested fix +ℹ Fix 29 29 | 30 30 | with open(f("a", "b", "c"), "U") as f: 31 31 | pass @@ -418,7 +418,7 @@ UP015.py:35:6: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 32 32 | with open(f("a", "b", "c"), "Ub") as f: 33 33 | pass 34 34 | @@ -439,7 +439,7 @@ UP015.py:35:30: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 32 32 | with open(f("a", "b", "c"), "Ub") as f: 33 33 | pass 34 34 | @@ -459,7 +459,7 @@ UP015.py:37:6: UP015 [*] Unnecessary open mode parameters, use ""rb"" | = help: Replace with ""rb"" -ℹ Suggested fix +ℹ Fix 34 34 | 35 35 | with open("foo", "U") as fa, open("bar", "U") as fb: 36 36 | pass @@ -479,7 +479,7 @@ UP015.py:37:31: UP015 [*] Unnecessary open mode parameters, use ""rb"" | = help: Replace with ""rb"" -ℹ Suggested fix +ℹ Fix 34 34 | 35 35 | with open("foo", "U") as fa, open("bar", "U") as fb: 36 36 | pass @@ -500,7 +500,7 @@ UP015.py:40:1: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 37 37 | with open("foo", "Ub") as fa, open("bar", "Ub") as fb: 38 38 | pass 39 39 | @@ -519,7 +519,7 @@ UP015.py:41:1: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 38 38 | pass 39 39 | 40 40 | open("foo", mode="U") @@ -540,7 +540,7 @@ UP015.py:42:1: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 39 39 | 40 40 | open("foo", mode="U") 41 41 | open(name="foo", mode="U") @@ -561,7 +561,7 @@ UP015.py:44:6: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 41 41 | open(name="foo", mode="U") 42 42 | open(mode="U", name="foo") 43 43 | @@ -582,7 +582,7 @@ UP015.py:46:6: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 43 43 | 44 44 | with open("foo", mode="U") as f: 45 45 | pass @@ -602,7 +602,7 @@ UP015.py:48:6: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 45 45 | pass 46 46 | with open(name="foo", mode="U") as f: 47 47 | pass @@ -623,7 +623,7 @@ UP015.py:51:1: UP015 [*] Unnecessary open mode parameters, use ""rb"" | = help: Replace with ""rb"" -ℹ Suggested fix +ℹ Fix 48 48 | with open(mode="U", name="foo") as f: 49 49 | pass 50 50 | @@ -642,7 +642,7 @@ UP015.py:52:1: UP015 [*] Unnecessary open mode parameters, use ""rb"" | = help: Replace with ""rb"" -ℹ Suggested fix +ℹ Fix 49 49 | pass 50 50 | 51 51 | open("foo", mode="Ub") @@ -663,7 +663,7 @@ UP015.py:53:1: UP015 [*] Unnecessary open mode parameters, use ""rb"" | = help: Replace with ""rb"" -ℹ Suggested fix +ℹ Fix 50 50 | 51 51 | open("foo", mode="Ub") 52 52 | open(name="foo", mode="Ub") @@ -684,7 +684,7 @@ UP015.py:55:6: UP015 [*] Unnecessary open mode parameters, use ""rb"" | = help: Replace with ""rb"" -ℹ Suggested fix +ℹ Fix 52 52 | open(name="foo", mode="Ub") 53 53 | open(mode="Ub", name="foo") 54 54 | @@ -705,7 +705,7 @@ UP015.py:57:6: UP015 [*] Unnecessary open mode parameters, use ""rb"" | = help: Replace with ""rb"" -ℹ Suggested fix +ℹ Fix 54 54 | 55 55 | with open("foo", mode="Ub") as f: 56 56 | pass @@ -725,7 +725,7 @@ UP015.py:59:6: UP015 [*] Unnecessary open mode parameters, use ""rb"" | = help: Replace with ""rb"" -ℹ Suggested fix +ℹ Fix 56 56 | pass 57 57 | with open(name="foo", mode="Ub") as f: 58 58 | pass @@ -746,7 +746,7 @@ UP015.py:62:1: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 59 59 | with open(mode="Ub", name="foo") as f: 60 60 | pass 61 61 | @@ -766,7 +766,7 @@ UP015.py:63:1: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 60 60 | pass 61 61 | 62 62 | open(file="foo", mode='U', buffering=- 1, encoding=None, errors=None, newline=None, closefd=True, opener=None) @@ -786,7 +786,7 @@ UP015.py:64:1: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 61 61 | 62 62 | open(file="foo", mode='U', buffering=- 1, encoding=None, errors=None, newline=None, closefd=True, opener=None) 63 63 | open(file="foo", buffering=- 1, encoding=None, errors=None, newline=None, closefd=True, opener=None, mode='U') @@ -807,7 +807,7 @@ UP015.py:65:1: UP015 [*] Unnecessary open mode parameters | = help: Remove open mode parameters -ℹ Suggested fix +ℹ Fix 62 62 | open(file="foo", mode='U', buffering=- 1, encoding=None, errors=None, newline=None, closefd=True, opener=None) 63 63 | open(file="foo", buffering=- 1, encoding=None, errors=None, newline=None, closefd=True, opener=None, mode='U') 64 64 | open(file="foo", buffering=- 1, encoding=None, errors=None, mode='U', newline=None, closefd=True, opener=None) @@ -828,7 +828,7 @@ UP015.py:67:1: UP015 [*] Unnecessary open mode parameters, use ""rb"" | = help: Replace with ""rb"" -ℹ Suggested fix +ℹ Fix 64 64 | open(file="foo", buffering=- 1, encoding=None, errors=None, mode='U', newline=None, closefd=True, opener=None) 65 65 | open(mode='U', file="foo", buffering=- 1, encoding=None, errors=None, newline=None, closefd=True, opener=None) 66 66 | @@ -848,7 +848,7 @@ UP015.py:68:1: UP015 [*] Unnecessary open mode parameters, use ""rb"" | = help: Replace with ""rb"" -ℹ Suggested fix +ℹ Fix 65 65 | open(mode='U', file="foo", buffering=- 1, encoding=None, errors=None, newline=None, closefd=True, opener=None) 66 66 | 67 67 | open(file="foo", mode='Ub', buffering=- 1, encoding=None, errors=None, newline=None, closefd=True, opener=None) @@ -868,7 +868,7 @@ UP015.py:69:1: UP015 [*] Unnecessary open mode parameters, use ""rb"" | = help: Replace with ""rb"" -ℹ Suggested fix +ℹ Fix 66 66 | 67 67 | open(file="foo", mode='Ub', buffering=- 1, encoding=None, errors=None, newline=None, closefd=True, opener=None) 68 68 | open(file="foo", buffering=- 1, encoding=None, errors=None, newline=None, closefd=True, opener=None, mode='Ub') @@ -889,7 +889,7 @@ UP015.py:70:1: UP015 [*] Unnecessary open mode parameters, use ""rb"" | = help: Replace with ""rb"" -ℹ Suggested fix +ℹ Fix 67 67 | open(file="foo", mode='Ub', buffering=- 1, encoding=None, errors=None, newline=None, closefd=True, opener=None) 68 68 | open(file="foo", buffering=- 1, encoding=None, errors=None, newline=None, closefd=True, opener=None, mode='Ub') 69 69 | open(file="foo", buffering=- 1, encoding=None, errors=None, mode='Ub', newline=None, closefd=True, opener=None) diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP018.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP018.py.snap index 537f50448f..d51fd2112e 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP018.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP018.py.snap @@ -11,7 +11,7 @@ UP018.py:21:1: UP018 [*] Unnecessary call to `str` | = help: Replace with empty string -ℹ Suggested fix +ℹ Fix 18 18 | f"{f'{str()}'}" 19 19 | 20 20 | # These become string or byte literals @@ -32,7 +32,7 @@ UP018.py:22:1: UP018 [*] Unnecessary call to `str` | = help: Replace with empty string -ℹ Suggested fix +ℹ Fix 19 19 | 20 20 | # These become string or byte literals 21 21 | str() @@ -54,7 +54,7 @@ UP018.py:23:1: UP018 [*] Unnecessary call to `str` | = help: Replace with empty string -ℹ Suggested fix +ℹ Fix 20 20 | # These become string or byte literals 21 21 | str() 22 22 | str("foo") @@ -77,7 +77,7 @@ UP018.py:25:1: UP018 [*] Unnecessary call to `bytes` | = help: Replace with empty bytes -ℹ Suggested fix +ℹ Fix 22 22 | str("foo") 23 23 | str(""" 24 24 | foo""") @@ -98,7 +98,7 @@ UP018.py:26:1: UP018 [*] Unnecessary call to `bytes` | = help: Replace with empty bytes -ℹ Suggested fix +ℹ Fix 23 23 | str(""" 24 24 | foo""") 25 25 | bytes() @@ -119,7 +119,7 @@ UP018.py:27:1: UP018 [*] Unnecessary call to `bytes` | = help: Replace with empty bytes -ℹ Suggested fix +ℹ Fix 24 24 | foo""") 25 25 | bytes() 26 26 | bytes(b"foo") @@ -138,7 +138,7 @@ UP018.py:29:4: UP018 [*] Unnecessary call to `str` | = help: Replace with empty string -ℹ Suggested fix +ℹ Fix 26 26 | bytes(b"foo") 27 27 | bytes(b""" 28 28 | foo""") diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP019.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP019.py.snap index c4697f0e39..291ef30d33 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP019.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP019.py.snap @@ -9,7 +9,7 @@ UP019.py:7:22: UP019 [*] `typing.Text` is deprecated, use `str` | = help: Replace with `str` -ℹ Suggested fix +ℹ Fix 4 4 | from typing import Text as Goodbye 5 5 | 6 6 | @@ -27,7 +27,7 @@ UP019.py:11:29: UP019 [*] `typing.Text` is deprecated, use `str` | = help: Replace with `str` -ℹ Suggested fix +ℹ Fix 8 8 | print(word) 9 9 | 10 10 | @@ -45,7 +45,7 @@ UP019.py:15:28: UP019 [*] `typing.Text` is deprecated, use `str` | = help: Replace with `str` -ℹ Suggested fix +ℹ Fix 12 12 | print(word) 13 13 | 14 14 | @@ -63,7 +63,7 @@ UP019.py:19:29: UP019 [*] `typing.Text` is deprecated, use `str` | = help: Replace with `str` -ℹ Suggested fix +ℹ Fix 16 16 | print(word) 17 17 | 18 18 | diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP022.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP022.py.snap index 6aade82834..75df54757d 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP022.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP022.py.snap @@ -1,7 +1,7 @@ --- source: crates/ruff/src/rules/pyupgrade/mod.rs --- -UP022.py:4:10: UP022 [*] Sending stdout and stderr to pipe is deprecated, use `capture_output` +UP022.py:4:10: UP022 [*] Sending `stdout` and `stderr` to `PIPE` is deprecated, use `capture_output` | 2 | import subprocess 3 | @@ -22,7 +22,7 @@ UP022.py:4:10: UP022 [*] Sending stdout and stderr to pipe is deprecated, use `c 6 6 | output = subprocess.run(["foo"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 7 7 | -UP022.py:6:10: UP022 [*] Sending stdout and stderr to pipe is deprecated, use `capture_output` +UP022.py:6:10: UP022 [*] Sending `stdout` and `stderr` to `PIPE` is deprecated, use `capture_output` | 4 | output = run(["foo"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 5 | @@ -43,7 +43,7 @@ UP022.py:6:10: UP022 [*] Sending stdout and stderr to pipe is deprecated, use `c 8 8 | output = subprocess.run(stdout=subprocess.PIPE, args=["foo"], stderr=subprocess.PIPE) 9 9 | -UP022.py:8:10: UP022 [*] Sending stdout and stderr to pipe is deprecated, use `capture_output` +UP022.py:8:10: UP022 [*] Sending `stdout` and `stderr` to `PIPE` is deprecated, use `capture_output` | 6 | output = subprocess.run(["foo"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 7 | @@ -64,7 +64,7 @@ UP022.py:8:10: UP022 [*] Sending stdout and stderr to pipe is deprecated, use `c 10 10 | output = subprocess.run( 11 11 | ["foo"], stdout=subprocess.PIPE, check=True, stderr=subprocess.PIPE -UP022.py:10:10: UP022 [*] Sending stdout and stderr to pipe is deprecated, use `capture_output` +UP022.py:10:10: UP022 [*] Sending `stdout` and `stderr` to `PIPE` is deprecated, use `capture_output` | 8 | output = subprocess.run(stdout=subprocess.PIPE, args=["foo"], stderr=subprocess.PIPE) 9 | @@ -88,7 +88,7 @@ UP022.py:10:10: UP022 [*] Sending stdout and stderr to pipe is deprecated, use ` 13 13 | 14 14 | output = subprocess.run( -UP022.py:14:10: UP022 [*] Sending stdout and stderr to pipe is deprecated, use `capture_output` +UP022.py:14:10: UP022 [*] Sending `stdout` and `stderr` to `PIPE` is deprecated, use `capture_output` | 12 | ) 13 | @@ -112,7 +112,7 @@ UP022.py:14:10: UP022 [*] Sending stdout and stderr to pipe is deprecated, use ` 17 17 | 18 18 | output = subprocess.run( -UP022.py:18:10: UP022 [*] Sending stdout and stderr to pipe is deprecated, use `capture_output` +UP022.py:18:10: UP022 [*] Sending `stdout` and `stderr` to `PIPE` is deprecated, use `capture_output` | 16 | ) 17 | @@ -144,7 +144,7 @@ UP022.py:18:10: UP022 [*] Sending stdout and stderr to pipe is deprecated, use ` 24 23 | encoding="utf-8", 25 24 | close_fds=True, -UP022.py:29:14: UP022 [*] Sending stdout and stderr to pipe is deprecated, use `capture_output` +UP022.py:29:14: UP022 [*] Sending `stdout` and `stderr` to `PIPE` is deprecated, use `capture_output` | 28 | if output: 29 | output = subprocess.run( diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP024_0.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP024_0.py.snap index cdfd9e18a3..f77d7d36a6 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP024_0.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP024_0.py.snap @@ -11,7 +11,7 @@ UP024_0.py:6:8: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `EnvironmentError` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 3 3 | # These should be fixed 4 4 | try: 5 5 | pass @@ -31,7 +31,7 @@ UP024_0.py:11:8: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `IOError` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 8 8 | 9 9 | try: 10 10 | pass @@ -51,7 +51,7 @@ UP024_0.py:16:8: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `WindowsError` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 13 13 | 14 14 | try: 15 15 | pass @@ -71,7 +71,7 @@ UP024_0.py:21:8: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `mmap.error` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 18 18 | 19 19 | try: 20 20 | pass @@ -91,7 +91,7 @@ UP024_0.py:26:8: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `select.error` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 23 23 | 24 24 | try: 25 25 | pass @@ -111,7 +111,7 @@ UP024_0.py:31:8: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `socket.error` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 28 28 | 29 29 | try: 30 30 | pass @@ -131,7 +131,7 @@ UP024_0.py:36:8: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `error` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 33 33 | 34 34 | try: 35 35 | pass @@ -152,7 +152,7 @@ UP024_0.py:43:8: UP024 [*] Replace aliased errors with `OSError` | = help: Replace with builtin `OSError` -ℹ Suggested fix +ℹ Fix 40 40 | 41 41 | try: 42 42 | pass @@ -173,7 +173,7 @@ UP024_0.py:47:8: UP024 [*] Replace aliased errors with `OSError` | = help: Replace with builtin `OSError` -ℹ Suggested fix +ℹ Fix 44 44 | pass 45 45 | try: 46 46 | pass @@ -193,7 +193,7 @@ UP024_0.py:51:8: UP024 [*] Replace aliased errors with `OSError` | = help: Replace with builtin `OSError` -ℹ Suggested fix +ℹ Fix 48 48 | pass 49 49 | try: 50 50 | pass @@ -213,7 +213,7 @@ UP024_0.py:58:8: UP024 [*] Replace aliased errors with `OSError` | = help: Replace with builtin `OSError` -ℹ Suggested fix +ℹ Fix 55 55 | 56 56 | try: 57 57 | pass @@ -234,7 +234,7 @@ UP024_0.py:65:8: UP024 [*] Replace aliased errors with `OSError` | = help: Replace with builtin `OSError` -ℹ Suggested fix +ℹ Fix 62 62 | from .mmap import error 63 63 | try: 64 64 | pass @@ -254,7 +254,7 @@ UP024_0.py:87:8: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `mmap.error` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 84 84 | pass 85 85 | try: 86 86 | pass diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP024_1.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP024_1.py.snap index f59ac0307d..ed64b2e20f 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP024_1.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP024_1.py.snap @@ -12,7 +12,7 @@ UP024_1.py:5:8: UP024 [*] Replace aliased errors with `OSError` | = help: Replace with builtin `OSError` -ℹ Suggested fix +ℹ Fix 2 2 | 3 3 | try: 4 4 | pass @@ -32,7 +32,7 @@ UP024_1.py:7:8: UP024 [*] Replace aliased errors with `OSError` | = help: Replace with builtin `OSError` -ℹ Suggested fix +ℹ Fix 4 4 | pass 5 5 | except (OSError, mmap.error, IOError): 6 6 | pass @@ -57,7 +57,7 @@ UP024_1.py:12:8: UP024 [*] Replace aliased errors with `OSError` | = help: Replace with builtin `OSError` -ℹ Suggested fix +ℹ Fix 9 9 | 10 10 | try: 11 11 | pass diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP024_2.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP024_2.py.snap index 093108e347..36ce30b1e5 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP024_2.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP024_2.py.snap @@ -12,7 +12,7 @@ UP024_2.py:10:7: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `socket.error` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 7 7 | 8 8 | # Testing the modules 9 9 | import socket, mmap, select @@ -32,7 +32,7 @@ UP024_2.py:11:7: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `mmap.error` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 8 8 | # Testing the modules 9 9 | import socket, mmap, select 10 10 | raise socket.error @@ -53,7 +53,7 @@ UP024_2.py:12:7: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `select.error` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 9 9 | import socket, mmap, select 10 10 | raise socket.error 11 11 | raise mmap.error @@ -74,7 +74,7 @@ UP024_2.py:14:7: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `socket.error` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 11 11 | raise mmap.error 12 12 | raise select.error 13 13 | @@ -93,7 +93,7 @@ UP024_2.py:15:7: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `mmap.error` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 12 12 | raise select.error 13 13 | 14 14 | raise socket.error() @@ -114,7 +114,7 @@ UP024_2.py:16:7: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `select.error` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 13 13 | 14 14 | raise socket.error() 15 15 | raise mmap.error(1) @@ -135,7 +135,7 @@ UP024_2.py:18:7: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `socket.error` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 15 15 | raise mmap.error(1) 16 16 | raise select.error(1, 2) 17 17 | @@ -155,7 +155,7 @@ UP024_2.py:25:7: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `error` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 22 22 | ) 23 23 | 24 24 | from mmap import error @@ -175,7 +175,7 @@ UP024_2.py:28:7: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `error` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 25 25 | raise error 26 26 | 27 27 | from socket import error @@ -195,7 +195,7 @@ UP024_2.py:31:7: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `error` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 28 28 | raise error(1) 29 29 | 30 30 | from select import error @@ -215,7 +215,7 @@ UP024_2.py:34:7: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `EnvironmentError` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 31 31 | raise error(1, 2) 32 32 | 33 33 | # Testing the names @@ -235,7 +235,7 @@ UP024_2.py:35:7: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `IOError` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 32 32 | 33 33 | # Testing the names 34 34 | raise EnvironmentError @@ -256,7 +256,7 @@ UP024_2.py:36:7: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `WindowsError` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 33 33 | # Testing the names 34 34 | raise EnvironmentError 35 35 | raise IOError @@ -277,7 +277,7 @@ UP024_2.py:38:7: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `EnvironmentError` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 35 35 | raise IOError 36 36 | raise WindowsError 37 37 | @@ -296,7 +296,7 @@ UP024_2.py:39:7: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `IOError` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 36 36 | raise WindowsError 37 37 | 38 38 | raise EnvironmentError() @@ -317,7 +317,7 @@ UP024_2.py:40:7: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `WindowsError` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 37 37 | 38 38 | raise EnvironmentError() 39 39 | raise IOError(1) @@ -338,7 +338,7 @@ UP024_2.py:42:7: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `EnvironmentError` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 39 39 | raise IOError(1) 40 40 | raise WindowsError(1, 2) 41 41 | @@ -359,7 +359,7 @@ UP024_2.py:48:7: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `WindowsError` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 45 45 | 3, 46 46 | ) 47 47 | @@ -377,7 +377,7 @@ UP024_2.py:49:7: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `EnvironmentError` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 46 46 | ) 47 47 | 48 48 | raise WindowsError @@ -394,7 +394,7 @@ UP024_2.py:50:7: UP024 [*] Replace aliased errors with `OSError` | = help: Replace `IOError` with builtin `OSError` -ℹ Suggested fix +ℹ Fix 47 47 | 48 48 | raise WindowsError 49 49 | raise EnvironmentError(1) diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP024_4.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP024_4.py.snap index 19e9d10e59..d1dec00413 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP024_4.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP024_4.py.snap @@ -11,7 +11,7 @@ UP024_4.py:9:8: UP024 [*] Replace aliased errors with `OSError` | = help: Replace with builtin `OSError` -ℹ Suggested fix +ℹ Fix 6 6 | conn = Connection(settings.CELERY_BROKER_URL) 7 7 | conn.ensure_connection(max_retries=2) 8 8 | conn._close() diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP025.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP025.py.snap index 0ef2bd7b18..380d022354 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP025.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP025.py.snap @@ -11,7 +11,7 @@ UP025.py:2:5: UP025 [*] Remove unicode literals from strings | = help: Remove unicode prefix -ℹ Suggested fix +ℹ Fix 1 1 | # These should change 2 |-x = u"Hello" 2 |+x = "Hello" @@ -30,7 +30,7 @@ UP025.py:4:1: UP025 [*] Remove unicode literals from strings | = help: Remove unicode prefix -ℹ Suggested fix +ℹ Fix 1 1 | # These should change 2 2 | x = u"Hello" 3 3 | @@ -51,7 +51,7 @@ UP025.py:6:7: UP025 [*] Remove unicode literals from strings | = help: Remove unicode prefix -ℹ Suggested fix +ℹ Fix 3 3 | 4 4 | u'world' 5 5 | @@ -72,7 +72,7 @@ UP025.py:8:7: UP025 [*] Remove unicode literals from strings | = help: Remove unicode prefix -ℹ Suggested fix +ℹ Fix 5 5 | 6 6 | print(u"Hello") 7 7 | @@ -93,7 +93,7 @@ UP025.py:12:5: UP025 [*] Remove unicode literals from strings | = help: Remove unicode prefix -ℹ Suggested fix +ℹ Fix 9 9 | 10 10 | import foo 11 11 | @@ -114,7 +114,7 @@ UP025.py:12:15: UP025 [*] Remove unicode literals from strings | = help: Remove unicode prefix -ℹ Suggested fix +ℹ Fix 9 9 | 10 10 | import foo 11 11 | @@ -135,7 +135,7 @@ UP025.py:12:27: UP025 [*] Remove unicode literals from strings | = help: Remove unicode prefix -ℹ Suggested fix +ℹ Fix 9 9 | 10 10 | import foo 11 11 | @@ -156,7 +156,7 @@ UP025.py:12:39: UP025 [*] Remove unicode literals from strings | = help: Remove unicode prefix -ℹ Suggested fix +ℹ Fix 9 9 | 10 10 | import foo 11 11 | @@ -177,7 +177,7 @@ UP025.py:16:5: UP025 [*] Remove unicode literals from strings | = help: Remove unicode prefix -ℹ Suggested fix +ℹ Fix 13 13 | 14 14 | # These should stay quoted they way they are 15 15 | @@ -197,7 +197,7 @@ UP025.py:17:5: UP025 [*] Remove unicode literals from strings | = help: Remove unicode prefix -ℹ Suggested fix +ℹ Fix 14 14 | # These should stay quoted they way they are 15 15 | 16 16 | x = u'hello' @@ -217,7 +217,7 @@ UP025.py:18:5: UP025 [*] Remove unicode literals from strings | = help: Remove unicode prefix -ℹ Suggested fix +ℹ Fix 15 15 | 16 16 | x = u'hello' 17 17 | x = u"""hello""" @@ -238,7 +238,7 @@ UP025.py:19:5: UP025 [*] Remove unicode literals from strings | = help: Remove unicode prefix -ℹ Suggested fix +ℹ Fix 16 16 | x = u'hello' 17 17 | x = u"""hello""" 18 18 | x = u'''hello''' diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP032_0.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP032_0.py.snap index 16833914ee..2a1340df50 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP032_0.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP032_0.py.snap @@ -533,7 +533,7 @@ UP032_0.py:55:1: UP032 [*] Use f-string instead of `format` call 55 | '''{[b]}'''.format(a) | ^^^^^^^^^^^^^^^^^^^^^ UP032 56 | -57 | ### +57 | "{}".format( | = help: Convert to f-string @@ -544,57 +544,107 @@ UP032_0.py:55:1: UP032 [*] Use f-string instead of `format` call 55 |-'''{[b]}'''.format(a) 55 |+f'''{a["b"]}''' 56 56 | -57 57 | ### -58 58 | # Non-errors +57 57 | "{}".format( +58 58 | 1 -UP032_0.py:100:11: UP032 [*] Use f-string instead of `format` call +UP032_0.py:57:1: UP032 [*] Use f-string instead of `format` call + | +55 | '''{[b]}'''.format(a) +56 | +57 | / "{}".format( +58 | | 1 +59 | | ) + | |_^ UP032 +60 | +61 | "123456789 {}".format( + | + = help: Convert to f-string + +ℹ Suggested fix +54 54 | +55 55 | '''{[b]}'''.format(a) +56 56 | +57 |-"{}".format( +58 |- 1 +59 |-) + 57 |+f"{1}" +60 58 | +61 59 | "123456789 {}".format( +62 60 | 1111111111111111111111111111111111111111111111111111111111111111111111111, + +UP032_0.py:61:1: UP032 [*] Use f-string instead of `format` call + | +59 | ) +60 | +61 | / "123456789 {}".format( +62 | | 1111111111111111111111111111111111111111111111111111111111111111111111111, +63 | | ) + | |_^ UP032 +64 | +65 | ### + | + = help: Convert to f-string + +ℹ Suggested fix +58 58 | 1 +59 59 | ) +60 60 | +61 |-"123456789 {}".format( +62 |- 1111111111111111111111111111111111111111111111111111111111111111111111111, +63 |-) + 61 |+f"123456789 {1111111111111111111111111111111111111111111111111111111111111111111111111}" +64 62 | +65 63 | ### +66 64 | # Non-errors + +UP032_0.py:111:11: UP032 [*] Use f-string instead of `format` call | - 99 | def d(osname, version, release): -100 | return"{}-{}.{}".format(osname, version, release) +110 | def d(osname, version, release): +111 | return"{}-{}.{}".format(osname, version, release) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP032 | = help: Convert to f-string ℹ Suggested fix -97 97 | -98 98 | -99 99 | def d(osname, version, release): -100 |- return"{}-{}.{}".format(osname, version, release) - 100 |+ return f"{osname}-{version}.{release}" -101 101 | -102 102 | -103 103 | def e(): +108 108 | +109 109 | +110 110 | def d(osname, version, release): +111 |- return"{}-{}.{}".format(osname, version, release) + 111 |+ return f"{osname}-{version}.{release}" +112 112 | +113 113 | +114 114 | def e(): -UP032_0.py:104:10: UP032 [*] Use f-string instead of `format` call +UP032_0.py:115:10: UP032 [*] Use f-string instead of `format` call | -103 | def e(): -104 | yield"{}".format(1) +114 | def e(): +115 | yield"{}".format(1) | ^^^^^^^^^^^^^^ UP032 | = help: Convert to f-string ℹ Suggested fix -101 101 | -102 102 | -103 103 | def e(): -104 |- yield"{}".format(1) - 104 |+ yield f"{1}" -105 105 | -106 106 | -107 107 | assert"{}".format(1) +112 112 | +113 113 | +114 114 | def e(): +115 |- yield"{}".format(1) + 115 |+ yield f"{1}" +116 116 | +117 117 | +118 118 | assert"{}".format(1) -UP032_0.py:107:7: UP032 [*] Use f-string instead of `format` call +UP032_0.py:118:7: UP032 [*] Use f-string instead of `format` call | -107 | assert"{}".format(1) +118 | assert"{}".format(1) | ^^^^^^^^^^^^^^ UP032 | = help: Convert to f-string ℹ Suggested fix -104 104 | yield"{}".format(1) -105 105 | -106 106 | -107 |-assert"{}".format(1) - 107 |+assert f"{1}" +115 115 | yield"{}".format(1) +116 116 | +117 117 | +118 |-assert"{}".format(1) + 118 |+assert f"{1}" diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP033_0.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP033_0.py.snap index 0a752a2cdf..dc40338e7e 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP033_0.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP033_0.py.snap @@ -10,7 +10,7 @@ UP033_0.py:4:21: UP033 [*] Use `@functools.cache` instead of `@functools.lru_cac | = help: Rewrite with `@functools.cache -ℹ Suggested fix +ℹ Fix 1 1 | import functools 2 2 | 3 3 | @@ -30,7 +30,7 @@ UP033_0.py:10:21: UP033 [*] Use `@functools.cache` instead of `@functools.lru_ca | = help: Rewrite with `@functools.cache -ℹ Suggested fix +ℹ Fix 7 7 | 8 8 | 9 9 | @other_decorator @@ -49,7 +49,7 @@ UP033_0.py:15:21: UP033 [*] Use `@functools.cache` instead of `@functools.lru_ca | = help: Rewrite with `@functools.cache -ℹ Suggested fix +ℹ Fix 12 12 | pass 13 13 | 14 14 | diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP033_1.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP033_1.py.snap index 91f6560faf..5fe701079b 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP033_1.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP033_1.py.snap @@ -10,7 +10,7 @@ UP033_1.py:4:11: UP033 [*] Use `@functools.cache` instead of `@functools.lru_cac | = help: Rewrite with `@functools.cache -ℹ Suggested fix +ℹ Fix 1 |-from functools import lru_cache 1 |+from functools import lru_cache, cache 2 2 | @@ -31,7 +31,7 @@ UP033_1.py:10:11: UP033 [*] Use `@functools.cache` instead of `@functools.lru_ca | = help: Rewrite with `@functools.cache -ℹ Suggested fix +ℹ Fix 1 |-from functools import lru_cache 1 |+from functools import lru_cache, cache 2 2 | @@ -56,7 +56,7 @@ UP033_1.py:15:11: UP033 [*] Use `@functools.cache` instead of `@functools.lru_ca | = help: Rewrite with `@functools.cache -ℹ Suggested fix +ℹ Fix 1 |-from functools import lru_cache 1 |+from functools import lru_cache, cache 2 2 | diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP034.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP034.py.snap index 17e5427eb5..4373bdabd9 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP034.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP034.py.snap @@ -11,7 +11,7 @@ UP034.py:2:7: UP034 [*] Avoid extraneous parentheses | = help: Remove extraneous parentheses -ℹ Suggested fix +ℹ Fix 1 1 | # UP034 2 |-print(("foo")) 2 |+print("foo") @@ -29,7 +29,7 @@ UP034.py:5:7: UP034 [*] Avoid extraneous parentheses | = help: Remove extraneous parentheses -ℹ Suggested fix +ℹ Fix 2 2 | print(("foo")) 3 3 | 4 4 | # UP034 @@ -49,7 +49,7 @@ UP034.py:8:7: UP034 [*] Avoid extraneous parentheses | = help: Remove extraneous parentheses -ℹ Suggested fix +ℹ Fix 5 5 | print(("hell((goodybe))o")) 6 6 | 7 7 | # UP034 @@ -69,7 +69,7 @@ UP034.py:11:7: UP034 [*] Avoid extraneous parentheses | = help: Remove extraneous parentheses -ℹ Suggested fix +ℹ Fix 8 8 | print((("foo"))) 9 9 | 10 10 | # UP034 @@ -89,7 +89,7 @@ UP034.py:14:7: UP034 [*] Avoid extraneous parentheses | = help: Remove extraneous parentheses -ℹ Suggested fix +ℹ Fix 11 11 | print((((1)))) 12 12 | 13 13 | # UP034 @@ -109,7 +109,7 @@ UP034.py:18:5: UP034 [*] Avoid extraneous parentheses | = help: Remove extraneous parentheses -ℹ Suggested fix +ℹ Fix 15 15 | 16 16 | # UP034 17 17 | print( @@ -132,7 +132,7 @@ UP034.py:23:5: UP034 [*] Avoid extraneous parentheses | = help: Remove extraneous parentheses -ℹ Suggested fix +ℹ Fix 20 20 | 21 21 | # UP034 22 22 | print( @@ -156,7 +156,7 @@ UP034.py:30:13: UP034 [*] Avoid extraneous parentheses | = help: Remove extraneous parentheses -ℹ Suggested fix +ℹ Fix 27 27 | 28 28 | # UP034 29 29 | def f(): @@ -176,7 +176,7 @@ UP034.py:35:9: UP034 [*] Avoid extraneous parentheses | = help: Remove extraneous parentheses -ℹ Suggested fix +ℹ Fix 32 32 | # UP034 33 33 | if True: 34 34 | print( @@ -196,7 +196,7 @@ UP034.py:39:7: UP034 [*] Avoid extraneous parentheses | = help: Remove extraneous parentheses -ℹ Suggested fix +ℹ Fix 36 36 | ) 37 37 | 38 38 | # UP034 diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP037.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP037.py.snap index 00e5a6a66d..8d3af3b967 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP037.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP037.py.snap @@ -9,7 +9,7 @@ UP037.py:18:14: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 15 15 | from mypy_extensions import Arg, DefaultArg, DefaultNamedArg, NamedArg, VarArg 16 16 | 17 17 | @@ -27,7 +27,7 @@ UP037.py:18:28: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 15 15 | from mypy_extensions import Arg, DefaultArg, DefaultNamedArg, NamedArg, VarArg 16 16 | 17 17 | @@ -45,7 +45,7 @@ UP037.py:19:8: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 16 16 | 17 17 | 18 18 | def foo(var: "MyClass") -> "MyClass": @@ -63,7 +63,7 @@ UP037.py:22:21: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 19 19 | x: "MyClass" 20 20 | 21 21 | @@ -81,7 +81,7 @@ UP037.py:26:16: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 23 23 | pass 24 24 | 25 25 | @@ -99,7 +99,7 @@ UP037.py:26:33: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 23 23 | pass 24 24 | 25 25 | @@ -118,7 +118,7 @@ UP037.py:30:10: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 27 27 | pass 28 28 | 29 29 | @@ -137,7 +137,7 @@ UP037.py:32:14: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 29 29 | 30 30 | x: Tuple["MyClass"] 31 31 | @@ -155,7 +155,7 @@ UP037.py:36:8: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 33 33 | 34 34 | 35 35 | class Foo(NamedTuple): @@ -173,7 +173,7 @@ UP037.py:40:27: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 37 37 | 38 38 | 39 39 | class D(TypedDict): @@ -191,7 +191,7 @@ UP037.py:44:31: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 41 41 | 42 42 | 43 43 | class D(TypedDict): @@ -210,7 +210,7 @@ UP037.py:47:14: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 44 44 | E: TypedDict("E", {"foo": "int"}) 45 45 | 46 46 | @@ -231,7 +231,7 @@ UP037.py:49:8: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 46 46 | 47 47 | x: Annotated["str", "metadata"] 48 48 | @@ -252,7 +252,7 @@ UP037.py:51:15: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 48 48 | 49 49 | x: Arg("str", "name") 50 50 | @@ -273,7 +273,7 @@ UP037.py:53:13: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 50 50 | 51 51 | x: DefaultArg("str", "name") 52 52 | @@ -294,7 +294,7 @@ UP037.py:55:20: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 52 52 | 53 53 | x: NamedArg("str", "name") 54 54 | @@ -315,7 +315,7 @@ UP037.py:57:20: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 54 54 | 55 55 | x: DefaultNamedArg("str", "name") 56 56 | @@ -336,7 +336,7 @@ UP037.py:59:11: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 56 56 | 57 57 | x: DefaultNamedArg("str", name="name") 58 58 | @@ -357,7 +357,7 @@ UP037.py:61:19: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 58 58 | 59 59 | x: VarArg("str") 60 60 | @@ -378,7 +378,7 @@ UP037.py:63:29: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 60 60 | 61 61 | x: List[List[List["MyClass"]]] 62 62 | @@ -399,7 +399,7 @@ UP037.py:63:45: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 60 60 | 61 61 | x: List[List[List["MyClass"]]] 62 62 | @@ -420,7 +420,7 @@ UP037.py:65:29: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 62 62 | 63 63 | x: NamedTuple("X", [("foo", "int"), ("bar", "str")]) 64 64 | @@ -441,7 +441,7 @@ UP037.py:65:36: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 62 62 | 63 63 | x: NamedTuple("X", [("foo", "int"), ("bar", "str")]) 64 64 | @@ -462,7 +462,7 @@ UP037.py:65:45: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 62 62 | 63 63 | x: NamedTuple("X", [("foo", "int"), ("bar", "str")]) 64 64 | @@ -483,7 +483,7 @@ UP037.py:65:52: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 62 62 | 63 63 | x: NamedTuple("X", [("foo", "int"), ("bar", "str")]) 64 64 | @@ -504,7 +504,7 @@ UP037.py:67:24: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 64 64 | 65 65 | x: NamedTuple("X", fields=[("foo", "int"), ("bar", "str")]) 66 66 | @@ -525,7 +525,7 @@ UP037.py:67:38: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 64 64 | 65 65 | x: NamedTuple("X", fields=[("foo", "int"), ("bar", "str")]) 66 66 | @@ -546,7 +546,7 @@ UP037.py:67:45: UP037 [*] Remove quotes from type annotation | = help: Remove quotes -ℹ Suggested fix +ℹ Fix 64 64 | 65 65 | x: NamedTuple("X", fields=[("foo", "int"), ("bar", "str")]) 66 66 | diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP039.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP039.py.snap new file mode 100644 index 0000000000..4d1a0bb04f --- /dev/null +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP039.py.snap @@ -0,0 +1,97 @@ +--- +source: crates/ruff/src/rules/pyupgrade/mod.rs +--- +UP039.py:2:8: UP039 [*] Unnecessary parentheses after class definition + | +1 | # Errors +2 | class A(): + | ^^ UP039 +3 | pass + | + = help: Remove parentheses + +ℹ Fix +1 1 | # Errors +2 |-class A(): + 2 |+class A: +3 3 | pass +4 4 | +5 5 | + +UP039.py:6:8: UP039 [*] Unnecessary parentheses after class definition + | +6 | class A() \ + | ^^ UP039 +7 | : +8 | pass + | + = help: Remove parentheses + +ℹ Fix +3 3 | pass +4 4 | +5 5 | +6 |-class A() \ + 6 |+class A \ +7 7 | : +8 8 | pass +9 9 | + +UP039.py:12:9: UP039 [*] Unnecessary parentheses after class definition + | +11 | class A \ +12 | (): + | ^^ UP039 +13 | pass + | + = help: Remove parentheses + +ℹ Fix +9 9 | +10 10 | +11 11 | class A \ +12 |- (): + 12 |+ : +13 13 | pass +14 14 | +15 15 | + +UP039.py:17:8: UP039 [*] Unnecessary parentheses after class definition + | +16 | @decorator() +17 | class A(): + | ^^ UP039 +18 | pass + | + = help: Remove parentheses + +ℹ Fix +14 14 | +15 15 | +16 16 | @decorator() +17 |-class A(): + 17 |+class A: +18 18 | pass +19 19 | +20 20 | @decorator + +UP039.py:21:8: UP039 [*] Unnecessary parentheses after class definition + | +20 | @decorator +21 | class A(): + | ^^ UP039 +22 | pass + | + = help: Remove parentheses + +ℹ Fix +18 18 | pass +19 19 | +20 20 | @decorator +21 |-class A(): + 21 |+class A: +22 22 | pass +23 23 | +24 24 | # OK + + diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__datetime_utc_alias_py311.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__datetime_utc_alias_py311.snap index f6a18934b0..7c1bbecb22 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__datetime_utc_alias_py311.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__datetime_utc_alias_py311.snap @@ -1,7 +1,7 @@ --- source: crates/ruff/src/rules/pyupgrade/mod.rs --- -UP017.py:7:7: UP017 Use `datetime.UTC` alias +UP017.py:7:7: UP017 [*] Use `datetime.UTC` alias | 6 | print(datetime.timezone(-1)) 7 | print(timezone.utc) @@ -10,7 +10,17 @@ UP017.py:7:7: UP017 Use `datetime.UTC` alias | = help: Convert to `datetime.UTC` alias -UP017.py:8:7: UP017 Use `datetime.UTC` alias +ℹ Suggested fix +4 4 | from datetime import timezone as tz +5 5 | +6 6 | print(datetime.timezone(-1)) +7 |-print(timezone.utc) + 7 |+print(datetime.UTC) +8 8 | print(tz.utc) +9 9 | +10 10 | print(datetime.timezone.utc) + +UP017.py:8:7: UP017 [*] Use `datetime.UTC` alias | 6 | print(datetime.timezone(-1)) 7 | print(timezone.utc) @@ -21,6 +31,16 @@ UP017.py:8:7: UP017 Use `datetime.UTC` alias | = help: Convert to `datetime.UTC` alias +ℹ Suggested fix +5 5 | +6 6 | print(datetime.timezone(-1)) +7 7 | print(timezone.utc) +8 |-print(tz.utc) + 8 |+print(datetime.UTC) +9 9 | +10 10 | print(datetime.timezone.utc) +11 11 | print(dt.timezone.utc) + UP017.py:10:7: UP017 [*] Use `datetime.UTC` alias | 8 | print(tz.utc) @@ -39,7 +59,7 @@ UP017.py:10:7: UP017 [*] Use `datetime.UTC` alias 10 |+print(datetime.UTC) 11 11 | print(dt.timezone.utc) -UP017.py:11:7: UP017 Use `datetime.UTC` alias +UP017.py:11:7: UP017 [*] Use `datetime.UTC` alias | 10 | print(datetime.timezone.utc) 11 | print(dt.timezone.utc) @@ -47,4 +67,11 @@ UP017.py:11:7: UP017 Use `datetime.UTC` alias | = help: Convert to `datetime.UTC` alias +ℹ Suggested fix +8 8 | print(tz.utc) +9 9 | +10 10 | print(datetime.timezone.utc) +11 |-print(dt.timezone.utc) + 11 |+print(datetime.UTC) + diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__future_annotations_keep_runtime_typing_p310.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__future_annotations_keep_runtime_typing_p310.snap new file mode 100644 index 0000000000..c9972fb6d9 --- /dev/null +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__future_annotations_keep_runtime_typing_p310.snap @@ -0,0 +1,75 @@ +--- +source: crates/ruff/src/rules/pyupgrade/mod.rs +--- +future_annotations.py:34:18: UP006 [*] Use `list` instead of `List` for type annotation + | +34 | def f(x: int) -> List[int]: + | ^^^^ UP006 +35 | y = List[int]() +36 | y.append(x) + | + = help: Replace with `list` + +ℹ Fix +31 31 | return cls(x=0, y=0) +32 32 | +33 33 | +34 |-def f(x: int) -> List[int]: + 34 |+def f(x: int) -> list[int]: +35 35 | y = List[int]() +36 36 | y.append(x) +37 37 | return y + +future_annotations.py:35:9: UP006 [*] Use `list` instead of `List` for type annotation + | +34 | def f(x: int) -> List[int]: +35 | y = List[int]() + | ^^^^ UP006 +36 | y.append(x) +37 | return y + | + = help: Replace with `list` + +ℹ Fix +32 32 | +33 33 | +34 34 | def f(x: int) -> List[int]: +35 |- y = List[int]() + 35 |+ y = list[int]() +36 36 | y.append(x) +37 37 | return y +38 38 | + +future_annotations.py:42:27: UP006 [*] Use `list` instead of `List` for type annotation + | +40 | x: Optional[int] = None +41 | +42 | MyList: TypeAlias = Union[List[int], List[str]] + | ^^^^ UP006 + | + = help: Replace with `list` + +ℹ Fix +39 39 | +40 40 | x: Optional[int] = None +41 41 | +42 |-MyList: TypeAlias = Union[List[int], List[str]] + 42 |+MyList: TypeAlias = Union[list[int], List[str]] + +future_annotations.py:42:38: UP006 [*] Use `list` instead of `List` for type annotation + | +40 | x: Optional[int] = None +41 | +42 | MyList: TypeAlias = Union[List[int], List[str]] + | ^^^^ UP006 + | + = help: Replace with `list` + +ℹ Fix +39 39 | +40 40 | x: Optional[int] = None +41 41 | +42 |-MyList: TypeAlias = Union[List[int], List[str]] + 42 |+MyList: TypeAlias = Union[List[int], list[str]] + + diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__future_annotations_keep_runtime_typing_p37.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__future_annotations_keep_runtime_typing_p37.snap new file mode 100644 index 0000000000..870ad3bf5d --- /dev/null +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__future_annotations_keep_runtime_typing_p37.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/pyupgrade/mod.rs +--- + diff --git a/crates/ruff/src/rules/ruff/mod.rs b/crates/ruff/src/rules/ruff/mod.rs index 66948c9d3e..79062bc839 100644 --- a/crates/ruff/src/rules/ruff/mod.rs +++ b/crates/ruff/src/rules/ruff/mod.rs @@ -1,6 +1,7 @@ //! Ruff-specific rules. pub(crate) mod rules; +pub(crate) mod typing; #[cfg(test)] mod tests { @@ -16,14 +17,29 @@ mod tests { use crate::pyproject_toml::lint_pyproject_toml; use crate::registry::Rule; use crate::settings::resolve_per_file_ignores; - use crate::settings::types::PerFileIgnore; + use crate::settings::types::{PerFileIgnore, PythonVersion}; use crate::test::{test_path, test_resource_path}; use crate::{assert_messages, settings}; - #[test_case(Rule::StaticKeyDictComprehension, Path::new("RUF011.py"))] - #[test_case(Rule::ExplicitFStringTypeConversion, Path::new("RUF010.py"))] - #[test_case(Rule::CollectionLiteralConcatenation, Path::new("RUF005.py"))] #[test_case(Rule::AsyncioDanglingTask, Path::new("RUF006.py"))] + #[test_case(Rule::CollectionLiteralConcatenation, Path::new("RUF005.py"))] + #[test_case(Rule::ExplicitFStringTypeConversion, Path::new("RUF010.py"))] + #[test_case(Rule::FunctionCallInDataclassDefaultArgument, Path::new("RUF009.py"))] + #[test_case(Rule::ImplicitOptional, Path::new("RUF013_0.py"))] + #[test_case(Rule::ImplicitOptional, Path::new("RUF013_1.py"))] + #[test_case(Rule::MutableClassDefault, Path::new("RUF012.py"))] + #[test_case(Rule::MutableDataclassDefault, Path::new("RUF008.py"))] + #[test_case(Rule::PairwiseOverZipped, Path::new("RUF007.py"))] + #[test_case(Rule::StaticKeyDictComprehension, Path::new("RUF011.py"))] + #[test_case( + Rule::UnnecessaryIterableAllocationForFirstElement, + Path::new("RUF015.py") + )] + #[test_case(Rule::InvalidIndexType, Path::new("RUF016.py"))] + #[cfg_attr( + feature = "unreachable-code", + test_case(Rule::UnreachableCode, Path::new("RUF014.py")) + )] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); let diagnostics = test_path( @@ -34,6 +50,25 @@ mod tests { Ok(()) } + #[test_case(Path::new("RUF013_0.py"))] + #[test_case(Path::new("RUF013_1.py"))] + fn implicit_optional_py39(path: &Path) -> Result<()> { + let snapshot = format!( + "PY39_{}_{}", + Rule::ImplicitOptional.noqa_code(), + path.to_string_lossy() + ); + let diagnostics = test_path( + Path::new("ruff").join(path).as_path(), + &settings::Settings { + target_version: PythonVersion::Py39, + ..settings::Settings::for_rule(Rule::ImplicitOptional) + }, + )?; + assert_messages!(snapshot, diagnostics); + Ok(()) + } + #[test] fn confusables() -> Result<()> { let diagnostics = test_path( @@ -65,13 +100,16 @@ mod tests { fn ruf100_0() -> Result<()> { let diagnostics = test_path( Path::new("ruff/RUF100_0.py"), - &settings::Settings::for_rules(vec![ - Rule::UnusedNOQA, - Rule::LineTooLong, - Rule::UnusedImport, - Rule::UnusedVariable, - Rule::TabIndentation, - ]), + &settings::Settings { + external: FxHashSet::from_iter(vec!["V101".to_string()]), + ..settings::Settings::for_rules(vec![ + Rule::UnusedNOQA, + Rule::LineTooLong, + Rule::UnusedImport, + Rule::UnusedVariable, + Rule::TabIndentation, + ]) + }, )?; assert_messages!(diagnostics); Ok(()) @@ -158,28 +196,6 @@ mod tests { Ok(()) } - #[test] - fn ruff_pairwise_over_zipped() -> Result<()> { - let diagnostics = test_path( - Path::new("ruff/RUF007.py"), - &settings::Settings::for_rules(vec![Rule::PairwiseOverZipped]), - )?; - assert_messages!(diagnostics); - Ok(()) - } - - #[test_case(Rule::MutableDataclassDefault, Path::new("RUF008.py"))] - #[test_case(Rule::FunctionCallInDataclassDefaultArgument, Path::new("RUF009.py"))] - fn mutable_defaults(rule_code: Rule, path: &Path) -> Result<()> { - let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); - let diagnostics = test_path( - Path::new("ruff").join(path).as_path(), - &settings::Settings::for_rule(rule_code), - )?; - assert_messages!(snapshot, diagnostics); - Ok(()) - } - #[test_case(Rule::InvalidPyprojectToml, Path::new("bleach"))] #[test_case(Rule::InvalidPyprojectToml, Path::new("invalid_author"))] #[test_case(Rule::InvalidPyprojectToml, Path::new("maturin"))] @@ -193,7 +209,10 @@ mod tests { .join("pyproject.toml"); let contents = fs::read_to_string(path)?; let source_file = SourceFileBuilder::new("pyproject.toml", contents).finish(); - let messages = lint_pyproject_toml(source_file)?; + let messages = lint_pyproject_toml( + source_file, + &settings::Settings::for_rule(Rule::InvalidPyprojectToml), + )?; assert_messages!(snapshot, messages); Ok(()) } diff --git a/crates/ruff/src/rules/ruff/rules/ambiguous_unicode_character.rs b/crates/ruff/src/rules/ruff/rules/ambiguous_unicode_character.rs index 65d05f9095..132814af1b 100644 --- a/crates/ruff/src/rules/ruff/rules/ambiguous_unicode_character.rs +++ b/crates/ruff/src/rules/ruff/rules/ambiguous_unicode_character.rs @@ -11,6 +11,22 @@ use crate::rules::ruff::rules::confusables::CONFUSABLES; use crate::rules::ruff::rules::Context; use crate::settings::Settings; +/// ## What it does +/// Checks for ambiguous unicode characters in strings. +/// +/// ## Why is this bad? +/// The use of ambiguous unicode characters can confuse readers and cause +/// subtle bugs. +/// +/// ## Example +/// ```python +/// print("Ηello, world!") # "Η" is the Greek eta (`U+0397`). +/// ``` +/// +/// Use instead: +/// ```python +/// print("Hello, world!") # "H" is the Latin capital H (`U+0048`). +/// ``` #[violation] pub struct AmbiguousUnicodeCharacterString { confusable: char, @@ -44,6 +60,22 @@ impl AlwaysAutofixableViolation for AmbiguousUnicodeCharacterString { } } +/// ## What it does +/// Checks for ambiguous unicode characters in docstrings. +/// +/// ## Why is this bad? +/// The use of ambiguous unicode characters can confuse readers and cause +/// subtle bugs. +/// +/// ## Example +/// ```python +/// """A lovely docstring (with a `U+FF09` parenthesis).""" +/// ``` +/// +/// Use instead: +/// ```python +/// """A lovely docstring (with no strange parentheses).""" +/// ``` #[violation] pub struct AmbiguousUnicodeCharacterDocstring { confusable: char, @@ -77,6 +109,22 @@ impl AlwaysAutofixableViolation for AmbiguousUnicodeCharacterDocstring { } } +/// ## What it does +/// Checks for ambiguous unicode characters in comments. +/// +/// ## Why is this bad? +/// The use of ambiguous unicode characters can confuse readers and cause +/// subtle bugs. +/// +/// ## Example +/// ```python +/// foo() # nоqa # "о" is Cyrillic (`U+043E`) +/// ``` +/// +/// Use instead: +/// ```python +/// foo() # noqa # "o" is Latin (`U+006F`) +/// ``` #[violation] pub struct AmbiguousUnicodeCharacterComment { confusable: char, @@ -111,18 +159,17 @@ impl AlwaysAutofixableViolation for AmbiguousUnicodeCharacterComment { } pub(crate) fn ambiguous_unicode_character( + diagnostics: &mut Vec, locator: &Locator, range: TextRange, context: Context, settings: &Settings, -) -> Vec { - let mut diagnostics = vec![]; - +) { let text = locator.slice(range); // Most of the time, we don't need to check for ambiguous unicode characters at all. if text.is_ascii() { - return diagnostics; + return; } // Iterate over the "words" in the text. @@ -184,8 +231,6 @@ pub(crate) fn ambiguous_unicode_character( } word_candidates.clear(); } - - diagnostics } bitflags! { @@ -256,8 +301,7 @@ impl Candidate { ); if settings.rules.enabled(diagnostic.kind.rule()) { if settings.rules.should_fix(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.set_fix(Fix::unspecified(Edit::range_replacement( + diagnostic.set_fix(Fix::manual(Edit::range_replacement( self.representant.to_string(), char_range, ))); diff --git a/crates/ruff/src/rules/ruff/rules/collection_literal_concatenation.rs b/crates/ruff/src/rules/ruff/rules/collection_literal_concatenation.rs index c5b2196d53..2a9c25725b 100644 --- a/crates/ruff/src/rules/ruff/rules/collection_literal_concatenation.rs +++ b/crates/ruff/src/rules/ruff/rules/collection_literal_concatenation.rs @@ -8,6 +8,36 @@ use ruff_python_ast::helpers::has_comments; use crate::checkers::ast::Checker; use crate::registry::AsRule; +/// ## What it does +/// Checks for uses of the `+` operator to concatenate collections. +/// +/// ## Why is this bad? +/// In Python, the `+` operator can be used to concatenate collections (e.g., +/// `x + y` to concatenate the lists `x` and `y`). +/// +/// However, collections can be concatenated more efficiently using the +/// unpacking operator (e.g., `[*x, *y]` to concatenate `x` and `y`). +/// +/// Prefer the unpacking operator to concatenate collections, as it is more +/// readable and flexible. The `*` operator can unpack any iterable, whereas +/// `+` operates only on particular sequences which, in many cases, must be of +/// the same type. +/// +/// ## Example +/// ```python +/// foo = [2, 3, 4] +/// bar = [1] + foo + [5, 6] +/// ``` +/// +/// Use instead: +/// ```python +/// foo = [2, 3, 4] +/// bar = [1, *foo, 5, 6] +/// ``` +/// +/// ## References +/// - [PEP 448 – Additional Unpacking Generalizations](https://peps.python.org/pep-0448/) +/// - [Python documentation: Sequence Types — `list`, `tuple`, `range`](https://docs.python.org/3/library/stdtypes.html#sequence-types-list-tuple-range) #[violation] pub struct CollectionLiteralConcatenation { expr: String, @@ -56,7 +86,13 @@ enum Type { /// Recursively merge all the tuples and lists in the expression. fn concatenate_expressions(expr: &Expr) -> Option<(Expr, Type)> { - let Expr::BinOp(ast::ExprBinOp { left, op: Operator::Add, right, range: _ }) = expr else { + let Expr::BinOp(ast::ExprBinOp { + left, + op: Operator::Add, + right, + range: _, + }) = expr + else { return None; }; @@ -131,7 +167,7 @@ fn concatenate_expressions(expr: &Expr) -> Option<(Expr, Type)> { pub(crate) fn collection_literal_concatenation(checker: &mut Checker, expr: &Expr) { // If the expression is already a child of an addition, we'll have analyzed it already. if matches!( - checker.semantic_model().expr_parent(), + checker.semantic().expr_parent(), Some(Expr::BinOp(ast::ExprBinOp { op: Operator::Add, .. @@ -141,7 +177,7 @@ pub(crate) fn collection_literal_concatenation(checker: &mut Checker, expr: &Exp } let Some((new_expr, type_)) = concatenate_expressions(expr) else { - return + return; }; let contents = match type_ { diff --git a/crates/ruff/src/rules/ruff/rules/explicit_f_string_type_conversion.rs b/crates/ruff/src/rules/ruff/rules/explicit_f_string_type_conversion.rs index 30046ab720..408c4ffbcb 100644 --- a/crates/ruff/src/rules/ruff/rules/explicit_f_string_type_conversion.rs +++ b/crates/ruff/src/rules/ruff/rules/explicit_f_string_type_conversion.rs @@ -6,7 +6,6 @@ use rustpython_parser::ast::{self, Expr, Ranged}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::prelude::ConversionFlag; use ruff_python_ast::source_code::{Locator, Stylist}; use crate::autofix::codemods::CodegenStylist; @@ -15,69 +14,43 @@ use crate::cst::matchers::{match_call_mut, match_expression, match_name}; use crate::registry::AsRule; /// ## What it does -/// Checks for usages of `str()`, `repr()`, and `ascii()` as explicit type +/// Checks for uses of `str()`, `repr()`, and `ascii()` as explicit type /// conversions within f-strings. /// /// ## Why is this bad? /// f-strings support dedicated conversion flags for these types, which are /// more succinct and idiomatic. /// -/// In the case of `str()`, it's also redundant, since `str()` is the default -/// conversion. +/// Note that, in many cases, calling `str()` within an f-string is +/// unnecessary and can be removed entirely, as the value will be converted +/// to a string automatically, the notable exception being for classes that +/// implement a custom `__format__` method. /// /// ## Example /// ```python /// a = "some string" -/// f"{str(a)}" /// f"{repr(a)}" /// ``` /// /// Use instead: /// ```python /// a = "some string" -/// f"{a}" /// f"{a!r}" /// ``` #[violation] -pub struct ExplicitFStringTypeConversion { - operation: Operation, -} +pub struct ExplicitFStringTypeConversion; impl AlwaysAutofixableViolation for ExplicitFStringTypeConversion { #[derive_message_formats] fn message(&self) -> String { - let ExplicitFStringTypeConversion { operation } = self; - match operation { - Operation::ConvertCallToConversionFlag => { - format!("Use explicit conversion flag") - } - Operation::RemoveCall => format!("Remove unnecessary `str` conversion"), - Operation::RemoveConversionFlag => format!("Remove unnecessary conversion flag"), - } + format!("Use explicit conversion flag") } fn autofix_title(&self) -> String { - let ExplicitFStringTypeConversion { operation } = self; - match operation { - Operation::ConvertCallToConversionFlag => { - format!("Replace with conversion flag") - } - Operation::RemoveCall => format!("Remove `str` call"), - Operation::RemoveConversionFlag => format!("Remove conversion flag"), - } + "Replace with conversion flag".to_string() } } -#[derive(Debug, PartialEq, Eq)] -enum Operation { - /// Ex) Convert `f"{repr(bla)}"` to `f"{bla!r}"` - ConvertCallToConversionFlag, - /// Ex) Convert `f"{bla!s}"` to `f"{bla}"` - RemoveConversionFlag, - /// Ex) Convert `f"{str(bla)}"` to `f"{bla}"` - RemoveCall, -} - /// RUF010 pub(crate) fn explicit_f_string_type_conversion( checker: &mut Checker, @@ -96,156 +69,64 @@ pub(crate) fn explicit_f_string_type_conversion( .enumerate() { let ast::ExprFormattedValue { - value, - conversion, - format_spec, - range: _, + value, conversion, .. } = formatted_value; - match conversion { - ConversionFlag::Ascii | ConversionFlag::Repr => { - // Nothing to do. - continue; - } - ConversionFlag::Str => { - // Skip if there's a format spec. - if format_spec.is_some() { - continue; - } - - // Remove the conversion flag entirely. - // Ex) `f"{bla!s}"` - let mut diagnostic = Diagnostic::new( - ExplicitFStringTypeConversion { - operation: Operation::RemoveConversionFlag, - }, - value.range(), - ); - if checker.patch(diagnostic.kind.rule()) { - diagnostic.try_set_fix(|| { - remove_conversion_flag(expr, index, checker.locator, checker.stylist) - }); - } - checker.diagnostics.push(diagnostic); - } - ConversionFlag::None => { - // Replace with the appropriate conversion flag. - let Expr::Call(ast::ExprCall { - func, - args, - keywords, - .. - }) = value.as_ref() else { - continue; - }; - - // Can't be a conversion otherwise. - if args.len() != 1 || !keywords.is_empty() { - continue; - } - - let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() else { - continue; - }; - - if !matches!(id.as_str(), "str" | "repr" | "ascii") { - continue; - }; - - if !checker.semantic_model().is_builtin(id) { - continue; - } - - if id == "str" && format_spec.is_none() { - // Ex) `f"{str(bla)}"` - let mut diagnostic = Diagnostic::new( - ExplicitFStringTypeConversion { - operation: Operation::RemoveCall, - }, - value.range(), - ); - if checker.patch(diagnostic.kind.rule()) { - diagnostic.try_set_fix(|| { - remove_conversion_call(expr, index, checker.locator, checker.stylist) - }); - } - checker.diagnostics.push(diagnostic); - } else { - // Ex) `f"{repr(bla)}"` - let mut diagnostic = Diagnostic::new( - ExplicitFStringTypeConversion { - operation: Operation::ConvertCallToConversionFlag, - }, - value.range(), - ); - if checker.patch(diagnostic.kind.rule()) { - diagnostic.try_set_fix(|| { - convert_call_to_conversion_flag( - expr, - index, - checker.locator, - checker.stylist, - ) - }); - } - checker.diagnostics.push(diagnostic); - } - } + // Skip if there's already a conversion flag. + if !conversion.is_none() { + continue; } + + let Expr::Call(ast::ExprCall { + func, + args, + keywords, + .. + }) = value.as_ref() + else { + continue; + }; + + // Can't be a conversion otherwise. + if !keywords.is_empty() { + continue; + } + + // Can't be a conversion otherwise. + let [arg] = args.as_slice() else { + continue; + }; + + // Avoid attempting to rewrite, e.g., `f"{str({})}"`; the curly braces are problematic. + if matches!( + arg, + Expr::Dict(_) | Expr::Set(_) | Expr::DictComp(_) | Expr::SetComp(_) + ) { + continue; + } + + let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() else { + continue; + }; + + if !matches!(id.as_str(), "str" | "repr" | "ascii") { + continue; + }; + + if !checker.semantic().is_builtin(id) { + continue; + } + + let mut diagnostic = Diagnostic::new(ExplicitFStringTypeConversion, value.range()); + if checker.patch(diagnostic.kind.rule()) { + diagnostic.try_set_fix(|| { + convert_call_to_conversion_flag(expr, index, checker.locator, checker.stylist) + }); + } + checker.diagnostics.push(diagnostic); } } -/// Generate a [`Fix`] to remove a conversion flag from a formatted expression. -fn remove_conversion_flag( - expr: &Expr, - index: usize, - locator: &Locator, - stylist: &Stylist, -) -> Result { - // Parenthesize the expression, to support implicit concatenation. - let range = expr.range(); - let content = locator.slice(range); - let parenthesized_content = format!("({content})"); - let mut expression = match_expression(&parenthesized_content)?; - - // Replace the formatted call expression at `index` with a conversion flag. - let formatted_string_expression = match_part(index, &mut expression)?; - formatted_string_expression.conversion = None; - - // Remove the parentheses (first and last characters). - let mut content = expression.codegen_stylist(stylist); - content.remove(0); - content.pop(); - - Ok(Fix::automatic(Edit::range_replacement(content, range))) -} - -/// Generate a [`Fix`] to remove a call from a formatted expression. -fn remove_conversion_call( - expr: &Expr, - index: usize, - locator: &Locator, - stylist: &Stylist, -) -> Result { - // Parenthesize the expression, to support implicit concatenation. - let range = expr.range(); - let content = locator.slice(range); - let parenthesized_content = format!("({content})"); - let mut expression = match_expression(&parenthesized_content)?; - - // Replace the formatted call expression at `index` with a conversion flag. - let formatted_string_expression = match_part(index, &mut expression)?; - let call = match_call_mut(&mut formatted_string_expression.expression)?; - formatted_string_expression.expression = call.args[0].value.clone(); - - // Remove the parentheses (first and last characters). - let mut content = expression.codegen_stylist(stylist); - content.remove(0); - content.pop(); - - Ok(Fix::automatic(Edit::range_replacement(content, range))) -} - /// Generate a [`Fix`] to replace an explicit type conversion with a conversion flag. fn convert_call_to_conversion_flag( expr: &Expr, diff --git a/crates/ruff/src/rules/ruff/rules/function_call_in_dataclass_default.rs b/crates/ruff/src/rules/ruff/rules/function_call_in_dataclass_default.rs new file mode 100644 index 0000000000..3b52cac788 --- /dev/null +++ b/crates/ruff/src/rules/ruff/rules/function_call_in_dataclass_default.rs @@ -0,0 +1,113 @@ +use rustpython_parser::ast::{self, Expr, Ranged, Stmt}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::call_path::compose_call_path; +use ruff_python_ast::call_path::{from_qualified_name, CallPath}; +use ruff_python_semantic::analyze::typing::is_immutable_func; + +use crate::checkers::ast::Checker; +use crate::rules::ruff::rules::helpers::{ + is_class_var_annotation, is_dataclass, is_dataclass_field, is_descriptor_class, +}; + +/// ## What it does +/// Checks for function calls in dataclass attribute defaults. +/// +/// ## Why is this bad? +/// Function calls are only performed once, at definition time. The returned +/// value is then reused by all instances of the dataclass. This can lead to +/// unexpected behavior when the function call returns a mutable object, as +/// changes to the object will be shared across all instances. +/// +/// If a field needs to be initialized with a mutable object, use the +/// `field(default_factory=...)` pattern. +/// +/// ## Examples +/// ```python +/// from dataclasses import dataclass +/// +/// +/// def simple_list() -> list[int]: +/// return [1, 2, 3, 4] +/// +/// +/// @dataclass +/// class A: +/// mutable_default: list[int] = simple_list() +/// ``` +/// +/// Use instead: +/// ```python +/// from dataclasses import dataclass, field +/// +/// +/// def creating_list() -> list[int]: +/// return [1, 2, 3, 4] +/// +/// +/// @dataclass +/// class A: +/// mutable_default: list[int] = field(default_factory=creating_list) +/// ``` +/// +/// ## Options +/// - `flake8-bugbear.extend-immutable-calls` +#[violation] +pub struct FunctionCallInDataclassDefaultArgument { + name: Option, +} + +impl Violation for FunctionCallInDataclassDefaultArgument { + #[derive_message_formats] + fn message(&self) -> String { + let FunctionCallInDataclassDefaultArgument { name } = self; + if let Some(name) = name { + format!("Do not perform function call `{name}` in dataclass defaults") + } else { + format!("Do not perform function call in dataclass defaults") + } + } +} + +/// RUF009 +pub(crate) fn function_call_in_dataclass_default( + checker: &mut Checker, + class_def: &ast::StmtClassDef, +) { + if !is_dataclass(class_def, checker.semantic()) { + return; + } + + let extend_immutable_calls: Vec = checker + .settings + .flake8_bugbear + .extend_immutable_calls + .iter() + .map(|target| from_qualified_name(target)) + .collect(); + + for statement in &class_def.body { + if let Stmt::AnnAssign(ast::StmtAnnAssign { + annotation, + value: Some(expr), + .. + }) = statement + { + if let Expr::Call(ast::ExprCall { func, .. }) = expr.as_ref() { + if !is_class_var_annotation(annotation, checker.semantic()) + && !is_immutable_func(func, checker.semantic(), &extend_immutable_calls) + && !is_dataclass_field(func, checker.semantic()) + && !is_descriptor_class(func, checker.semantic()) + { + checker.diagnostics.push(Diagnostic::new( + FunctionCallInDataclassDefaultArgument { + name: compose_call_path(func), + }, + expr.range(), + )); + } + } + } + } +} diff --git a/crates/ruff/src/rules/ruff/rules/helpers.rs b/crates/ruff/src/rules/ruff/rules/helpers.rs new file mode 100644 index 0000000000..8ba7e01624 --- /dev/null +++ b/crates/ruff/src/rules/ruff/rules/helpers.rs @@ -0,0 +1,85 @@ +use rustpython_parser::ast::{self, Expr}; + +use ruff_python_ast::helpers::map_callable; +use ruff_python_semantic::{BindingKind, SemanticModel}; + +/// Return `true` if the given [`Expr`] is a special class attribute, like `__slots__`. +/// +/// While `__slots__` is typically defined via a tuple, Python accepts any iterable and, in +/// particular, allows the use of a dictionary to define the attribute names (as keys) and +/// docstrings (as values). +pub(super) fn is_special_attribute(value: &Expr) -> bool { + if let Expr::Name(ast::ExprName { id, .. }) = value { + matches!( + id.as_str(), + "__slots__" | "__dict__" | "__weakref__" | "__annotations__" + ) + } else { + false + } +} + +/// Returns `true` if the given [`Expr`] is a `dataclasses.field` call. +pub(super) fn is_dataclass_field(func: &Expr, semantic: &SemanticModel) -> bool { + semantic.resolve_call_path(func).map_or(false, |call_path| { + matches!(call_path.as_slice(), ["dataclasses", "field"]) + }) +} + +/// Returns `true` if the given [`Expr`] is a `typing.ClassVar` annotation. +pub(super) fn is_class_var_annotation(annotation: &Expr, semantic: &SemanticModel) -> bool { + let Expr::Subscript(ast::ExprSubscript { value, .. }) = &annotation else { + return false; + }; + semantic.match_typing_expr(value, "ClassVar") +} + +/// Returns `true` if the given [`Expr`] is a `typing.Final` annotation. +pub(super) fn is_final_annotation(annotation: &Expr, semantic: &SemanticModel) -> bool { + let Expr::Subscript(ast::ExprSubscript { value, .. }) = &annotation else { + return false; + }; + semantic.match_typing_expr(value, "Final") +} + +/// Returns `true` if the given class is a dataclass. +pub(super) fn is_dataclass(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool { + class_def.decorator_list.iter().any(|decorator| { + semantic + .resolve_call_path(map_callable(&decorator.expression)) + .map_or(false, |call_path| { + matches!(call_path.as_slice(), ["dataclasses", "dataclass"]) + }) + }) +} + +/// Returns `true` if the given class is a Pydantic `BaseModel` or `BaseSettings` subclass. +pub(super) fn is_pydantic_model(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool { + class_def.bases.iter().any(|expr| { + semantic.resolve_call_path(expr).map_or(false, |call_path| { + matches!( + call_path.as_slice(), + ["pydantic", "BaseModel" | "BaseSettings"] + ) + }) + }) +} + +/// Returns `true` if the given function is an instantiation of a class that implements the +/// descriptor protocol. +/// +/// See: +pub(super) fn is_descriptor_class(func: &Expr, semantic: &SemanticModel) -> bool { + semantic.lookup_attribute(func).map_or(false, |id| { + let BindingKind::ClassDefinition(scope_id) = semantic.binding(id).kind else { + return false; + }; + + // Look for `__get__`, `__set__`, and `__delete__` methods. + ["__get__", "__set__", "__delete__"].iter().any(|method| { + semantic.scopes[scope_id].get(method).map_or(false, |id| { + semantic.binding(id).kind.is_function_definition() + }) + }) + }) +} diff --git a/crates/ruff/src/rules/ruff/rules/implicit_optional.rs b/crates/ruff/src/rules/ruff/rules/implicit_optional.rs new file mode 100644 index 0000000000..d9f03e2699 --- /dev/null +++ b/crates/ruff/src/rules/ruff/rules/implicit_optional.rs @@ -0,0 +1,234 @@ +use std::fmt; + +use anyhow::Result; +use ruff_text_size::TextRange; +use rustpython_parser::ast::{self, ArgWithDefault, Arguments, Constant, Expr, Operator, Ranged}; + +use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers::is_const_none; +use ruff_python_ast::typing::parse_type_annotation; + +use crate::checkers::ast::Checker; +use crate::importer::ImportRequest; +use crate::registry::AsRule; +use crate::settings::types::PythonVersion; + +use super::super::typing::type_hint_explicitly_allows_none; + +/// ## What it does +/// Checks for the use of implicit `Optional` in type annotations when the +/// default parameter value is `None`. +/// +/// ## Why is this bad? +/// Implicit `Optional` is prohibited by [PEP 484]. It is confusing and +/// inconsistent with the rest of the type system. +/// +/// It's recommended to use `Optional[T]` instead. For Python 3.10 and later, +/// you can also use `T | None`. +/// +/// ## Example +/// ```python +/// def foo(arg: int = None): +/// pass +/// ``` +/// +/// Use instead: +/// ```python +/// from typing import Optional +/// +/// +/// def foo(arg: Optional[int] = None): +/// pass +/// ``` +/// +/// Or, for Python 3.10 and later: +/// ```python +/// def foo(arg: int | None = None): +/// pass +/// ``` +/// +/// If you want to use the `|` operator in Python 3.9 and earlier, you can +/// use future imports: +/// ```python +/// from __future__ import annotations +/// +/// +/// def foo(arg: int | None = None): +/// pass +/// ``` +/// +/// ## Limitations +/// +/// Type aliases are not supported and could result in false negatives. +/// For example, the following code will not be flagged: +/// ```python +/// Text = str | bytes +/// +/// +/// def foo(arg: Text = None): +/// pass +/// ``` +/// +/// ## Options +/// - `target-version` +/// +/// [PEP 484]: https://peps.python.org/pep-0484/#union-types +#[violation] +pub struct ImplicitOptional { + conversion_type: ConversionType, +} + +impl Violation for ImplicitOptional { + const AUTOFIX: AutofixKind = AutofixKind::Sometimes; + + #[derive_message_formats] + fn message(&self) -> String { + format!("PEP 484 prohibits implicit `Optional`") + } + + fn autofix_title(&self) -> Option { + Some(format!("Convert to `{}`", self.conversion_type)) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ConversionType { + /// Conversion using the `|` operator e.g., `str | None` + BinOpOr, + /// Conversion using the `typing.Optional` type e.g., `typing.Optional[str]` + Optional, +} + +impl fmt::Display for ConversionType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::BinOpOr => f.write_str("T | None"), + Self::Optional => f.write_str("Optional[T]"), + } + } +} + +impl From for ConversionType { + fn from(target_version: PythonVersion) -> Self { + if target_version >= PythonVersion::Py310 { + Self::BinOpOr + } else { + Self::Optional + } + } +} + +/// Generate a [`Fix`] for the given [`Expr`] as per the [`ConversionType`]. +fn generate_fix(checker: &Checker, conversion_type: ConversionType, expr: &Expr) -> Result { + match conversion_type { + ConversionType::BinOpOr => { + let new_expr = Expr::BinOp(ast::ExprBinOp { + left: Box::new(expr.clone()), + op: Operator::BitOr, + right: Box::new(Expr::Constant(ast::ExprConstant { + value: Constant::None, + kind: None, + range: TextRange::default(), + })), + range: TextRange::default(), + }); + let content = checker.generator().expr(&new_expr); + Ok(Fix::suggested(Edit::range_replacement( + content, + expr.range(), + ))) + } + ConversionType::Optional => { + let (import_edit, binding) = checker.importer.get_or_import_symbol( + &ImportRequest::import_from("typing", "Optional"), + expr.start(), + checker.semantic(), + )?; + let new_expr = Expr::Subscript(ast::ExprSubscript { + range: TextRange::default(), + value: Box::new(Expr::Name(ast::ExprName { + id: binding, + ctx: ast::ExprContext::Store, + range: TextRange::default(), + })), + slice: Box::new(expr.clone()), + ctx: ast::ExprContext::Load, + }); + let content = checker.generator().expr(&new_expr); + Ok(Fix::suggested_edits( + Edit::range_replacement(content, expr.range()), + [import_edit], + )) + } + } +} + +/// RUF013 +pub(crate) fn implicit_optional(checker: &mut Checker, arguments: &Arguments) { + for ArgWithDefault { + def, + default, + range: _, + } in arguments + .posonlyargs + .iter() + .chain(&arguments.args) + .chain(&arguments.kwonlyargs) + { + let Some(default) = default else { continue }; + if !is_const_none(default) { + continue; + } + let Some(annotation) = &def.annotation else { + continue; + }; + + if let Expr::Constant(ast::ExprConstant { + range, + value: Constant::Str(string), + .. + }) = annotation.as_ref() + { + // Quoted annotation. + if let Ok((annotation, kind)) = parse_type_annotation(string, *range, checker.locator) { + let Some(expr) = type_hint_explicitly_allows_none( + &annotation, + checker.semantic(), + checker.locator, + checker.settings.target_version.minor(), + ) else { + continue; + }; + let conversion_type = checker.settings.target_version.into(); + + let mut diagnostic = + Diagnostic::new(ImplicitOptional { conversion_type }, expr.range()); + if checker.patch(diagnostic.kind.rule()) { + if kind.is_simple() { + diagnostic.try_set_fix(|| generate_fix(checker, conversion_type, expr)); + } + } + checker.diagnostics.push(diagnostic); + } + } else { + // Unquoted annotation. + let Some(expr) = type_hint_explicitly_allows_none( + annotation, + checker.semantic(), + checker.locator, + checker.settings.target_version.minor(), + ) else { + continue; + }; + let conversion_type = checker.settings.target_version.into(); + + let mut diagnostic = + Diagnostic::new(ImplicitOptional { conversion_type }, expr.range()); + if checker.patch(diagnostic.kind.rule()) { + diagnostic.try_set_fix(|| generate_fix(checker, conversion_type, expr)); + } + checker.diagnostics.push(diagnostic); + } + } +} diff --git a/crates/ruff/src/rules/ruff/rules/invalid_index_type.rs b/crates/ruff/src/rules/ruff/rules/invalid_index_type.rs new file mode 100644 index 0000000000..b8e832a62e --- /dev/null +++ b/crates/ruff/src/rules/ruff/rules/invalid_index_type.rs @@ -0,0 +1,217 @@ +use rustpython_parser::ast::{Constant, Expr, ExprConstant, ExprSlice, ExprSubscript, Ranged}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use std::fmt; + +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for indexed access to lists, strings, tuples, bytes, and comprehensions +/// using a type other than an integer or slice. +/// +/// ## Why is this bad? +/// Only integers or slices can be used as indices to these types. Using +/// other types will result in a `TypeError` at runtime and a `SyntaxWarning` at +/// import time. +/// +/// ## Example +/// ```python +/// var = [1, 2, 3]["x"] +/// ``` +/// +/// Use instead: +/// ```python +/// var = [1, 2, 3][0] +/// ``` +#[violation] +pub struct InvalidIndexType { + value_type: String, + index_type: String, + is_slice: bool, +} + +impl Violation for InvalidIndexType { + #[derive_message_formats] + fn message(&self) -> String { + let InvalidIndexType { + value_type, + index_type, + is_slice, + } = self; + if *is_slice { + format!("Slice in indexed access to type `{value_type}` uses type `{index_type}` instead of an integer.") + } else { + format!( + "Indexed access to type `{value_type}` uses type `{index_type}` instead of an integer or slice." + ) + } + } +} + +/// RUF015 +pub(crate) fn invalid_index_type(checker: &mut Checker, expr: &ExprSubscript) { + let ExprSubscript { + value, + slice: index, + .. + } = expr; + + // Check the value being indexed is a list, tuple, string, f-string, bytes, or comprehension + if !matches!( + value.as_ref(), + Expr::List(_) + | Expr::ListComp(_) + | Expr::Tuple(_) + | Expr::JoinedStr(_) + | Expr::Constant(ExprConstant { + value: Constant::Str(_) | Constant::Bytes(_), + .. + }) + ) { + return; + } + + // The value types supported by this rule should always be checkable + let Some(value_type) = CheckableExprType::try_from(value) else { + debug_assert!( + false, + "Index value must be a checkable type to generate a violation message." + ); + return; + }; + + // If the index is not a checkable type then we can't easily determine if there is a violation + let Some(index_type) = CheckableExprType::try_from(index) else { + return; + }; + + // Then check the contents of the index + match index.as_ref() { + Expr::Constant(ExprConstant { + value: index_value, .. + }) => { + // If the index is a constant, require an integer + if !index_value.is_int() { + checker.diagnostics.push(Diagnostic::new( + InvalidIndexType { + value_type: value_type.to_string(), + index_type: constant_type_name(index_value).to_string(), + is_slice: false, + }, + index.range(), + )); + } + } + Expr::Slice(ExprSlice { + lower, upper, step, .. + }) => { + // If the index is a slice, require integer or null bounds + for is_slice in [lower, upper, step].into_iter().flatten() { + if let Expr::Constant(ExprConstant { + value: index_value, .. + }) = is_slice.as_ref() + { + if !(index_value.is_int() || index_value.is_none()) { + checker.diagnostics.push(Diagnostic::new( + InvalidIndexType { + value_type: value_type.to_string(), + index_type: constant_type_name(index_value).to_string(), + is_slice: true, + }, + is_slice.range(), + )); + } + } else if let Some(is_slice_type) = CheckableExprType::try_from(is_slice.as_ref()) { + checker.diagnostics.push(Diagnostic::new( + InvalidIndexType { + value_type: value_type.to_string(), + index_type: is_slice_type.to_string(), + is_slice: true, + }, + is_slice.range(), + )); + } + } + } + _ => { + // If it's some other checkable data type, it's a violation + checker.diagnostics.push(Diagnostic::new( + InvalidIndexType { + value_type: value_type.to_string(), + index_type: index_type.to_string(), + is_slice: false, + }, + index.range(), + )); + } + } +} + +/// An expression that can be checked for type compatibility. +/// +/// These are generally "literal" type expressions in that we know their concrete type +/// without additional analysis; opposed to expressions like a function call where we +/// cannot determine what type it may return. +#[derive(Debug)] +enum CheckableExprType<'a> { + Constant(&'a Constant), + JoinedStr, + List, + ListComp, + SetComp, + DictComp, + Set, + Dict, + Tuple, + Slice, +} + +impl fmt::Display for CheckableExprType<'_> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Constant(constant) => f.write_str(constant_type_name(constant)), + Self::JoinedStr => f.write_str("str"), + Self::List => f.write_str("list"), + Self::SetComp => f.write_str("set comprehension"), + Self::ListComp => f.write_str("list comprehension"), + Self::DictComp => f.write_str("dict comprehension"), + Self::Set => f.write_str("set"), + Self::Slice => f.write_str("slice"), + Self::Dict => f.write_str("dict"), + Self::Tuple => f.write_str("tuple"), + } + } +} + +impl<'a> CheckableExprType<'a> { + fn try_from(expr: &'a Expr) -> Option { + match expr { + Expr::Constant(ExprConstant { value, .. }) => Some(Self::Constant(value)), + Expr::JoinedStr(_) => Some(Self::JoinedStr), + Expr::List(_) => Some(Self::List), + Expr::ListComp(_) => Some(Self::ListComp), + Expr::SetComp(_) => Some(Self::SetComp), + Expr::DictComp(_) => Some(Self::DictComp), + Expr::Set(_) => Some(Self::Set), + Expr::Dict(_) => Some(Self::Dict), + Expr::Tuple(_) => Some(Self::Tuple), + Expr::Slice(_) => Some(Self::Slice), + _ => None, + } + } +} + +fn constant_type_name(constant: &Constant) -> &'static str { + match constant { + Constant::None => "None", + Constant::Bool(_) => "bool", + Constant::Str(_) => "str", + Constant::Bytes(_) => "bytes", + Constant::Int(_) => "int", + Constant::Tuple(_) => "tuple", + Constant::Float(_) => "float", + Constant::Complex { .. } => "complex", + Constant::Ellipsis => "ellipsis", + } +} diff --git a/crates/ruff/src/rules/ruff/rules/mod.rs b/crates/ruff/src/rules/ruff/rules/mod.rs index 4409d034d2..e4d39feefd 100644 --- a/crates/ruff/src/rules/ruff/rules/mod.rs +++ b/crates/ruff/src/rules/ruff/rules/mod.rs @@ -1,35 +1,37 @@ -pub(crate) use ambiguous_unicode_character::{ - ambiguous_unicode_character, AmbiguousUnicodeCharacterComment, - AmbiguousUnicodeCharacterDocstring, AmbiguousUnicodeCharacterString, -}; -pub(crate) use asyncio_dangling_task::{asyncio_dangling_task, AsyncioDanglingTask}; -pub(crate) use collection_literal_concatenation::{ - collection_literal_concatenation, CollectionLiteralConcatenation, -}; -pub(crate) use explicit_f_string_type_conversion::{ - explicit_f_string_type_conversion, ExplicitFStringTypeConversion, -}; -pub(crate) use invalid_pyproject_toml::InvalidPyprojectToml; -pub(crate) use mutable_defaults_in_dataclass_fields::{ - function_call_in_dataclass_defaults, is_dataclass, mutable_dataclass_default, - FunctionCallInDataclassDefaultArgument, MutableDataclassDefault, -}; -pub(crate) use pairwise_over_zipped::{pairwise_over_zipped, PairwiseOverZipped}; -pub(crate) use unused_noqa::{UnusedCodes, UnusedNOQA}; - -pub(crate) use static_key_dict_comprehension::{ - static_key_dict_comprehension, StaticKeyDictComprehension, -}; +pub(crate) use ambiguous_unicode_character::*; +pub(crate) use asyncio_dangling_task::*; +pub(crate) use collection_literal_concatenation::*; +pub(crate) use explicit_f_string_type_conversion::*; +pub(crate) use function_call_in_dataclass_default::*; +pub(crate) use implicit_optional::*; +pub(crate) use invalid_index_type::*; +pub(crate) use invalid_pyproject_toml::*; +pub(crate) use mutable_class_default::*; +pub(crate) use mutable_dataclass_default::*; +pub(crate) use pairwise_over_zipped::*; +pub(crate) use static_key_dict_comprehension::*; +pub(crate) use unnecessary_iterable_allocation_for_first_element::*; +#[cfg(feature = "unreachable-code")] +pub(crate) use unreachable::*; +pub(crate) use unused_noqa::*; mod ambiguous_unicode_character; mod asyncio_dangling_task; mod collection_literal_concatenation; mod confusables; mod explicit_f_string_type_conversion; +mod function_call_in_dataclass_default; +mod helpers; +mod implicit_optional; +mod invalid_index_type; mod invalid_pyproject_toml; -mod mutable_defaults_in_dataclass_fields; +mod mutable_class_default; +mod mutable_dataclass_default; mod pairwise_over_zipped; mod static_key_dict_comprehension; +mod unnecessary_iterable_allocation_for_first_element; +#[cfg(feature = "unreachable-code")] +pub(crate) mod unreachable; mod unused_noqa; #[derive(Clone, Copy)] diff --git a/crates/ruff/src/rules/ruff/rules/mutable_class_default.rs b/crates/ruff/src/rules/ruff/rules/mutable_class_default.rs new file mode 100644 index 0000000000..c1d17c30d5 --- /dev/null +++ b/crates/ruff/src/rules/ruff/rules/mutable_class_default.rs @@ -0,0 +1,93 @@ +use rustpython_parser::ast::{self, Ranged, Stmt}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_semantic::analyze::typing::{is_immutable_annotation, is_mutable_expr}; + +use crate::checkers::ast::Checker; +use crate::rules::ruff::rules::helpers::{ + is_class_var_annotation, is_dataclass, is_final_annotation, is_pydantic_model, + is_special_attribute, +}; + +/// ## What it does +/// Checks for mutable default values in class attributes. +/// +/// ## Why is this bad? +/// Mutable default values share state across all instances of the class, +/// while not being obvious. This can lead to bugs when the attributes are +/// changed in one instance, as those changes will unexpectedly affect all +/// other instances. +/// +/// When mutable value are intended, they should be annotated with +/// `typing.ClassVar`. +/// +/// ## Examples +/// ```python +/// class A: +/// mutable_default: list[int] = [] +/// ``` +/// +/// Use instead: +/// ```python +/// from typing import ClassVar +/// +/// +/// class A: +/// mutable_default: ClassVar[list[int]] = [] +/// ``` +#[violation] +pub struct MutableClassDefault; + +impl Violation for MutableClassDefault { + #[derive_message_formats] + fn message(&self) -> String { + format!("Mutable class attributes should be annotated with `typing.ClassVar`") + } +} + +/// RUF012 +pub(crate) fn mutable_class_default(checker: &mut Checker, class_def: &ast::StmtClassDef) { + for statement in &class_def.body { + match statement { + Stmt::AnnAssign(ast::StmtAnnAssign { + annotation, + target, + value: Some(value), + .. + }) => { + if !is_special_attribute(target) + && is_mutable_expr(value, checker.semantic()) + && !is_class_var_annotation(annotation, checker.semantic()) + && !is_final_annotation(annotation, checker.semantic()) + && !is_immutable_annotation(annotation, checker.semantic()) + && !is_dataclass(class_def, checker.semantic()) + { + // Avoid Pydantic models, which end up copying defaults on instance creation. + if is_pydantic_model(class_def, checker.semantic()) { + return; + } + + checker + .diagnostics + .push(Diagnostic::new(MutableClassDefault, value.range())); + } + } + Stmt::Assign(ast::StmtAssign { value, targets, .. }) => { + if !targets.iter().all(is_special_attribute) + && is_mutable_expr(value, checker.semantic()) + { + // Avoid Pydantic models, which end up copying defaults on instance creation. + if is_pydantic_model(class_def, checker.semantic()) { + return; + } + + checker + .diagnostics + .push(Diagnostic::new(MutableClassDefault, value.range())); + } + } + _ => (), + } + } +} diff --git a/crates/ruff/src/rules/ruff/rules/mutable_dataclass_default.rs b/crates/ruff/src/rules/ruff/rules/mutable_dataclass_default.rs new file mode 100644 index 0000000000..2b47c32a46 --- /dev/null +++ b/crates/ruff/src/rules/ruff/rules/mutable_dataclass_default.rs @@ -0,0 +1,87 @@ +use rustpython_parser::ast::{self, Ranged, Stmt}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_semantic::analyze::typing::{is_immutable_annotation, is_mutable_expr}; + +use crate::checkers::ast::Checker; +use crate::rules::ruff::rules::helpers::{is_class_var_annotation, is_dataclass}; + +/// ## What it does +/// Checks for mutable default values in dataclass attributes. +/// +/// ## Why is this bad? +/// Mutable default values share state across all instances of the dataclass. +/// This can lead to bugs when the attributes are changed in one instance, as +/// those changes will unexpectedly affect all other instances. +/// +/// Instead of sharing mutable defaults, use the `field(default_factory=...)` +/// pattern. +/// +/// If the default value is intended to be mutable, it should be annotated with +/// `typing.ClassVar`. +/// +/// ## Examples +/// ```python +/// from dataclasses import dataclass +/// +/// +/// @dataclass +/// class A: +/// mutable_default: list[int] = [] +/// ``` +/// +/// Use instead: +/// ```python +/// from dataclasses import dataclass, field +/// +/// +/// @dataclass +/// class A: +/// mutable_default: list[int] = field(default_factory=list) +/// ``` +/// +/// Or: +/// ```python +/// from dataclasses import dataclass, field +/// from typing import ClassVar +/// +/// +/// @dataclass +/// class A: +/// mutable_default: ClassVar[list[int]] = [] +/// ``` +#[violation] +pub struct MutableDataclassDefault; + +impl Violation for MutableDataclassDefault { + #[derive_message_formats] + fn message(&self) -> String { + format!("Do not use mutable default values for dataclass attributes") + } +} + +/// RUF008 +pub(crate) fn mutable_dataclass_default(checker: &mut Checker, class_def: &ast::StmtClassDef) { + if !is_dataclass(class_def, checker.semantic()) { + return; + } + + for statement in &class_def.body { + if let Stmt::AnnAssign(ast::StmtAnnAssign { + annotation, + value: Some(value), + .. + }) = statement + { + if is_mutable_expr(value, checker.semantic()) + && !is_class_var_annotation(annotation, checker.semantic()) + && !is_immutable_annotation(annotation, checker.semantic()) + { + checker + .diagnostics + .push(Diagnostic::new(MutableDataclassDefault, value.range())); + } + } + } +} diff --git a/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_dataclass_fields.rs b/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_dataclass_fields.rs deleted file mode 100644 index 1f300b303b..0000000000 --- a/crates/ruff/src/rules/ruff/rules/mutable_defaults_in_dataclass_fields.rs +++ /dev/null @@ -1,255 +0,0 @@ -use rustpython_parser::ast::{self, Decorator, Expr, Ranged, Stmt}; - -use ruff_diagnostics::{Diagnostic, Violation}; -use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::call_path::{from_qualified_name, CallPath}; -use ruff_python_ast::{call_path::compose_call_path, helpers::map_callable}; -use ruff_python_semantic::{ - analyze::typing::{is_immutable_annotation, is_immutable_func}, - model::SemanticModel, -}; - -use crate::checkers::ast::Checker; - -/// ## What it does -/// Checks for mutable default values in dataclasses without the use of -/// `dataclasses.field`. -/// -/// ## Why is this bad? -/// Mutable default values share state across all instances of the dataclass, -/// while not being obvious. This can lead to bugs when the attributes are -/// changed in one instance, as those changes will unexpectedly affect all -/// other instances. -/// -/// ## Examples: -/// ```python -/// from dataclasses import dataclass -/// -/// -/// @dataclass -/// class A: -/// mutable_default: list[int] = [] -/// ``` -/// -/// Use instead: -/// ```python -/// from dataclasses import dataclass, field -/// -/// -/// @dataclass -/// class A: -/// mutable_default: list[int] = field(default_factory=list) -/// ``` -/// -/// Alternatively, if you _want_ shared behaviour, make it more obvious -/// by assigning to a module-level variable: -/// ```python -/// from dataclasses import dataclass -/// -/// I_KNOW_THIS_IS_SHARED_STATE = [1, 2, 3, 4] -/// -/// -/// @dataclass -/// class A: -/// mutable_default: list[int] = I_KNOW_THIS_IS_SHARED_STATE -/// ``` -#[violation] -pub struct MutableDataclassDefault; - -impl Violation for MutableDataclassDefault { - #[derive_message_formats] - fn message(&self) -> String { - format!("Do not use mutable default values for dataclass attributes") - } -} - -/// ## What it does -/// Checks for function calls in dataclass defaults. -/// -/// ## Why is this bad? -/// Function calls are only performed once, at definition time. The returned -/// value is then reused by all instances of the dataclass. -/// -/// ## Options -/// - `flake8-bugbear.extend-immutable-calls` -/// -/// ## Examples: -/// ```python -/// from dataclasses import dataclass -/// -/// -/// def creating_list() -> list[int]: -/// return [1, 2, 3, 4] -/// -/// -/// @dataclass -/// class A: -/// mutable_default: list[int] = creating_list() -/// -/// -/// # also: -/// -/// -/// @dataclass -/// class B: -/// also_mutable_default_but_sneakier: A = A() -/// ``` -/// -/// Use instead: -/// ```python -/// from dataclasses import dataclass, field -/// -/// -/// def creating_list() -> list[int]: -/// return [1, 2, 3, 4] -/// -/// -/// @dataclass -/// class A: -/// mutable_default: list[int] = field(default_factory=creating_list) -/// -/// -/// @dataclass -/// class B: -/// also_mutable_default_but_sneakier: A = field(default_factory=A) -/// ``` -/// -/// Alternatively, if you _want_ the shared behaviour, make it more obvious -/// by assigning it to a module-level variable: -/// ```python -/// from dataclasses import dataclass -/// -/// -/// def creating_list() -> list[int]: -/// return [1, 2, 3, 4] -/// -/// -/// I_KNOW_THIS_IS_SHARED_STATE = creating_list() -/// -/// -/// @dataclass -/// class A: -/// mutable_default: list[int] = I_KNOW_THIS_IS_SHARED_STATE -/// ``` -#[violation] -pub struct FunctionCallInDataclassDefaultArgument { - pub name: Option, -} - -impl Violation for FunctionCallInDataclassDefaultArgument { - #[derive_message_formats] - fn message(&self) -> String { - let FunctionCallInDataclassDefaultArgument { name } = self; - if let Some(name) = name { - format!("Do not perform function call `{name}` in dataclass defaults") - } else { - format!("Do not perform function call in dataclass defaults") - } - } -} - -fn is_mutable_expr(expr: &Expr) -> bool { - matches!( - expr, - Expr::List(_) - | Expr::Dict(_) - | Expr::Set(_) - | Expr::ListComp(_) - | Expr::DictComp(_) - | Expr::SetComp(_) - ) -} - -const ALLOWED_DATACLASS_SPECIFIC_FUNCTIONS: &[&[&str]] = &[&["dataclasses", "field"]]; - -fn is_allowed_dataclass_function(model: &SemanticModel, func: &Expr) -> bool { - model.resolve_call_path(func).map_or(false, |call_path| { - ALLOWED_DATACLASS_SPECIFIC_FUNCTIONS - .iter() - .any(|target| call_path.as_slice() == *target) - }) -} - -/// Returns `true` if the given [`Expr`] is a `typing.ClassVar` annotation. -fn is_class_var_annotation(model: &SemanticModel, annotation: &Expr) -> bool { - let Expr::Subscript(ast::ExprSubscript { value, .. }) = &annotation else { - return false; - }; - model.match_typing_expr(value, "ClassVar") -} - -/// RUF009 -pub(crate) fn function_call_in_dataclass_defaults(checker: &mut Checker, body: &[Stmt]) { - let extend_immutable_calls: Vec = checker - .settings - .flake8_bugbear - .extend_immutable_calls - .iter() - .map(|target| from_qualified_name(target)) - .collect(); - - for statement in body { - if let Stmt::AnnAssign(ast::StmtAnnAssign { - annotation, - value: Some(expr), - .. - }) = statement - { - if is_class_var_annotation(checker.semantic_model(), annotation) { - continue; - } - if let Expr::Call(ast::ExprCall { func, .. }) = expr.as_ref() { - if !is_immutable_func(checker.semantic_model(), func, &extend_immutable_calls) - && !is_allowed_dataclass_function(checker.semantic_model(), func) - { - checker.diagnostics.push(Diagnostic::new( - FunctionCallInDataclassDefaultArgument { - name: compose_call_path(func), - }, - expr.range(), - )); - } - } - } - } -} - -/// RUF008 -pub(crate) fn mutable_dataclass_default(checker: &mut Checker, body: &[Stmt]) { - for statement in body { - match statement { - Stmt::AnnAssign(ast::StmtAnnAssign { - annotation, - value: Some(value), - .. - }) => { - if !is_class_var_annotation(checker.semantic_model(), annotation) - && !is_immutable_annotation(checker.semantic_model(), annotation) - && is_mutable_expr(value) - { - checker - .diagnostics - .push(Diagnostic::new(MutableDataclassDefault, value.range())); - } - } - Stmt::Assign(ast::StmtAssign { value, .. }) => { - if is_mutable_expr(value) { - checker - .diagnostics - .push(Diagnostic::new(MutableDataclassDefault, value.range())); - } - } - _ => (), - } - } -} - -pub(crate) fn is_dataclass(model: &SemanticModel, decorator_list: &[Decorator]) -> bool { - decorator_list.iter().any(|decorator| { - model - .resolve_call_path(map_callable(&decorator.expression)) - .map_or(false, |call_path| { - call_path.as_slice() == ["dataclasses", "dataclass"] - }) - }) -} diff --git a/crates/ruff/src/rules/ruff/rules/pairwise_over_zipped.rs b/crates/ruff/src/rules/ruff/rules/pairwise_over_zipped.rs index 99c2286a75..1b3f234a78 100644 --- a/crates/ruff/src/rules/ruff/rules/pairwise_over_zipped.rs +++ b/crates/ruff/src/rules/ruff/rules/pairwise_over_zipped.rs @@ -1,11 +1,37 @@ use num_traits::ToPrimitive; -use rustpython_parser::ast::{self, Constant, Expr, Ranged, Unaryop}; +use rustpython_parser::ast::{self, Constant, Expr, Ranged, UnaryOp}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for use of `zip()` to iterate over successive pairs of elements. +/// +/// ## Why is this bad? +/// When iterating over successive pairs of elements, prefer +/// `itertools.pairwise()` over `zip()`. +/// +/// `itertools.pairwise()` is more readable and conveys the intent of the code +/// more clearly. +/// +/// ## Example +/// ```python +/// letters = "ABCD" +/// zip(letters, letters[1:]) # ("A", "B"), ("B", "C"), ("C", "D") +/// ``` +/// +/// Use instead: +/// ```python +/// from itertools import pairwise +/// +/// letters = "ABCD" +/// pairwise(letters) # ("A", "B"), ("B", "C"), ("C", "D") +/// ``` +/// +/// ## References +/// - [Python documentation: `itertools.pairwise`](https://docs.python.org/3/library/itertools.html#itertools.pairwise) #[violation] pub struct PairwiseOverZipped; @@ -41,7 +67,7 @@ fn match_slice_info(expr: &Expr) -> Option { return None; }; - let Expr::Slice(ast::ExprSlice { lower, step, .. }) = slice.as_ref() else { + let Expr::Slice(ast::ExprSlice { lower, step, .. }) = slice.as_ref() else { return None; }; @@ -69,7 +95,7 @@ fn to_bound(expr: &Expr) -> Option { .. }) => value.to_i64(), Expr::UnaryOp(ast::ExprUnaryOp { - op: Unaryop::USub | Unaryop::Invert, + op: UnaryOp::USub | UnaryOp::Invert, operand, range: _, }) => { @@ -99,7 +125,7 @@ pub(crate) fn pairwise_over_zipped(checker: &mut Checker, func: &Expr, args: &[E } // Require the function to be the builtin `zip`. - if !(id == "zip" && checker.semantic_model().is_builtin(id)) { + if !(id == "zip" && checker.semantic().is_builtin(id)) { return; } @@ -115,7 +141,7 @@ pub(crate) fn pairwise_over_zipped(checker: &mut Checker, func: &Expr, args: &[E }; // Require second argument to be a `Subscript`. - if !matches!(&args[1], Expr::Subscript(_)) { + if !args[1].is_subscript_expr() { return; } let Some(second_arg_info) = match_slice_info(&args[1]) else { diff --git a/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__assert.py.md.snap b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__assert.py.md.snap new file mode 100644 index 0000000000..17ca4671a0 --- /dev/null +++ b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__assert.py.md.snap @@ -0,0 +1,97 @@ +--- +source: crates/ruff/src/rules/ruff/rules/unreachable.rs +description: "This is a Mermaid graph. You can use https://mermaid.live to visualize it as a diagram." +--- +## Function 0 +### Source +```python +def func(): + assert True +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1[["Exception raised"]] + block2["assert True\n"] + + start --> block2 + block2 -- "True" --> block0 + block2 -- "else" --> block1 + block1 --> return + block0 --> return +``` + +## Function 1 +### Source +```python +def func(): + assert False +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1[["Exception raised"]] + block2["assert False\n"] + + start --> block2 + block2 -- "False" --> block0 + block2 -- "else" --> block1 + block1 --> return + block0 --> return +``` + +## Function 2 +### Source +```python +def func(): + assert True, "oops" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1[["Exception raised"]] + block2["assert True, #quot;oops#quot;\n"] + + start --> block2 + block2 -- "True" --> block0 + block2 -- "else" --> block1 + block1 --> return + block0 --> return +``` + +## Function 3 +### Source +```python +def func(): + assert False, "oops" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1[["Exception raised"]] + block2["assert False, #quot;oops#quot;\n"] + + start --> block2 + block2 -- "False" --> block0 + block2 -- "else" --> block1 + block1 --> return + block0 --> return +``` + + diff --git a/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__async-for.py.md.snap b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__async-for.py.md.snap new file mode 100644 index 0000000000..431c82d33c --- /dev/null +++ b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__async-for.py.md.snap @@ -0,0 +1,241 @@ +--- +source: crates/ruff/src/rules/ruff/rules/unreachable.rs +description: "This is a Mermaid graph. You can use https://mermaid.live to visualize it as a diagram." +--- +## Function 0 +### Source +```python +def func(): + async for i in range(5): + print(i) +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["print(i)\n"] + block2["async for i in range(5): + print(i)\n"] + + start --> block2 + block2 -- "range(5)" --> block1 + block2 -- "else" --> block0 + block1 --> block2 + block0 --> return +``` + +## Function 1 +### Source +```python +def func(): + async for i in range(20): + print(i) + else: + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["print(i)\n"] + block1["return 0\n"] + block2["async for i in range(20): + print(i) + else: + return 0\n"] + + start --> block2 + block2 -- "range(20)" --> block0 + block2 -- "else" --> block1 + block1 --> return + block0 --> return +``` + +## Function 2 +### Source +```python +def func(): + async for i in range(10): + if i == 5: + return 1 + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 0\n"] + block1["return 1\n"] + block2["if i == 5: + return 1\n"] + block3["async for i in range(10): + if i == 5: + return 1\n"] + + start --> block3 + block3 -- "range(10)" --> block2 + block3 -- "else" --> block0 + block2 -- "i == 5" --> block1 + block2 -- "else" --> block3 + block1 --> return + block0 --> return +``` + +## Function 3 +### Source +```python +def func(): + async for i in range(111): + if i == 5: + return 1 + else: + return 0 + return 2 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 2\n"] + block1["return 1\n"] + block2["if i == 5: + return 1\n"] + block3["return 0\n"] + block4["async for i in range(111): + if i == 5: + return 1 + else: + return 0\n"] + + start --> block4 + block4 -- "range(111)" --> block2 + block4 -- "else" --> block3 + block3 --> return + block2 -- "i == 5" --> block1 + block2 -- "else" --> block4 + block1 --> return + block0 --> return +``` + +## Function 4 +### Source +```python +def func(): + async for i in range(12): + continue +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["continue\n"] + block2["async for i in range(12): + continue\n"] + + start --> block2 + block2 -- "range(12)" --> block1 + block2 -- "else" --> block0 + block1 --> block2 + block0 --> return +``` + +## Function 5 +### Source +```python +def func(): + async for i in range(1110): + if True: + continue +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["continue\n"] + block2["if True: + continue\n"] + block3["async for i in range(1110): + if True: + continue\n"] + + start --> block3 + block3 -- "range(1110)" --> block2 + block3 -- "else" --> block0 + block2 -- "True" --> block1 + block2 -- "else" --> block3 + block1 --> block3 + block0 --> return +``` + +## Function 6 +### Source +```python +def func(): + async for i in range(13): + break +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["break\n"] + block2["async for i in range(13): + break\n"] + + start --> block2 + block2 -- "range(13)" --> block1 + block2 -- "else" --> block0 + block1 --> block0 + block0 --> return +``` + +## Function 7 +### Source +```python +def func(): + async for i in range(1110): + if True: + break +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["break\n"] + block2["if True: + break\n"] + block3["async for i in range(1110): + if True: + break\n"] + + start --> block3 + block3 -- "range(1110)" --> block2 + block3 -- "else" --> block0 + block2 -- "True" --> block1 + block2 -- "else" --> block3 + block1 --> block0 + block0 --> return +``` + + diff --git a/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__for.py.md.snap b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__for.py.md.snap new file mode 100644 index 0000000000..f3d3def743 --- /dev/null +++ b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__for.py.md.snap @@ -0,0 +1,241 @@ +--- +source: crates/ruff/src/rules/ruff/rules/unreachable.rs +description: "This is a Mermaid graph. You can use https://mermaid.live to visualize it as a diagram." +--- +## Function 0 +### Source +```python +def func(): + for i in range(5): + print(i) +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["print(i)\n"] + block2["for i in range(5): + print(i)\n"] + + start --> block2 + block2 -- "range(5)" --> block1 + block2 -- "else" --> block0 + block1 --> block2 + block0 --> return +``` + +## Function 1 +### Source +```python +def func(): + for i in range(20): + print(i) + else: + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["print(i)\n"] + block1["return 0\n"] + block2["for i in range(20): + print(i) + else: + return 0\n"] + + start --> block2 + block2 -- "range(20)" --> block0 + block2 -- "else" --> block1 + block1 --> return + block0 --> return +``` + +## Function 2 +### Source +```python +def func(): + for i in range(10): + if i == 5: + return 1 + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 0\n"] + block1["return 1\n"] + block2["if i == 5: + return 1\n"] + block3["for i in range(10): + if i == 5: + return 1\n"] + + start --> block3 + block3 -- "range(10)" --> block2 + block3 -- "else" --> block0 + block2 -- "i == 5" --> block1 + block2 -- "else" --> block3 + block1 --> return + block0 --> return +``` + +## Function 3 +### Source +```python +def func(): + for i in range(111): + if i == 5: + return 1 + else: + return 0 + return 2 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 2\n"] + block1["return 1\n"] + block2["if i == 5: + return 1\n"] + block3["return 0\n"] + block4["for i in range(111): + if i == 5: + return 1 + else: + return 0\n"] + + start --> block4 + block4 -- "range(111)" --> block2 + block4 -- "else" --> block3 + block3 --> return + block2 -- "i == 5" --> block1 + block2 -- "else" --> block4 + block1 --> return + block0 --> return +``` + +## Function 4 +### Source +```python +def func(): + for i in range(12): + continue +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["continue\n"] + block2["for i in range(12): + continue\n"] + + start --> block2 + block2 -- "range(12)" --> block1 + block2 -- "else" --> block0 + block1 --> block2 + block0 --> return +``` + +## Function 5 +### Source +```python +def func(): + for i in range(1110): + if True: + continue +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["continue\n"] + block2["if True: + continue\n"] + block3["for i in range(1110): + if True: + continue\n"] + + start --> block3 + block3 -- "range(1110)" --> block2 + block3 -- "else" --> block0 + block2 -- "True" --> block1 + block2 -- "else" --> block3 + block1 --> block3 + block0 --> return +``` + +## Function 6 +### Source +```python +def func(): + for i in range(13): + break +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["break\n"] + block2["for i in range(13): + break\n"] + + start --> block2 + block2 -- "range(13)" --> block1 + block2 -- "else" --> block0 + block1 --> block0 + block0 --> return +``` + +## Function 7 +### Source +```python +def func(): + for i in range(1110): + if True: + break +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["break\n"] + block2["if True: + break\n"] + block3["for i in range(1110): + if True: + break\n"] + + start --> block3 + block3 -- "range(1110)" --> block2 + block3 -- "else" --> block0 + block2 -- "True" --> block1 + block2 -- "else" --> block3 + block1 --> block0 + block0 --> return +``` + + diff --git a/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__if.py.md.snap b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__if.py.md.snap new file mode 100644 index 0000000000..1ab3a11c7d --- /dev/null +++ b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__if.py.md.snap @@ -0,0 +1,535 @@ +--- +source: crates/ruff/src/rules/ruff/rules/unreachable.rs +description: "This is a Mermaid graph. You can use https://mermaid.live to visualize it as a diagram." +--- +## Function 0 +### Source +```python +def func(): + if False: + return 0 + return 1 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 1\n"] + block1["return 0\n"] + block2["if False: + return 0\n"] + + start --> block2 + block2 -- "False" --> block1 + block2 -- "else" --> block0 + block1 --> return + block0 --> return +``` + +## Function 1 +### Source +```python +def func(): + if True: + return 1 + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 0\n"] + block1["return 1\n"] + block2["if True: + return 1\n"] + + start --> block2 + block2 -- "True" --> block1 + block2 -- "else" --> block0 + block1 --> return + block0 --> return +``` + +## Function 2 +### Source +```python +def func(): + if False: + return 0 + else: + return 1 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 0\n"] + block1["return 1\n"] + block2["if False: + return 0 + else: + return 1\n"] + + start --> block2 + block2 -- "False" --> block0 + block2 -- "else" --> block1 + block1 --> return + block0 --> return +``` + +## Function 3 +### Source +```python +def func(): + if True: + return 1 + else: + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 1\n"] + block1["return 0\n"] + block2["if True: + return 1 + else: + return 0\n"] + + start --> block2 + block2 -- "True" --> block0 + block2 -- "else" --> block1 + block1 --> return + block0 --> return +``` + +## Function 4 +### Source +```python +def func(): + if False: + return 0 + else: + return 1 + return "unreachable" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return #quot;unreachable#quot;\n"] + block1["return 0\n"] + block2["return 1\n"] + block3["if False: + return 0 + else: + return 1\n"] + + start --> block3 + block3 -- "False" --> block1 + block3 -- "else" --> block2 + block2 --> return + block1 --> return + block0 --> return +``` + +## Function 5 +### Source +```python +def func(): + if True: + return 1 + else: + return 0 + return "unreachable" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return #quot;unreachable#quot;\n"] + block1["return 1\n"] + block2["return 0\n"] + block3["if True: + return 1 + else: + return 0\n"] + + start --> block3 + block3 -- "True" --> block1 + block3 -- "else" --> block2 + block2 --> return + block1 --> return + block0 --> return +``` + +## Function 6 +### Source +```python +def func(): + if True: + if True: + return 1 + return 2 + else: + return 3 + return "unreachable2" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return #quot;unreachable2#quot;\n"] + block1["return 2\n"] + block2["return 1\n"] + block3["if True: + return 1\n"] + block4["return 3\n"] + block5["if True: + if True: + return 1 + return 2 + else: + return 3\n"] + + start --> block5 + block5 -- "True" --> block3 + block5 -- "else" --> block4 + block4 --> return + block3 -- "True" --> block2 + block3 -- "else" --> block1 + block2 --> return + block1 --> return + block0 --> return +``` + +## Function 7 +### Source +```python +def func(): + if False: + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["return 0\n"] + block2["if False: + return 0\n"] + + start --> block2 + block2 -- "False" --> block1 + block2 -- "else" --> block0 + block1 --> return + block0 --> return +``` + +## Function 8 +### Source +```python +def func(): + if True: + return 1 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["return 1\n"] + block2["if True: + return 1\n"] + + start --> block2 + block2 -- "True" --> block1 + block2 -- "else" --> block0 + block1 --> return + block0 --> return +``` + +## Function 9 +### Source +```python +def func(): + if True: + return 1 + elif False: + return 2 + else: + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 1\n"] + block1["return 2\n"] + block2["return 0\n"] + block3["elif False: + return 2 + else: + return 0\n"] + block4["if True: + return 1 + elif False: + return 2 + else: + return 0\n"] + + start --> block4 + block4 -- "True" --> block0 + block4 -- "else" --> block3 + block3 -- "False" --> block1 + block3 -- "else" --> block2 + block2 --> return + block1 --> return + block0 --> return +``` + +## Function 10 +### Source +```python +def func(): + if False: + return 1 + elif True: + return 2 + else: + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 1\n"] + block1["return 2\n"] + block2["return 0\n"] + block3["elif True: + return 2 + else: + return 0\n"] + block4["if False: + return 1 + elif True: + return 2 + else: + return 0\n"] + + start --> block4 + block4 -- "False" --> block0 + block4 -- "else" --> block3 + block3 -- "True" --> block1 + block3 -- "else" --> block2 + block2 --> return + block1 --> return + block0 --> return +``` + +## Function 11 +### Source +```python +def func(): + if True: + if False: + return 0 + elif True: + return 1 + else: + return 2 + return 3 + elif True: + return 4 + else: + return 5 + return 6 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 6\n"] + block1["return 3\n"] + block2["return 0\n"] + block3["return 1\n"] + block4["return 2\n"] + block5["elif True: + return 1 + else: + return 2\n"] + block6["if False: + return 0 + elif True: + return 1 + else: + return 2\n"] + block7["return 4\n"] + block8["return 5\n"] + block9["elif True: + return 4 + else: + return 5\n"] + block10["if True: + if False: + return 0 + elif True: + return 1 + else: + return 2 + return 3 + elif True: + return 4 + else: + return 5\n"] + + start --> block10 + block10 -- "True" --> block6 + block10 -- "else" --> block9 + block9 -- "True" --> block7 + block9 -- "else" --> block8 + block8 --> return + block7 --> return + block6 -- "False" --> block2 + block6 -- "else" --> block5 + block5 -- "True" --> block3 + block5 -- "else" --> block4 + block4 --> return + block3 --> return + block2 --> return + block1 --> return + block0 --> return +``` + +## Function 12 +### Source +```python +def func(): + if False: + return "unreached" + elif False: + return "also unreached" + return "reached" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return #quot;reached#quot;\n"] + block1["return #quot;unreached#quot;\n"] + block2["return #quot;also unreached#quot;\n"] + block3["elif False: + return #quot;also unreached#quot;\n"] + block4["if False: + return #quot;unreached#quot; + elif False: + return #quot;also unreached#quot;\n"] + + start --> block4 + block4 -- "False" --> block1 + block4 -- "else" --> block3 + block3 -- "False" --> block2 + block3 -- "else" --> block0 + block2 --> return + block1 --> return + block0 --> return +``` + +## Function 13 +### Source +```python +def func(self, obj: BytesRep) -> bytes: + data = obj["data"] + + if isinstance(data, str): + return base64.b64decode(data) + elif isinstance(data, Buffer): + buffer = data + else: + id = data["id"] + + if id in self._buffers: + buffer = self._buffers[id] + else: + self.error(f"can't resolve buffer '{id}'") + + return buffer.data +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return buffer.data\n"] + block1["return base64.b64decode(data)\n"] + block2["buffer = data\n"] + block3["buffer = self._buffers[id]\n"] + block4["self.error(f#quot;can't resolve buffer '{id}'#quot;)\n"] + block5["id = data[#quot;id#quot;]\nif id in self._buffers: + buffer = self._buffers[id] + else: + self.error(f#quot;can't resolve buffer '{id}'#quot;)\n"] + block6["elif isinstance(data, Buffer): + buffer = data + else: + id = data[#quot;id#quot;] + + if id in self._buffers: + buffer = self._buffers[id] + else: + self.error(f#quot;can't resolve buffer '{id}'#quot;)\n"] + block7["data = obj[#quot;data#quot;]\nif isinstance(data, str): + return base64.b64decode(data) + elif isinstance(data, Buffer): + buffer = data + else: + id = data[#quot;id#quot;] + + if id in self._buffers: + buffer = self._buffers[id] + else: + self.error(f#quot;can't resolve buffer '{id}'#quot;)\n"] + + start --> block7 + block7 -- "isinstance(data, str)" --> block1 + block7 -- "else" --> block6 + block6 -- "isinstance(data, Buffer)" --> block2 + block6 -- "else" --> block5 + block5 -- "id in self._buffers" --> block3 + block5 -- "else" --> block4 + block4 --> block0 + block3 --> block0 + block2 --> block0 + block1 --> return + block0 --> return +``` + + diff --git a/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__match.py.md.snap b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__match.py.md.snap new file mode 100644 index 0000000000..d8a6ddb59b --- /dev/null +++ b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__match.py.md.snap @@ -0,0 +1,776 @@ +--- +source: crates/ruff/src/rules/ruff/rules/unreachable.rs +description: "This is a Mermaid graph. You can use https://mermaid.live to visualize it as a diagram." +--- +## Function 0 +### Source +```python +def func(status): + match status: + case _: + return 0 + return "unreachable" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return #quot;unreachable#quot;\n"] + block1["return 0\n"] + block2["match status: + case _: + return 0\n"] + + start --> block2 + block2 --> block1 + block1 --> return + block0 --> return +``` + +## Function 1 +### Source +```python +def func(status): + match status: + case 1: + return 1 + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 0\n"] + block1["return 1\n"] + block2["match status: + case 1: + return 1\n"] + + start --> block2 + block2 -- "case 1" --> block1 + block2 -- "else" --> block0 + block1 --> return + block0 --> return +``` + +## Function 2 +### Source +```python +def func(status): + match status: + case 1: + return 1 + case _: + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 0\n"] + block1["match status: + case 1: + return 1 + case _: + return 0\n"] + block2["return 1\n"] + block3["match status: + case 1: + return 1 + case _: + return 0\n"] + + start --> block3 + block3 -- "case 1" --> block2 + block3 -- "else" --> block1 + block2 --> return + block1 --> block0 + block0 --> return +``` + +## Function 3 +### Source +```python +def func(status): + match status: + case 1 | 2 | 3: + return 5 + return 6 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 6\n"] + block1["return 5\n"] + block2["match status: + case 1 | 2 | 3: + return 5\n"] + + start --> block2 + block2 -- "case 1 | 2 | 3" --> block1 + block2 -- "else" --> block0 + block1 --> return + block0 --> return +``` + +## Function 4 +### Source +```python +def func(status): + match status: + case 1 | 2 | 3: + return 5 + case _: + return 10 + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 0\n"] + block1["return 10\n"] + block2["match status: + case 1 | 2 | 3: + return 5 + case _: + return 10\n"] + block3["return 5\n"] + block4["match status: + case 1 | 2 | 3: + return 5 + case _: + return 10\n"] + + start --> block4 + block4 -- "case 1 | 2 | 3" --> block3 + block4 -- "else" --> block2 + block3 --> return + block2 --> block1 + block1 --> return + block0 --> return +``` + +## Function 5 +### Source +```python +def func(status): + match status: + case 0: + return 0 + case 1: + return 1 + case 1: + return "1 again" + case _: + return 3 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 3\n"] + block1["match status: + case 0: + return 0 + case 1: + return 1 + case 1: + return #quot;1 again#quot; + case _: + return 3\n"] + block2["return #quot;1 again#quot;\n"] + block3["match status: + case 0: + return 0 + case 1: + return 1 + case 1: + return #quot;1 again#quot; + case _: + return 3\n"] + block4["return 1\n"] + block5["match status: + case 0: + return 0 + case 1: + return 1 + case 1: + return #quot;1 again#quot; + case _: + return 3\n"] + block6["return 0\n"] + block7["match status: + case 0: + return 0 + case 1: + return 1 + case 1: + return #quot;1 again#quot; + case _: + return 3\n"] + + start --> block7 + block7 -- "case 0" --> block6 + block7 -- "else" --> block5 + block6 --> return + block5 -- "case 1" --> block4 + block5 -- "else" --> block3 + block4 --> return + block3 -- "case 1" --> block2 + block3 -- "else" --> block1 + block2 --> return + block1 --> block0 + block0 --> return +``` + +## Function 6 +### Source +```python +def func(status): + i = 0 + match status, i: + case _, _: + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["return 0\n"] + block2["match status, i: + case _, _: + return 0\n"] + block3["i = 0\n"] + + start --> block3 + block3 --> block2 + block2 -- "case _, _" --> block1 + block2 -- "else" --> block0 + block1 --> return + block0 --> return +``` + +## Function 7 +### Source +```python +def func(status): + i = 0 + match status, i: + case _, 0: + return 0 + case _, 2: + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["return 0\n"] + block2["match status, i: + case _, 0: + return 0 + case _, 2: + return 0\n"] + block3["return 0\n"] + block4["match status, i: + case _, 0: + return 0 + case _, 2: + return 0\n"] + block5["i = 0\n"] + + start --> block5 + block5 --> block4 + block4 -- "case _, 0" --> block3 + block4 -- "else" --> block2 + block3 --> return + block2 -- "case _, 2" --> block1 + block2 -- "else" --> block0 + block1 --> return + block0 --> return +``` + +## Function 8 +### Source +```python +def func(point): + match point: + case (0, 0): + print("Origin") + case _: + raise ValueError("oops") +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["raise ValueError(#quot;oops#quot;)\n"] + block2["match point: + case (0, 0): + print(#quot;Origin#quot;) + case _: + raise ValueError(#quot;oops#quot;)\n"] + block3["print(#quot;Origin#quot;)\n"] + block4["match point: + case (0, 0): + print(#quot;Origin#quot;) + case _: + raise ValueError(#quot;oops#quot;)\n"] + + start --> block4 + block4 -- "case (0, 0)" --> block3 + block4 -- "else" --> block2 + block3 --> block0 + block2 --> block1 + block1 --> return + block0 --> return +``` + +## Function 9 +### Source +```python +def func(point): + match point: + case (0, 0): + print("Origin") + case (0, y): + print(f"Y={y}") + case (x, 0): + print(f"X={x}") + case (x, y): + print(f"X={x}, Y={y}") + case _: + raise ValueError("Not a point") +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["raise ValueError(#quot;Not a point#quot;)\n"] + block2["match point: + case (0, 0): + print(#quot;Origin#quot;) + case (0, y): + print(f#quot;Y={y}#quot;) + case (x, 0): + print(f#quot;X={x}#quot;) + case (x, y): + print(f#quot;X={x}, Y={y}#quot;) + case _: + raise ValueError(#quot;Not a point#quot;)\n"] + block3["print(f#quot;X={x}, Y={y}#quot;)\n"] + block4["match point: + case (0, 0): + print(#quot;Origin#quot;) + case (0, y): + print(f#quot;Y={y}#quot;) + case (x, 0): + print(f#quot;X={x}#quot;) + case (x, y): + print(f#quot;X={x}, Y={y}#quot;) + case _: + raise ValueError(#quot;Not a point#quot;)\n"] + block5["print(f#quot;X={x}#quot;)\n"] + block6["match point: + case (0, 0): + print(#quot;Origin#quot;) + case (0, y): + print(f#quot;Y={y}#quot;) + case (x, 0): + print(f#quot;X={x}#quot;) + case (x, y): + print(f#quot;X={x}, Y={y}#quot;) + case _: + raise ValueError(#quot;Not a point#quot;)\n"] + block7["print(f#quot;Y={y}#quot;)\n"] + block8["match point: + case (0, 0): + print(#quot;Origin#quot;) + case (0, y): + print(f#quot;Y={y}#quot;) + case (x, 0): + print(f#quot;X={x}#quot;) + case (x, y): + print(f#quot;X={x}, Y={y}#quot;) + case _: + raise ValueError(#quot;Not a point#quot;)\n"] + block9["print(#quot;Origin#quot;)\n"] + block10["match point: + case (0, 0): + print(#quot;Origin#quot;) + case (0, y): + print(f#quot;Y={y}#quot;) + case (x, 0): + print(f#quot;X={x}#quot;) + case (x, y): + print(f#quot;X={x}, Y={y}#quot;) + case _: + raise ValueError(#quot;Not a point#quot;)\n"] + + start --> block10 + block10 -- "case (0, 0)" --> block9 + block10 -- "else" --> block8 + block9 --> block0 + block8 -- "case (0, y)" --> block7 + block8 -- "else" --> block6 + block7 --> block0 + block6 -- "case (x, 0)" --> block5 + block6 -- "else" --> block4 + block5 --> block0 + block4 -- "case (x, y)" --> block3 + block4 -- "else" --> block2 + block3 --> block0 + block2 --> block1 + block1 --> return + block0 --> return +``` + +## Function 10 +### Source +```python +def where_is(point): + class Point: + x: int + y: int + + match point: + case Point(x=0, y=0): + print("Origin") + case Point(x=0, y=y): + print(f"Y={y}") + case Point(x=x, y=0): + print(f"X={x}") + case Point(): + print("Somewhere else") + case _: + print("Not a point") +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["print(#quot;Not a point#quot;)\n"] + block2["match point: + case Point(x=0, y=0): + print(#quot;Origin#quot;) + case Point(x=0, y=y): + print(f#quot;Y={y}#quot;) + case Point(x=x, y=0): + print(f#quot;X={x}#quot;) + case Point(): + print(#quot;Somewhere else#quot;) + case _: + print(#quot;Not a point#quot;)\n"] + block3["print(#quot;Somewhere else#quot;)\n"] + block4["match point: + case Point(x=0, y=0): + print(#quot;Origin#quot;) + case Point(x=0, y=y): + print(f#quot;Y={y}#quot;) + case Point(x=x, y=0): + print(f#quot;X={x}#quot;) + case Point(): + print(#quot;Somewhere else#quot;) + case _: + print(#quot;Not a point#quot;)\n"] + block5["print(f#quot;X={x}#quot;)\n"] + block6["match point: + case Point(x=0, y=0): + print(#quot;Origin#quot;) + case Point(x=0, y=y): + print(f#quot;Y={y}#quot;) + case Point(x=x, y=0): + print(f#quot;X={x}#quot;) + case Point(): + print(#quot;Somewhere else#quot;) + case _: + print(#quot;Not a point#quot;)\n"] + block7["print(f#quot;Y={y}#quot;)\n"] + block8["match point: + case Point(x=0, y=0): + print(#quot;Origin#quot;) + case Point(x=0, y=y): + print(f#quot;Y={y}#quot;) + case Point(x=x, y=0): + print(f#quot;X={x}#quot;) + case Point(): + print(#quot;Somewhere else#quot;) + case _: + print(#quot;Not a point#quot;)\n"] + block9["print(#quot;Origin#quot;)\n"] + block10["match point: + case Point(x=0, y=0): + print(#quot;Origin#quot;) + case Point(x=0, y=y): + print(f#quot;Y={y}#quot;) + case Point(x=x, y=0): + print(f#quot;X={x}#quot;) + case Point(): + print(#quot;Somewhere else#quot;) + case _: + print(#quot;Not a point#quot;)\n"] + block11["class Point: + x: int + y: int\n"] + + start --> block11 + block11 --> block10 + block10 -- "case Point(x=0, y=0)" --> block9 + block10 -- "else" --> block8 + block9 --> block0 + block8 -- "case Point(x=0, y=y)" --> block7 + block8 -- "else" --> block6 + block7 --> block0 + block6 -- "case Point(x=x, y=0)" --> block5 + block6 -- "else" --> block4 + block5 --> block0 + block4 -- "case Point()" --> block3 + block4 -- "else" --> block2 + block3 --> block0 + block2 --> block1 + block1 --> block0 + block0 --> return +``` + +## Function 11 +### Source +```python +def func(points): + match points: + case []: + print("No points") + case [Point(0, 0)]: + print("The origin") + case [Point(x, y)]: + print(f"Single point {x}, {y}") + case [Point(0, y1), Point(0, y2)]: + print(f"Two on the Y axis at {y1}, {y2}") + case _: + print("Something else") +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["print(#quot;Something else#quot;)\n"] + block2["match points: + case []: + print(#quot;No points#quot;) + case [Point(0, 0)]: + print(#quot;The origin#quot;) + case [Point(x, y)]: + print(f#quot;Single point {x}, {y}#quot;) + case [Point(0, y1), Point(0, y2)]: + print(f#quot;Two on the Y axis at {y1}, {y2}#quot;) + case _: + print(#quot;Something else#quot;)\n"] + block3["print(f#quot;Two on the Y axis at {y1}, {y2}#quot;)\n"] + block4["match points: + case []: + print(#quot;No points#quot;) + case [Point(0, 0)]: + print(#quot;The origin#quot;) + case [Point(x, y)]: + print(f#quot;Single point {x}, {y}#quot;) + case [Point(0, y1), Point(0, y2)]: + print(f#quot;Two on the Y axis at {y1}, {y2}#quot;) + case _: + print(#quot;Something else#quot;)\n"] + block5["print(f#quot;Single point {x}, {y}#quot;)\n"] + block6["match points: + case []: + print(#quot;No points#quot;) + case [Point(0, 0)]: + print(#quot;The origin#quot;) + case [Point(x, y)]: + print(f#quot;Single point {x}, {y}#quot;) + case [Point(0, y1), Point(0, y2)]: + print(f#quot;Two on the Y axis at {y1}, {y2}#quot;) + case _: + print(#quot;Something else#quot;)\n"] + block7["print(#quot;The origin#quot;)\n"] + block8["match points: + case []: + print(#quot;No points#quot;) + case [Point(0, 0)]: + print(#quot;The origin#quot;) + case [Point(x, y)]: + print(f#quot;Single point {x}, {y}#quot;) + case [Point(0, y1), Point(0, y2)]: + print(f#quot;Two on the Y axis at {y1}, {y2}#quot;) + case _: + print(#quot;Something else#quot;)\n"] + block9["print(#quot;No points#quot;)\n"] + block10["match points: + case []: + print(#quot;No points#quot;) + case [Point(0, 0)]: + print(#quot;The origin#quot;) + case [Point(x, y)]: + print(f#quot;Single point {x}, {y}#quot;) + case [Point(0, y1), Point(0, y2)]: + print(f#quot;Two on the Y axis at {y1}, {y2}#quot;) + case _: + print(#quot;Something else#quot;)\n"] + + start --> block10 + block10 -- "case []" --> block9 + block10 -- "else" --> block8 + block9 --> block0 + block8 -- "case [Point(0, 0)]" --> block7 + block8 -- "else" --> block6 + block7 --> block0 + block6 -- "case [Point(x, y)]" --> block5 + block6 -- "else" --> block4 + block5 --> block0 + block4 -- "case [Point(0, y1), Point(0, y2)]" --> block3 + block4 -- "else" --> block2 + block3 --> block0 + block2 --> block1 + block1 --> block0 + block0 --> return +``` + +## Function 12 +### Source +```python +def func(point): + match point: + case Point(x, y) if x == y: + print(f"Y=X at {x}") + case Point(x, y): + print(f"Not on the diagonal") +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["print(f#quot;Not on the diagonal#quot;)\n"] + block2["match point: + case Point(x, y) if x == y: + print(f#quot;Y=X at {x}#quot;) + case Point(x, y): + print(f#quot;Not on the diagonal#quot;)\n"] + block3["print(f#quot;Y=X at {x}#quot;)\n"] + block4["match point: + case Point(x, y) if x == y: + print(f#quot;Y=X at {x}#quot;) + case Point(x, y): + print(f#quot;Not on the diagonal#quot;)\n"] + + start --> block4 + block4 -- "case Point(x, y) if x == y" --> block3 + block4 -- "else" --> block2 + block3 --> block0 + block2 -- "case Point(x, y)" --> block1 + block2 -- "else" --> block0 + block1 --> block0 + block0 --> return +``` + +## Function 13 +### Source +```python +def func(): + from enum import Enum + class Color(Enum): + RED = 'red' + GREEN = 'green' + BLUE = 'blue' + + color = Color(input("Enter your choice of 'red', 'blue' or 'green': ")) + + match color: + case Color.RED: + print("I see red!") + case Color.GREEN: + print("Grass is green") + case Color.BLUE: + print("I'm feeling the blues :(") +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["print(#quot;I'm feeling the blues :(#quot;)\n"] + block2["match color: + case Color.RED: + print(#quot;I see red!#quot;) + case Color.GREEN: + print(#quot;Grass is green#quot;) + case Color.BLUE: + print(#quot;I'm feeling the blues :(#quot;)\n"] + block3["print(#quot;Grass is green#quot;)\n"] + block4["match color: + case Color.RED: + print(#quot;I see red!#quot;) + case Color.GREEN: + print(#quot;Grass is green#quot;) + case Color.BLUE: + print(#quot;I'm feeling the blues :(#quot;)\n"] + block5["print(#quot;I see red!#quot;)\n"] + block6["match color: + case Color.RED: + print(#quot;I see red!#quot;) + case Color.GREEN: + print(#quot;Grass is green#quot;) + case Color.BLUE: + print(#quot;I'm feeling the blues :(#quot;)\n"] + block7["from enum import Enum\nclass Color(Enum): + RED = 'red' + GREEN = 'green' + BLUE = 'blue'\ncolor = Color(input(#quot;Enter your choice of 'red', 'blue' or 'green': #quot;))\n"] + + start --> block7 + block7 --> block6 + block6 -- "case Color.RED" --> block5 + block6 -- "else" --> block4 + block5 --> block0 + block4 -- "case Color.GREEN" --> block3 + block4 -- "else" --> block2 + block3 --> block0 + block2 -- "case Color.BLUE" --> block1 + block2 -- "else" --> block0 + block1 --> block0 + block0 --> return +``` + + diff --git a/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__raise.py.md.snap b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__raise.py.md.snap new file mode 100644 index 0000000000..7da998458d --- /dev/null +++ b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__raise.py.md.snap @@ -0,0 +1,41 @@ +--- +source: crates/ruff/src/rules/ruff/rules/unreachable.rs +description: "This is a Mermaid graph. You can use https://mermaid.live to visualize it as a diagram." +--- +## Function 0 +### Source +```python +def func(): + raise Exception +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["raise Exception\n"] + + start --> block0 + block0 --> return +``` + +## Function 1 +### Source +```python +def func(): + raise "a glass!" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["raise #quot;a glass!#quot;\n"] + + start --> block0 + block0 --> return +``` + + diff --git a/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__simple.py.md.snap b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__simple.py.md.snap new file mode 100644 index 0000000000..881df6fad1 --- /dev/null +++ b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__simple.py.md.snap @@ -0,0 +1,136 @@ +--- +source: crates/ruff/src/rules/ruff/rules/unreachable.rs +description: "This is a Mermaid graph. You can use https://mermaid.live to visualize it as a diagram." +--- +## Function 0 +### Source +```python +def func(): + pass +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["pass\n"] + + start --> block0 + block0 --> return +``` + +## Function 1 +### Source +```python +def func(): + pass +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["pass\n"] + + start --> block0 + block0 --> return +``` + +## Function 2 +### Source +```python +def func(): + return +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return\n"] + + start --> block0 + block0 --> return +``` + +## Function 3 +### Source +```python +def func(): + return 1 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 1\n"] + + start --> block0 + block0 --> return +``` + +## Function 4 +### Source +```python +def func(): + return 1 + return "unreachable" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return #quot;unreachable#quot;\n"] + block1["return 1\n"] + + start --> block1 + block1 --> return + block0 --> return +``` + +## Function 5 +### Source +```python +def func(): + i = 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["i = 0\n"] + + start --> block0 + block0 --> return +``` + +## Function 6 +### Source +```python +def func(): + i = 0 + i += 2 + return i +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["i = 0\ni += 2\nreturn i\n"] + + start --> block0 + block0 --> return +``` + + diff --git a/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__while.py.md.snap b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__while.py.md.snap new file mode 100644 index 0000000000..aa030a03a4 --- /dev/null +++ b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__while.py.md.snap @@ -0,0 +1,527 @@ +--- +source: crates/ruff/src/rules/ruff/rules/unreachable.rs +description: "This is a Mermaid graph. You can use https://mermaid.live to visualize it as a diagram." +--- +## Function 0 +### Source +```python +def func(): + while False: + return "unreachable" + return 1 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 1\n"] + block1["return #quot;unreachable#quot;\n"] + block2["while False: + return #quot;unreachable#quot;\n"] + + start --> block2 + block2 -- "False" --> block1 + block2 -- "else" --> block0 + block1 --> return + block0 --> return +``` + +## Function 1 +### Source +```python +def func(): + while False: + return "unreachable" + else: + return 1 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return #quot;unreachable#quot;\n"] + block1["return 1\n"] + block2["while False: + return #quot;unreachable#quot; + else: + return 1\n"] + + start --> block2 + block2 -- "False" --> block0 + block2 -- "else" --> block1 + block1 --> return + block0 --> return +``` + +## Function 2 +### Source +```python +def func(): + while False: + return "unreachable" + else: + return 1 + return "also unreachable" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return #quot;also unreachable#quot;\n"] + block1["return #quot;unreachable#quot;\n"] + block2["return 1\n"] + block3["while False: + return #quot;unreachable#quot; + else: + return 1\n"] + + start --> block3 + block3 -- "False" --> block1 + block3 -- "else" --> block2 + block2 --> return + block1 --> return + block0 --> return +``` + +## Function 3 +### Source +```python +def func(): + while True: + return 1 + return "unreachable" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return #quot;unreachable#quot;\n"] + block1["return 1\n"] + block2["while True: + return 1\n"] + + start --> block2 + block2 -- "True" --> block1 + block2 -- "else" --> block0 + block1 --> return + block0 --> return +``` + +## Function 4 +### Source +```python +def func(): + while True: + return 1 + else: + return "unreachable" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 1\n"] + block1["return #quot;unreachable#quot;\n"] + block2["while True: + return 1 + else: + return #quot;unreachable#quot;\n"] + + start --> block2 + block2 -- "True" --> block0 + block2 -- "else" --> block1 + block1 --> return + block0 --> return +``` + +## Function 5 +### Source +```python +def func(): + while True: + return 1 + else: + return "unreachable" + return "also unreachable" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return #quot;also unreachable#quot;\n"] + block1["return 1\n"] + block2["return #quot;unreachable#quot;\n"] + block3["while True: + return 1 + else: + return #quot;unreachable#quot;\n"] + + start --> block3 + block3 -- "True" --> block1 + block3 -- "else" --> block2 + block2 --> return + block1 --> return + block0 --> return +``` + +## Function 6 +### Source +```python +def func(): + i = 0 + while False: + i += 1 + return i +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return i\n"] + block1["i += 1\n"] + block2["i = 0\nwhile False: + i += 1\n"] + + start --> block2 + block2 -- "False" --> block1 + block2 -- "else" --> block0 + block1 --> block2 + block0 --> return +``` + +## Function 7 +### Source +```python +def func(): + i = 0 + while True: + i += 1 + return i +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return i\n"] + block1["i += 1\n"] + block2["i = 0\nwhile True: + i += 1\n"] + + start --> block2 + block2 -- "True" --> block1 + block2 -- "else" --> block0 + block1 --> block2 + block0 --> return +``` + +## Function 8 +### Source +```python +def func(): + while True: + pass + return 1 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 1\n"] + block1["pass\n"] + block2["while True: + pass\n"] + + start --> block2 + block2 -- "True" --> block1 + block2 -- "else" --> block0 + block1 --> block2 + block0 --> return +``` + +## Function 9 +### Source +```python +def func(): + i = 0 + while True: + if True: + print("ok") + i += 1 + return i +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return i\n"] + block1["i += 1\n"] + block2["print(#quot;ok#quot;)\n"] + block3["if True: + print(#quot;ok#quot;)\n"] + block4["i = 0\nwhile True: + if True: + print(#quot;ok#quot;) + i += 1\n"] + + start --> block4 + block4 -- "True" --> block3 + block4 -- "else" --> block0 + block3 -- "True" --> block2 + block3 -- "else" --> block1 + block2 --> block1 + block1 --> block4 + block0 --> return +``` + +## Function 10 +### Source +```python +def func(): + i = 0 + while True: + if False: + print("ok") + i += 1 + return i +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return i\n"] + block1["i += 1\n"] + block2["print(#quot;ok#quot;)\n"] + block3["if False: + print(#quot;ok#quot;)\n"] + block4["i = 0\nwhile True: + if False: + print(#quot;ok#quot;) + i += 1\n"] + + start --> block4 + block4 -- "True" --> block3 + block4 -- "else" --> block0 + block3 -- "False" --> block2 + block3 -- "else" --> block1 + block2 --> block1 + block1 --> block4 + block0 --> return +``` + +## Function 11 +### Source +```python +def func(): + while True: + if True: + return 1 + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 0\n"] + block1["return 1\n"] + block2["if True: + return 1\n"] + block3["while True: + if True: + return 1\n"] + + start --> block3 + block3 -- "True" --> block2 + block3 -- "else" --> block0 + block2 -- "True" --> block1 + block2 -- "else" --> block3 + block1 --> return + block0 --> return +``` + +## Function 12 +### Source +```python +def func(): + while True: + continue +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["continue\n"] + block2["while True: + continue\n"] + + start --> block2 + block2 -- "True" --> block1 + block2 -- "else" --> block0 + block1 --> block2 + block0 --> return +``` + +## Function 13 +### Source +```python +def func(): + while False: + continue +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["continue\n"] + block2["while False: + continue\n"] + + start --> block2 + block2 -- "False" --> block1 + block2 -- "else" --> block0 + block1 --> block2 + block0 --> return +``` + +## Function 14 +### Source +```python +def func(): + while True: + break +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["break\n"] + block2["while True: + break\n"] + + start --> block2 + block2 -- "True" --> block1 + block2 -- "else" --> block0 + block1 --> block0 + block0 --> return +``` + +## Function 15 +### Source +```python +def func(): + while False: + break +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["break\n"] + block2["while False: + break\n"] + + start --> block2 + block2 -- "False" --> block1 + block2 -- "else" --> block0 + block1 --> block0 + block0 --> return +``` + +## Function 16 +### Source +```python +def func(): + while True: + if True: + continue +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["continue\n"] + block2["if True: + continue\n"] + block3["while True: + if True: + continue\n"] + + start --> block3 + block3 -- "True" --> block2 + block3 -- "else" --> block0 + block2 -- "True" --> block1 + block2 -- "else" --> block3 + block1 --> block3 + block0 --> return +``` + +## Function 17 +### Source +```python +def func(): + while True: + if True: + break +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["break\n"] + block2["if True: + break\n"] + block3["while True: + if True: + break\n"] + + start --> block3 + block3 -- "True" --> block2 + block3 -- "else" --> block0 + block2 -- "True" --> block1 + block2 -- "else" --> block3 + block1 --> block0 + block0 --> return +``` + + diff --git a/crates/ruff/src/rules/ruff/rules/unnecessary_iterable_allocation_for_first_element.rs b/crates/ruff/src/rules/ruff/rules/unnecessary_iterable_allocation_for_first_element.rs new file mode 100644 index 0000000000..17fa0922e5 --- /dev/null +++ b/crates/ruff/src/rules/ruff/rules/unnecessary_iterable_allocation_for_first_element.rs @@ -0,0 +1,233 @@ +use num_bigint::BigInt; +use num_traits::{One, Zero}; +use rustpython_parser::ast::{self, Comprehension, Constant, Expr, ExprSubscript}; + +use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_semantic::SemanticModel; + +use crate::checkers::ast::Checker; +use crate::registry::AsRule; + +/// ## What it does +/// Checks for uses of `list(...)[0]` that can be replaced with +/// `next(iter(...))`. +/// +/// ## Why is this bad? +/// Calling `list(...)` will create a new list of the entire collection, which +/// can be very expensive for large collections. If you only need the first +/// element of the collection, you can use `next(iter(...))` to lazily fetch +/// the first element without creating a new list. +/// +/// Note that migrating from `list(...)[0]` to `next(iter(...))` can change +/// the behavior of your program in two ways: +/// +/// 1. First, `list(...)` will eagerly evaluate the entire collection, while +/// `next(iter(...))` will only evaluate the first element. As such, any +/// side effects that occur during iteration will be delayed. +/// 2. Second, `list(...)[0]` will raise `IndexError` if the collection is +/// empty, while `next(iter(...))` will raise `StopIteration`. +/// +/// ## Example +/// ```python +/// head = list(range(1000000000000))[0] +/// ``` +/// +/// Use instead: +/// ```python +/// head = next(iter(range(1000000000000))) +/// ``` +/// +/// ## References +/// - [Iterators and Iterables in Python: Run Efficient Iterations](https://realpython.com/python-iterators-iterables/#when-to-use-an-iterator-in-python) +#[violation] +pub(crate) struct UnnecessaryIterableAllocationForFirstElement { + iterable: String, + subscript_kind: HeadSubscriptKind, +} + +impl AlwaysAutofixableViolation for UnnecessaryIterableAllocationForFirstElement { + #[derive_message_formats] + fn message(&self) -> String { + let UnnecessaryIterableAllocationForFirstElement { + iterable, + subscript_kind, + } = self; + match subscript_kind { + HeadSubscriptKind::Index => { + format!("Prefer `next(iter({iterable}))` over `list({iterable})[0]`") + } + HeadSubscriptKind::Slice => { + format!("Prefer `[next(iter({iterable}))]` over `list({iterable})[:1]`") + } + } + } + + fn autofix_title(&self) -> String { + let UnnecessaryIterableAllocationForFirstElement { + iterable, + subscript_kind, + } = self; + match subscript_kind { + HeadSubscriptKind::Index => format!("Replace with `next(iter({iterable}))`"), + HeadSubscriptKind::Slice => format!("Replace with `[next(iter({iterable}))]"), + } + } +} + +/// RUF015 +pub(crate) fn unnecessary_iterable_allocation_for_first_element( + checker: &mut Checker, + subscript: &ExprSubscript, +) { + let ast::ExprSubscript { + value, + slice, + range, + .. + } = subscript; + + let Some(subscript_kind) = classify_subscript(slice) else { + return; + }; + + let Some(iterable) = iterable_name(value, checker.semantic()) else { + return; + }; + + let mut diagnostic = Diagnostic::new( + UnnecessaryIterableAllocationForFirstElement { + iterable: iterable.to_string(), + subscript_kind, + }, + *range, + ); + + if checker.patch(diagnostic.kind.rule()) { + let replacement = match subscript_kind { + HeadSubscriptKind::Index => format!("next(iter({iterable}))"), + HeadSubscriptKind::Slice => format!("[next(iter({iterable}))]"), + }; + diagnostic.set_fix(Fix::suggested(Edit::range_replacement(replacement, *range))); + } + + checker.diagnostics.push(diagnostic); +} + +/// A subscript slice that represents the first element of a list. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum HeadSubscriptKind { + /// The subscript is an index (e.g., `[0]`). + Index, + /// The subscript is a slice (e.g., `[:1]`). + Slice, +} + +/// Check that the slice [`Expr`] is functionally equivalent to slicing into the first element. The +/// first `bool` checks that the element is in fact first, the second checks if it's a slice or an +/// index. +fn classify_subscript(expr: &Expr) -> Option { + match expr { + Expr::Constant(ast::ExprConstant { + value: Constant::Int(value), + .. + }) if value.is_zero() => Some(HeadSubscriptKind::Index), + Expr::Slice(ast::ExprSlice { + step, lower, upper, .. + }) => { + // Avoid, e.g., `list(...)[:2]` + let upper = upper.as_ref()?; + let upper = as_int(upper)?; + if !upper.is_one() { + return None; + } + + // Avoid, e.g., `list(...)[2:]`. + if let Some(lower) = lower.as_ref() { + let lower = as_int(lower)?; + if !lower.is_zero() { + return None; + } + } + + // Avoid, e.g., `list(...)[::-1]` + if let Some(step) = step.as_ref() { + let step = as_int(step)?; + if step < upper { + return None; + } + } + + Some(HeadSubscriptKind::Slice) + } + _ => None, + } +} + +/// Fetch the name of the iterable from an expression if the expression returns an unmodified list +/// which can be sliced into. +fn iterable_name<'a>(expr: &'a Expr, model: &SemanticModel) -> Option<&'a str> { + match expr { + Expr::Call(ast::ExprCall { func, args, .. }) => { + let ast::ExprName { id, .. } = func.as_name_expr()?; + + if !matches!(id.as_str(), "tuple" | "list") { + return None; + } + + if !model.is_builtin(id.as_str()) { + return None; + } + + match args.first() { + Some(Expr::Name(ast::ExprName { id: arg_name, .. })) => Some(arg_name.as_str()), + Some(Expr::GeneratorExp(ast::ExprGeneratorExp { + elt, generators, .. + })) => generator_iterable(elt, generators), + _ => None, + } + } + Expr::ListComp(ast::ExprListComp { + elt, generators, .. + }) => generator_iterable(elt, generators), + _ => None, + } +} + +/// Given a comprehension, returns the name of the iterable over which it iterates, if it's +/// a simple comprehension (e.g., `x` for `[i for i in x]`). +fn generator_iterable<'a>(elt: &'a Expr, generators: &'a Vec) -> Option<&'a str> { + // If the `elt` field is anything other than a [`Expr::Name`], we can't be sure that it + // doesn't modify the elements of the underlying iterator (e.g., `[i + 1 for i in x][0]`). + if !elt.is_name_expr() { + return None; + } + + // If there's more than 1 generator, we can't safely say that it fits the diagnostic conditions + // (e.g., `[(i, j) for i in x for j in y][0]`). + let [generator] = generators.as_slice() else { + return None; + }; + + // Ignore if there's an `if` statement in the comprehension, since it filters the list. + if !generator.ifs.is_empty() { + return None; + } + + let ast::ExprName { id, .. } = generator.iter.as_name_expr()?; + Some(id.as_str()) +} + +/// If an expression is a constant integer, returns the value of that integer; otherwise, +/// returns `None`. +fn as_int(expr: &Expr) -> Option<&BigInt> { + if let Expr::Constant(ast::ExprConstant { + value: Constant::Int(value), + .. + }) = expr + { + Some(value) + } else { + None + } +} diff --git a/crates/ruff/src/rules/ruff/rules/unreachable.rs b/crates/ruff/src/rules/ruff/rules/unreachable.rs new file mode 100644 index 0000000000..5c81acd9be --- /dev/null +++ b/crates/ruff/src/rules/ruff/rules/unreachable.rs @@ -0,0 +1,1101 @@ +use std::{fmt, iter, usize}; + +use log::error; +use rustpython_parser::ast::{ + Expr, Identifier, MatchCase, Pattern, PatternMatchAs, Ranged, Stmt, StmtAsyncFor, + StmtAsyncWith, StmtFor, StmtMatch, StmtReturn, StmtTry, StmtTryStar, StmtWhile, StmtWith, +}; +use rustpython_parser::text_size::{TextRange, TextSize}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_index::{IndexSlice, IndexVec}; +use ruff_macros::{derive_message_formats, newtype_index, violation}; + +/// ## What it does +/// Checks for unreachable code. +/// +/// ## Why is this bad? +/// Unreachable code can be a maintenance burden without ever being used. +/// +/// ## Example +/// ```python +/// def function(): +/// if False: +/// return "unreachable" +/// return "reachable" +/// ``` +/// +/// Use instead: +/// ```python +/// def function(): +/// return "reachable" +/// ``` +#[violation] +pub struct UnreachableCode { + name: String, +} + +impl Violation for UnreachableCode { + #[derive_message_formats] + fn message(&self) -> String { + let UnreachableCode { name } = self; + format!("Unreachable code in {name}") + } +} + +pub(crate) fn in_function(name: &Identifier, body: &[Stmt]) -> Vec { + // Create basic code blocks from the body. + let basic_blocks = BasicBlocks::from(body); + + // Basic on the code blocks we can (more) easily follow what statements are + // and aren't reached, we'll mark them as such in `reached_map`. + let mut reached_map = Bitmap::with_capacity(basic_blocks.len()); + + if let Some(start_index) = basic_blocks.start_index() { + mark_reached(&mut reached_map, &basic_blocks.blocks, start_index); + } + + // For each unreached code block create a diagnostic. + reached_map + .unset() + .filter_map(|idx| { + let block = &basic_blocks.blocks[idx]; + if block.is_sentinel() { + return None; + } + + // TODO: add more information to the diagnostic. Include the entire + // code block, not just the first line. Maybe something to indicate + // the code flow and where it prevents this block from being reached + // for example. + let Some(stmt) = block.stmts.first() else { + // This should never happen. + error!("Got an unexpected empty code block"); + return None; + }; + Some(Diagnostic::new( + UnreachableCode { + name: name.as_str().to_owned(), + }, + stmt.range(), + )) + }) + .collect() +} + +/// Simple bitmap. +#[derive(Debug)] +struct Bitmap { + bits: Box<[usize]>, + capacity: usize, +} + +impl Bitmap { + /// Create a new `Bitmap` with `capacity` capacity. + fn with_capacity(capacity: usize) -> Bitmap { + let mut size = capacity / usize::BITS as usize; + if (capacity % usize::BITS as usize) != 0 { + size += 1; + } + Bitmap { + bits: vec![0; size].into_boxed_slice(), + capacity, + } + } + + /// Set bit at index `idx` to true. + /// + /// Returns a boolean indicating if the bit was already set. + fn set(&mut self, idx: BlockIndex) -> bool { + let bits_index = (idx.as_u32() / usize::BITS) as usize; + let shift = idx.as_u32() % usize::BITS; + if (self.bits[bits_index] & (1 << shift)) == 0 { + self.bits[bits_index] |= 1 << shift; + false + } else { + true + } + } + + /// Returns an iterator of all unset indices. + fn unset(&self) -> impl Iterator + '_ { + let mut index = 0; + let mut shift = 0; + let last_max_shift = self.capacity % usize::BITS as usize; + iter::from_fn(move || loop { + if shift >= usize::BITS as usize { + shift = 0; + index += 1; + } + if self.bits.len() <= index || (index >= self.bits.len() - 1 && shift >= last_max_shift) + { + return None; + } + + let is_set = (self.bits[index] & (1 << shift)) != 0; + shift += 1; + if !is_set { + return Some(BlockIndex::from_usize( + (index * usize::BITS as usize) + shift - 1, + )); + } + }) + } +} + +/// Set bits in `reached_map` for all blocks that are reached in `blocks` +/// starting with block at index `idx`. +fn mark_reached( + reached_map: &mut Bitmap, + blocks: &IndexSlice>, + start_index: BlockIndex, +) { + let mut idx = start_index; + + loop { + let block = &blocks[idx]; + if reached_map.set(idx) { + return; // Block already visited, no needed to do it again. + } + + match &block.next { + NextBlock::Always(next) => idx = *next, + NextBlock::If { + condition, + next, + orelse, + } => { + match taken(condition) { + Some(true) => idx = *next, // Always taken. + Some(false) => idx = *orelse, // Never taken. + None => { + // Don't know, both branches might be taken. + idx = *next; + mark_reached(reached_map, blocks, *orelse); + } + } + } + NextBlock::Terminate => return, + } + } +} + +/// Determines if `condition` is taken. +/// Returns `Some(true)` if the condition is always true, e.g. `if True`, same +/// with `Some(false)` if it's never taken. If it can't be determined it returns +/// `None`, e.g. `If i == 100`. +fn taken(condition: &Condition) -> Option { + // TODO: add more cases to this where we can determine a condition + // statically. For now we only consider constant booleans. + match condition { + Condition::Test(expr) => match expr { + Expr::Constant(constant) => constant.value.as_bool().copied(), + _ => None, + }, + Condition::Iterator(_) => None, + Condition::Match { .. } => None, + } +} + +/// Index into [`BasicBlocks::blocks`]. +#[newtype_index] +#[derive(PartialOrd, Ord)] +struct BlockIndex; + +/// Collection of basic block. +#[derive(Debug, PartialEq)] +struct BasicBlocks<'stmt> { + /// # Notes + /// + /// The order of these block is unspecified. However it's guaranteed that + /// the last block is the first statement in the function and the first + /// block is the last statement. The block are more or less in reverse + /// order, but it gets fussy around control flow statements (e.g. `while` + /// statements). + /// + /// For loop blocks, and similar recurring control flows, the end of the + /// body will point to the loop block again (to create the loop). However an + /// oddity here is that this block might contain statements before the loop + /// itself which, of course, won't be executed again. + /// + /// For example: + /// ```python + /// i = 0 # block 0 + /// while True: # + /// continue # block 1 + /// ``` + /// Will create a connection between block 1 (loop body) and block 0, which + /// includes the `i = 0` statement. + /// + /// To keep `NextBlock` simple(r) `NextBlock::If`'s `next` and `orelse` + /// fields only use `BlockIndex`, which means that they can't terminate + /// themselves. To support this we insert *empty*/fake blocks before the end + /// of the function that we can link to. + /// + /// Finally `BasicBlock` can also be a sentinel node, see the associated + /// constants of [`BasicBlock`]. + blocks: IndexVec>, +} + +impl BasicBlocks<'_> { + fn len(&self) -> usize { + self.blocks.len() + } + + fn start_index(&self) -> Option { + self.blocks.indices().last() + } +} + +impl<'stmt> From<&'stmt [Stmt]> for BasicBlocks<'stmt> { + /// # Notes + /// + /// This assumes that `stmts` is a function body. + fn from(stmts: &'stmt [Stmt]) -> BasicBlocks<'stmt> { + let mut blocks = BasicBlocksBuilder::with_capacity(stmts.len()); + + blocks.create_blocks(stmts, None); + + blocks.finish() + } +} + +/// Basic code block, sequence of statements unconditionally executed +/// "together". +#[derive(Debug, PartialEq)] +struct BasicBlock<'stmt> { + stmts: &'stmt [Stmt], + next: NextBlock<'stmt>, +} + +/// Edge between basic blocks (in the control-flow graph). +#[derive(Debug, PartialEq)] +enum NextBlock<'stmt> { + /// Always continue with a block. + Always(BlockIndex), + /// Condition jump. + If { + /// Condition that needs to be evaluated to jump to the `next` or + /// `orelse` block. + condition: Condition<'stmt>, + /// Next block if `condition` is true. + next: BlockIndex, + /// Next block if `condition` is false. + orelse: BlockIndex, + }, + /// The end. + Terminate, +} + +/// Condition used to determine to take the `next` or `orelse` branch in +/// [`NextBlock::If`]. +#[derive(Clone, Debug, PartialEq)] +enum Condition<'stmt> { + /// Conditional statement, this should evaluate to a boolean, for e.g. `if` + /// or `while`. + Test(&'stmt Expr), + /// Iterator for `for` statements, e.g. for `i in range(10)` this will be + /// `range(10)`. + Iterator(&'stmt Expr), + Match { + /// `match $subject`. + subject: &'stmt Expr, + /// `case $case`, include pattern, guard, etc. + case: &'stmt MatchCase, + }, +} + +impl<'stmt> Ranged for Condition<'stmt> { + fn range(&self) -> TextRange { + match self { + Condition::Test(expr) | Condition::Iterator(expr) => expr.range(), + // The case of the match statement, without the body. + Condition::Match { subject: _, case } => TextRange::new( + case.start(), + case.guard + .as_ref() + .map_or(case.pattern.end(), |guard| guard.end()), + ), + } + } +} + +impl<'stmt> BasicBlock<'stmt> { + /// A sentinel block indicating an empty termination block. + const EMPTY: BasicBlock<'static> = BasicBlock { + stmts: &[], + next: NextBlock::Terminate, + }; + + /// A sentinel block indicating an exception was raised. + const EXCEPTION: BasicBlock<'static> = BasicBlock { + stmts: &[Stmt::Return(StmtReturn { + range: TextRange::new(TextSize::new(0), TextSize::new(0)), + value: None, + })], + next: NextBlock::Terminate, + }; + + /// Return true if the block is a sentinel or fake block. + fn is_sentinel(&self) -> bool { + self.is_empty() || self.is_exception() + } + + /// Returns an empty block that terminates. + fn is_empty(&self) -> bool { + matches!(self.next, NextBlock::Terminate) && self.stmts.is_empty() + } + + /// Returns true if `self` an [`BasicBlock::EXCEPTION`]. + fn is_exception(&self) -> bool { + matches!(self.next, NextBlock::Terminate) && BasicBlock::EXCEPTION.stmts == self.stmts + } +} + +/// Handle a loop block, such as a `while`, `for` or `async for` statement. +fn loop_block<'stmt>( + blocks: &mut BasicBlocksBuilder<'stmt>, + condition: Condition<'stmt>, + body: &'stmt [Stmt], + orelse: &'stmt [Stmt], + after: Option, +) -> NextBlock<'stmt> { + let after_block = blocks.maybe_next_block_index(after, || orelse.is_empty()); + // NOTE: a while loop's body must not be empty, so we can safely + // create at least one block from it. + let last_statement_index = blocks.append_blocks(body, after); + let last_orelse_statement = blocks.append_blocks_if_not_empty(orelse, after_block); + // `create_blocks` always continues to the next block by + // default. However in a while loop we want to continue with the + // while block (we're about to create) to create the loop. + // NOTE: `blocks.len()` is an invalid index at time of creation + // as it points to the block which we're about to create. + blocks.change_next_block( + last_statement_index, + after_block, + blocks.blocks.next_index(), + |block| { + // For `break` statements we don't want to continue with the + // loop, but instead with the statement after the loop (i.e. + // not change anything). + !block.stmts.last().map_or(false, Stmt::is_break_stmt) + }, + ); + NextBlock::If { + condition, + next: last_statement_index, + orelse: last_orelse_statement, + } +} + +/// Handle a single match case. +/// +/// `next_after_block` is the block *after* the entire match statement that is +/// taken after this case is taken. +/// `orelse_after_block` is the next match case (or the block after the match +/// statement if this is the last case). +fn match_case<'stmt>( + blocks: &mut BasicBlocksBuilder<'stmt>, + match_stmt: &'stmt Stmt, + subject: &'stmt Expr, + case: &'stmt MatchCase, + next_after_block: BlockIndex, + orelse_after_block: BlockIndex, +) -> BasicBlock<'stmt> { + // FIXME: this is not ideal, we want to only use the `case` statement here, + // but that is type `MatchCase`, not `Stmt`. For now we'll point to the + // entire match statement. + let stmts = std::slice::from_ref(match_stmt); + let next_block_index = if case.body.is_empty() { + next_after_block + } else { + let from = blocks.last_index(); + let last_statement_index = blocks.append_blocks(&case.body, Some(next_after_block)); + if let Some(from) = from { + blocks.change_next_block(last_statement_index, from, next_after_block, |_| true); + } + last_statement_index + }; + // TODO: handle named arguments, e.g. + // ```python + // match $subject: + // case $binding: + // print($binding) + // ``` + // These should also return `NextBlock::Always`. + let next = if is_wildcard(case) { + // Wildcard case is always taken. + NextBlock::Always(next_block_index) + } else { + NextBlock::If { + condition: Condition::Match { subject, case }, + next: next_block_index, + orelse: orelse_after_block, + } + }; + BasicBlock { stmts, next } +} + +/// Returns true if `pattern` is a wildcard (`_`) pattern. +fn is_wildcard(pattern: &MatchCase) -> bool { + pattern.guard.is_none() + && matches!(&pattern.pattern, Pattern::MatchAs(PatternMatchAs { pattern, name, .. }) if pattern.is_none() && name.is_none()) +} + +#[derive(Debug, Default)] +struct BasicBlocksBuilder<'stmt> { + blocks: IndexVec>, +} + +impl<'stmt> BasicBlocksBuilder<'stmt> { + fn with_capacity(capacity: usize) -> Self { + Self { + blocks: IndexVec::with_capacity(capacity), + } + } + + /// Creates basic blocks from `stmts` and appends them to `blocks`. + fn create_blocks( + &mut self, + stmts: &'stmt [Stmt], + mut after: Option, + ) -> Option { + // We process the statements in reverse so that we can always point to the + // next block (as that should always be processed). + let mut stmts_iter = stmts.iter().enumerate().rev().peekable(); + while let Some((i, stmt)) = stmts_iter.next() { + let next = match stmt { + // Statements that continue to the next statement after execution. + Stmt::FunctionDef(_) + | Stmt::AsyncFunctionDef(_) + | Stmt::Import(_) + | Stmt::ImportFrom(_) + | Stmt::ClassDef(_) + | Stmt::Global(_) + | Stmt::Nonlocal(_) + | Stmt::Delete(_) + | Stmt::Assign(_) + | Stmt::AugAssign(_) + | Stmt::AnnAssign(_) + | Stmt::Break(_) + | Stmt::Pass(_) => self.unconditional_next_block(after), + Stmt::Continue(_) => { + // NOTE: the next branch gets fixed up in `change_next_block`. + self.unconditional_next_block(after) + } + // Statements that (can) divert the control flow. + Stmt::If(stmt) => { + let next_after_block = + self.maybe_next_block_index(after, || needs_next_block(&stmt.body)); + let orelse_after_block = + self.maybe_next_block_index(after, || needs_next_block(&stmt.orelse)); + let next = self.append_blocks_if_not_empty(&stmt.body, next_after_block); + let orelse = self.append_blocks_if_not_empty(&stmt.orelse, orelse_after_block); + NextBlock::If { + condition: Condition::Test(&stmt.test), + next, + orelse, + } + } + Stmt::While(StmtWhile { + test: condition, + body, + orelse, + .. + }) => loop_block(self, Condition::Test(condition), body, orelse, after), + Stmt::For(StmtFor { + iter: condition, + body, + orelse, + .. + }) + | Stmt::AsyncFor(StmtAsyncFor { + iter: condition, + body, + orelse, + .. + }) => loop_block(self, Condition::Iterator(condition), body, orelse, after), + Stmt::Try(StmtTry { + body, + handlers, + orelse, + finalbody, + .. + }) + | Stmt::TryStar(StmtTryStar { + body, + handlers, + orelse, + finalbody, + .. + }) => { + // TODO: handle `try` statements. The `try` control flow is very + // complex, what blocks are and aren't taken and from which + // block the control flow is actually returns is **very** + // specific to the contents of the block. Read + // + // very carefully. + // For now we'll skip over it. + let _ = (body, handlers, orelse, finalbody); // Silence unused code warnings. + self.unconditional_next_block(after) + } + Stmt::With(StmtWith { + items, + body, + type_comment, + .. + }) + | Stmt::AsyncWith(StmtAsyncWith { + items, + body, + type_comment, + .. + }) => { + // TODO: handle `with` statements, see + // . + // I recommend to `try` statements first as `with` can desugar + // to a `try` statement. + // For now we'll skip over it. + let _ = (items, body, type_comment); // Silence unused code warnings. + self.unconditional_next_block(after) + } + Stmt::Match(StmtMatch { subject, cases, .. }) => { + let next_after_block = self.maybe_next_block_index(after, || { + // We don't need need a next block if all cases don't need a + // next block, i.e. if no cases need a next block, and we + // have a wildcard case (to ensure one of the block is + // always taken). + // NOTE: match statement require at least one case, so we + // don't have to worry about empty `cases`. + // TODO: support exhaustive cases without a wildcard. + cases.iter().any(|case| needs_next_block(&case.body)) + || !cases.iter().any(is_wildcard) + }); + let mut orelse_after_block = next_after_block; + for case in cases.iter().rev() { + let block = match_case( + self, + stmt, + subject, + case, + next_after_block, + orelse_after_block, + ); + // For the case above this use the just added case as the + // `orelse` branch, this convert the match statement to + // (essentially) a bunch of if statements. + orelse_after_block = self.blocks.push(block); + } + // TODO: currently we don't include the lines before the match + // statement in the block, unlike what we do for other + // statements. + after = Some(orelse_after_block); + continue; + } + Stmt::Raise(_) => { + // TODO: this needs special handling within `try` and `with` + // statements. For now we just terminate the execution, it's + // possible it's continued in an `catch` or `finally` block, + // possibly outside of the function. + // Also see `Stmt::Assert` handling. + NextBlock::Terminate + } + Stmt::Assert(stmt) => { + // TODO: this needs special handling within `try` and `with` + // statements. For now we just terminate the execution if the + // assertion fails, it's possible it's continued in an `catch` + // or `finally` block, possibly outside of the function. + // Also see `Stmt::Raise` handling. + let next = self.force_next_block_index(); + let orelse = self.fake_exception_block_index(); + NextBlock::If { + condition: Condition::Test(&stmt.test), + next, + orelse, + } + } + Stmt::Expr(stmt) => { + match &*stmt.value { + Expr::BoolOp(_) + | Expr::BinOp(_) + | Expr::UnaryOp(_) + | Expr::Dict(_) + | Expr::Set(_) + | Expr::Compare(_) + | Expr::Call(_) + | Expr::FormattedValue(_) + | Expr::JoinedStr(_) + | Expr::Constant(_) + | Expr::Attribute(_) + | Expr::Subscript(_) + | Expr::Starred(_) + | Expr::Name(_) + | Expr::List(_) + | Expr::Tuple(_) + | Expr::Slice(_) => self.unconditional_next_block(after), + // TODO: handle these expressions. + Expr::NamedExpr(_) + | Expr::Lambda(_) + | Expr::IfExp(_) + | Expr::ListComp(_) + | Expr::SetComp(_) + | Expr::DictComp(_) + | Expr::GeneratorExp(_) + | Expr::Await(_) + | Expr::Yield(_) + | Expr::YieldFrom(_) => self.unconditional_next_block(after), + } + } + // The tough branches are done, here is an easy one. + Stmt::Return(_) => NextBlock::Terminate, + }; + + // Include any statements in the block that don't divert the control flow. + let mut start = i; + let end = i + 1; + while stmts_iter + .next_if(|(_, stmt)| !is_control_flow_stmt(stmt)) + .is_some() + { + start -= 1; + } + + let block = BasicBlock { + stmts: &stmts[start..end], + next, + }; + after = Some(self.blocks.push(block)); + } + + after + } + + /// Calls [`create_blocks`] and returns this first block reached (i.e. the last + /// block). + fn append_blocks(&mut self, stmts: &'stmt [Stmt], after: Option) -> BlockIndex { + assert!(!stmts.is_empty()); + self.create_blocks(stmts, after) + .expect("Expect `create_blocks` to create a block if `stmts` is not empty") + } + + /// If `stmts` is not empty this calls [`create_blocks`] and returns this first + /// block reached (i.e. the last block). If `stmts` is empty this returns + /// `after` and doesn't change `blocks`. + fn append_blocks_if_not_empty( + &mut self, + stmts: &'stmt [Stmt], + after: BlockIndex, + ) -> BlockIndex { + if stmts.is_empty() { + after // Empty body, continue with block `after` it. + } else { + self.append_blocks(stmts, Some(after)) + } + } + + /// Select the next block from `blocks` unconditionally. + fn unconditional_next_block(&self, after: Option) -> NextBlock<'static> { + if let Some(after) = after { + return NextBlock::Always(after); + } + + // Either we continue with the next block (that is the last block `blocks`). + // Or it's the last statement, thus we terminate. + self.blocks + .last_index() + .map_or(NextBlock::Terminate, NextBlock::Always) + } + + /// Select the next block index from `blocks`. If there is no next block it will + /// add a fake/empty block. + fn force_next_block_index(&mut self) -> BlockIndex { + self.maybe_next_block_index(None, || true) + } + + /// Select the next block index from `blocks`. If there is no next block it will + /// add a fake/empty block if `condition` returns true. If `condition` returns + /// false the returned index may not be used. + fn maybe_next_block_index( + &mut self, + after: Option, + condition: impl FnOnce() -> bool, + ) -> BlockIndex { + if let Some(after) = after { + // Next block is already determined. + after + } else if let Some(idx) = self.blocks.last_index() { + // Otherwise we either continue with the next block (that is the last + // block in `blocks`). + idx + } else if condition() { + // Or if there are no blocks, but need one based on `condition` than we + // add a fake end block. + self.blocks.push(BasicBlock::EMPTY) + } else { + // NOTE: invalid, but because `condition` returned false this shouldn't + // be used. This only used as an optimisation to avoid adding fake end + // blocks. + BlockIndex::MAX + } + } + + /// Returns a block index for a fake exception block in `blocks`. + fn fake_exception_block_index(&mut self) -> BlockIndex { + for (i, block) in self.blocks.iter_enumerated() { + if block.is_exception() { + return i; + } + } + self.blocks.push(BasicBlock::EXCEPTION) + } + + /// Change the next basic block for the block, or chain of blocks, in index + /// `fixup_index` from `from` to `to`. + /// + /// This doesn't change the target if it's `NextBlock::Terminate`. + fn change_next_block( + &mut self, + mut fixup_index: BlockIndex, + from: BlockIndex, + to: BlockIndex, + check_condition: impl Fn(&BasicBlock) -> bool + Copy, + ) { + /// Check if we found our target and if `check_condition` is met. + fn is_target( + block: &BasicBlock<'_>, + got: BlockIndex, + expected: BlockIndex, + check_condition: impl Fn(&BasicBlock) -> bool, + ) -> bool { + got == expected && check_condition(block) + } + + loop { + match self.blocks.get(fixup_index).map(|b| &b.next) { + Some(NextBlock::Always(next)) => { + let next = *next; + if is_target(&self.blocks[fixup_index], next, from, check_condition) { + // Found our target, change it. + self.blocks[fixup_index].next = NextBlock::Always(to); + } + return; + } + Some(NextBlock::If { + condition, + next, + orelse, + }) => { + let idx = fixup_index; + let condition = condition.clone(); + let next = *next; + let orelse = *orelse; + let new_next = if is_target(&self.blocks[idx], next, from, check_condition) { + // Found our target in the next branch, change it (below). + Some(to) + } else { + // Follow the chain. + fixup_index = next; + None + }; + + let new_orelse = if is_target(&self.blocks[idx], orelse, from, check_condition) + { + // Found our target in the else branch, change it (below). + Some(to) + } else if new_next.is_none() { + // If we done with the next branch we only continue with the + // else branch. + fixup_index = orelse; + None + } else { + // If we're not done with the next and else branches we need + // to deal with the else branch before deal with the next + // branch (in the next iteration). + self.change_next_block(orelse, from, to, check_condition); + None + }; + + let (next, orelse) = match (new_next, new_orelse) { + (Some(new_next), Some(new_orelse)) => (new_next, new_orelse), + (Some(new_next), None) => (new_next, orelse), + (None, Some(new_orelse)) => (next, new_orelse), + (None, None) => continue, // Not changing anything. + }; + + self.blocks[idx].next = NextBlock::If { + condition, + next, + orelse, + }; + } + Some(NextBlock::Terminate) | None => return, + } + } + } + + fn finish(mut self) -> BasicBlocks<'stmt> { + if self.blocks.is_empty() { + self.blocks.push(BasicBlock::EMPTY); + } + + BasicBlocks { + blocks: self.blocks, + } + } +} + +impl<'stmt> std::ops::Deref for BasicBlocksBuilder<'stmt> { + type Target = IndexSlice>; + + fn deref(&self) -> &Self::Target { + &self.blocks + } +} + +impl<'stmt> std::ops::DerefMut for BasicBlocksBuilder<'stmt> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.blocks + } +} + +/// Returns true if `stmts` need a next block, false otherwise. +fn needs_next_block(stmts: &[Stmt]) -> bool { + // No statements, we automatically continue with the next block. + let Some(last) = stmts.last() else { + return true; + }; + + match last { + Stmt::Return(_) | Stmt::Raise(_) => false, + Stmt::If(stmt) => needs_next_block(&stmt.body) || needs_next_block(&stmt.orelse), + Stmt::FunctionDef(_) + | Stmt::AsyncFunctionDef(_) + | Stmt::Import(_) + | Stmt::ImportFrom(_) + | Stmt::ClassDef(_) + | Stmt::Global(_) + | Stmt::Nonlocal(_) + | Stmt::Delete(_) + | Stmt::Assign(_) + | Stmt::AugAssign(_) + | Stmt::AnnAssign(_) + | Stmt::Expr(_) + | Stmt::Pass(_) + // TODO: check below. + | Stmt::Break(_) + | Stmt::Continue(_) + | Stmt::For(_) + | Stmt::AsyncFor(_) + | Stmt::While(_) + | Stmt::With(_) + | Stmt::AsyncWith(_) + | Stmt::Match(_) + | Stmt::Try(_) + | Stmt::TryStar(_) + | Stmt::Assert(_) => true, + } +} + +/// Returns true if `stmt` contains a control flow statement, e.g. an `if` or +/// `return` statement. +fn is_control_flow_stmt(stmt: &Stmt) -> bool { + match stmt { + Stmt::FunctionDef(_) + | Stmt::AsyncFunctionDef(_) + | Stmt::Import(_) + | Stmt::ImportFrom(_) + | Stmt::ClassDef(_) + | Stmt::Global(_) + | Stmt::Nonlocal(_) + | Stmt::Delete(_) + | Stmt::Assign(_) + | Stmt::AugAssign(_) + | Stmt::AnnAssign(_) + | Stmt::Expr(_) + | Stmt::Pass(_) => false, + Stmt::Return(_) + | Stmt::For(_) + | Stmt::AsyncFor(_) + | Stmt::While(_) + | Stmt::If(_) + | Stmt::With(_) + | Stmt::AsyncWith(_) + | Stmt::Match(_) + | Stmt::Raise(_) + | Stmt::Try(_) + | Stmt::TryStar(_) + | Stmt::Assert(_) + | Stmt::Break(_) + | Stmt::Continue(_) => true, + } +} + +/// Type to create a Mermaid graph. +/// +/// To learn amount Mermaid see , for the syntax +/// see . +struct MermaidGraph<'stmt, 'source> { + graph: &'stmt BasicBlocks<'stmt>, + source: &'source str, +} + +impl<'stmt, 'source> fmt::Display for MermaidGraph<'stmt, 'source> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // Flowchart type of graph, top down. + writeln!(f, "flowchart TD")?; + + // List all blocks. + writeln!(f, " start((\"Start\"))")?; + writeln!(f, " return((\"End\"))")?; + for (i, block) in self.graph.blocks.iter().enumerate() { + let (open, close) = if block.is_sentinel() { + ("[[", "]]") + } else { + ("[", "]") + }; + write!(f, " block{i}{open}\"")?; + if block.is_empty() { + write!(f, "`*(empty)*`")?; + } else if block.is_exception() { + write!(f, "Exception raised")?; + } else { + for stmt in block.stmts { + let code_line = &self.source[stmt.range()].trim(); + mermaid_write_quoted_str(f, code_line)?; + write!(f, "\\n")?; + } + } + writeln!(f, "\"{close}")?; + } + writeln!(f)?; + + // Then link all the blocks. + writeln!(f, " start --> block{}", self.graph.blocks.len() - 1)?; + for (i, block) in self.graph.blocks.iter_enumerated().rev() { + let i = i.as_u32(); + match &block.next { + NextBlock::Always(target) => { + writeln!(f, " block{i} --> block{target}", target = target.as_u32())?; + } + NextBlock::If { + condition, + next, + orelse, + } => { + let condition_code = &self.source[condition.range()].trim(); + writeln!( + f, + " block{i} -- \"{condition_code}\" --> block{next}", + next = next.as_u32() + )?; + writeln!( + f, + " block{i} -- \"else\" --> block{orelse}", + orelse = orelse.as_u32() + )?; + } + NextBlock::Terminate => writeln!(f, " block{i} --> return")?, + } + } + + Ok(()) + } +} + +/// Escape double quotes (`"`) in `value` using `#quot;`. +fn mermaid_write_quoted_str(f: &mut fmt::Formatter<'_>, value: &str) -> fmt::Result { + let mut parts = value.split('"'); + if let Some(v) = parts.next() { + write!(f, "{v}")?; + } + for v in parts { + write!(f, "#quot;{v}")?; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::fs; + use std::path::PathBuf; + + use rustpython_parser::ast::Ranged; + use rustpython_parser::{parse, Mode}; + use std::fmt::Write; + use test_case::test_case; + + use crate::rules::ruff::rules::unreachable::{ + BasicBlocks, BlockIndex, MermaidGraph, NextBlock, + }; + + #[test_case("simple.py")] + #[test_case("if.py")] + #[test_case("while.py")] + #[test_case("for.py")] + #[test_case("async-for.py")] + //#[test_case("try.py")] // TODO. + #[test_case("raise.py")] + #[test_case("assert.py")] + #[test_case("match.py")] + fn control_flow_graph(filename: &str) { + let path = PathBuf::from_iter(["resources/test/fixtures/control-flow-graph", filename]); + let source = fs::read_to_string(&path).expect("failed to read file"); + let stmts = parse(&source, Mode::Module, filename) + .unwrap_or_else(|err| panic!("failed to parse source: '{source}': {err}")) + .expect_module() + .body; + + let mut output = String::new(); + + for (i, stmts) in stmts.into_iter().enumerate() { + let Some(func) = stmts.function_def_stmt() else { + use std::io::Write; + let _ = std::io::stderr().write_all(b"unexpected statement kind, ignoring"); + continue; + }; + + let got = BasicBlocks::from(&*func.body); + // Basic sanity checks. + assert!(!got.blocks.is_empty(), "basic blocks should never be empty"); + assert_eq!( + got.blocks.first().unwrap().next, + NextBlock::Terminate, + "first block should always terminate" + ); + + // All block index should be valid. + let valid = BlockIndex::from_usize(got.blocks.len()); + for block in &got.blocks { + match block.next { + NextBlock::Always(index) => assert!(index < valid, "invalid block index"), + NextBlock::If { next, orelse, .. } => { + assert!(next < valid, "invalid next block index"); + assert!(orelse < valid, "invalid orelse block index"); + } + NextBlock::Terminate => {} + } + } + + let got_mermaid = MermaidGraph { + graph: &got, + source: &source, + }; + + writeln!( + output, + "## Function {i}\n### Source\n```python\n{}\n```\n\n### Control Flow Graph\n```mermaid\n{}```\n", + &source[func.range()], + got_mermaid + ) + .unwrap(); + } + + insta::with_settings!({ + omit_expression => true, + input_file => filename, + description => "This is a Mermaid graph. You can use https://mermaid.live to visualize it as a diagram." + }, { + insta::assert_snapshot!(format!("{filename}.md"), output); + }); + } +} diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__PY39_RUF013_RUF013_0.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__PY39_RUF013_RUF013_0.py.snap new file mode 100644 index 0000000000..7d4b9a9f00 --- /dev/null +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__PY39_RUF013_RUF013_0.py.snap @@ -0,0 +1,344 @@ +--- +source: crates/ruff/src/rules/ruff/mod.rs +--- +RUF013_0.py:21:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +21 | def f(arg: int = None): # RUF013 + | ^^^ RUF013 +22 | pass + | + = help: Convert to `Optional[T]` + +ℹ Suggested fix +18 18 | pass +19 19 | +20 20 | +21 |-def f(arg: int = None): # RUF013 + 21 |+def f(arg: Optional[int] = None): # RUF013 +22 22 | pass +23 23 | +24 24 | + +RUF013_0.py:25:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +25 | def f(arg: str = None): # RUF013 + | ^^^ RUF013 +26 | pass + | + = help: Convert to `Optional[T]` + +ℹ Suggested fix +22 22 | pass +23 23 | +24 24 | +25 |-def f(arg: str = None): # RUF013 + 25 |+def f(arg: Optional[str] = None): # RUF013 +26 26 | pass +27 27 | +28 28 | + +RUF013_0.py:67:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +67 | def f(arg: Union = None): # RUF013 + | ^^^^^ RUF013 +68 | pass + | + = help: Convert to `Optional[T]` + +ℹ Suggested fix +64 64 | pass +65 65 | +66 66 | +67 |-def f(arg: Union = None): # RUF013 + 67 |+def f(arg: Optional[Union] = None): # RUF013 +68 68 | pass +69 69 | +70 70 | + +RUF013_0.py:71:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +71 | def f(arg: Union[int, str] = None): # RUF013 + | ^^^^^^^^^^^^^^^ RUF013 +72 | pass + | + = help: Convert to `Optional[T]` + +ℹ Suggested fix +68 68 | pass +69 69 | +70 70 | +71 |-def f(arg: Union[int, str] = None): # RUF013 + 71 |+def f(arg: Optional[Union[int, str]] = None): # RUF013 +72 72 | pass +73 73 | +74 74 | + +RUF013_0.py:75:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +75 | def f(arg: typing.Union[int, str] = None): # RUF013 + | ^^^^^^^^^^^^^^^^^^^^^^ RUF013 +76 | pass + | + = help: Convert to `Optional[T]` + +ℹ Suggested fix +72 72 | pass +73 73 | +74 74 | +75 |-def f(arg: typing.Union[int, str] = None): # RUF013 + 75 |+def f(arg: Optional[typing.Union[int, str]] = None): # RUF013 +76 76 | pass +77 77 | +78 78 | + +RUF013_0.py:94:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +94 | def f(arg: int | float = None): # RUF013 + | ^^^^^^^^^^^ RUF013 +95 | pass + | + = help: Convert to `Optional[T]` + +ℹ Suggested fix +91 91 | pass +92 92 | +93 93 | +94 |-def f(arg: int | float = None): # RUF013 + 94 |+def f(arg: Optional[int | float] = None): # RUF013 +95 95 | pass +96 96 | +97 97 | + +RUF013_0.py:98:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +98 | def f(arg: int | float | str | bytes = None): # RUF013 + | ^^^^^^^^^^^^^^^^^^^^^^^^^ RUF013 +99 | pass + | + = help: Convert to `Optional[T]` + +ℹ Suggested fix +95 95 | pass +96 96 | +97 97 | +98 |-def f(arg: int | float | str | bytes = None): # RUF013 + 98 |+def f(arg: Optional[int | float | str | bytes] = None): # RUF013 +99 99 | pass +100 100 | +101 101 | + +RUF013_0.py:113:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +113 | def f(arg: Literal[1, "foo"] = None): # RUF013 + | ^^^^^^^^^^^^^^^^^ RUF013 +114 | pass + | + = help: Convert to `Optional[T]` + +ℹ Suggested fix +110 110 | pass +111 111 | +112 112 | +113 |-def f(arg: Literal[1, "foo"] = None): # RUF013 + 113 |+def f(arg: Optional[Literal[1, "foo"]] = None): # RUF013 +114 114 | pass +115 115 | +116 116 | + +RUF013_0.py:117:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +117 | def f(arg: typing.Literal[1, "foo", True] = None): # RUF013 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF013 +118 | pass + | + = help: Convert to `Optional[T]` + +ℹ Suggested fix +114 114 | pass +115 115 | +116 116 | +117 |-def f(arg: typing.Literal[1, "foo", True] = None): # RUF013 + 117 |+def f(arg: Optional[typing.Literal[1, "foo", True]] = None): # RUF013 +118 118 | pass +119 119 | +120 120 | + +RUF013_0.py:136:22: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +136 | def f(arg: Annotated[int, ...] = None): # RUF013 + | ^^^ RUF013 +137 | pass + | + = help: Convert to `Optional[T]` + +ℹ Suggested fix +133 133 | pass +134 134 | +135 135 | +136 |-def f(arg: Annotated[int, ...] = None): # RUF013 + 136 |+def f(arg: Annotated[Optional[int], ...] = None): # RUF013 +137 137 | pass +138 138 | +139 139 | + +RUF013_0.py:140:32: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +140 | def f(arg: Annotated[Annotated[int | str, ...], ...] = None): # RUF013 + | ^^^^^^^^^ RUF013 +141 | pass + | + = help: Convert to `Optional[T]` + +ℹ Suggested fix +137 137 | pass +138 138 | +139 139 | +140 |-def f(arg: Annotated[Annotated[int | str, ...], ...] = None): # RUF013 + 140 |+def f(arg: Annotated[Annotated[Optional[int | str], ...], ...] = None): # RUF013 +141 141 | pass +142 142 | +143 143 | + +RUF013_0.py:156:11: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +155 | def f( +156 | arg1: int = None, # RUF013 + | ^^^ RUF013 +157 | arg2: Union[int, float] = None, # RUF013 +158 | arg3: Literal[1, 2, 3] = None, # RUF013 + | + = help: Convert to `Optional[T]` + +ℹ Suggested fix +153 153 | +154 154 | +155 155 | def f( +156 |- arg1: int = None, # RUF013 + 156 |+ arg1: Optional[int] = None, # RUF013 +157 157 | arg2: Union[int, float] = None, # RUF013 +158 158 | arg3: Literal[1, 2, 3] = None, # RUF013 +159 159 | ): + +RUF013_0.py:157:11: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +155 | def f( +156 | arg1: int = None, # RUF013 +157 | arg2: Union[int, float] = None, # RUF013 + | ^^^^^^^^^^^^^^^^^ RUF013 +158 | arg3: Literal[1, 2, 3] = None, # RUF013 +159 | ): + | + = help: Convert to `Optional[T]` + +ℹ Suggested fix +154 154 | +155 155 | def f( +156 156 | arg1: int = None, # RUF013 +157 |- arg2: Union[int, float] = None, # RUF013 + 157 |+ arg2: Optional[Union[int, float]] = None, # RUF013 +158 158 | arg3: Literal[1, 2, 3] = None, # RUF013 +159 159 | ): +160 160 | pass + +RUF013_0.py:158:11: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +156 | arg1: int = None, # RUF013 +157 | arg2: Union[int, float] = None, # RUF013 +158 | arg3: Literal[1, 2, 3] = None, # RUF013 + | ^^^^^^^^^^^^^^^^ RUF013 +159 | ): +160 | pass + | + = help: Convert to `Optional[T]` + +ℹ Suggested fix +155 155 | def f( +156 156 | arg1: int = None, # RUF013 +157 157 | arg2: Union[int, float] = None, # RUF013 +158 |- arg3: Literal[1, 2, 3] = None, # RUF013 + 158 |+ arg3: Optional[Literal[1, 2, 3]] = None, # RUF013 +159 159 | ): +160 160 | pass +161 161 | + +RUF013_0.py:186:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +186 | def f(arg: Union[Annotated[int, ...], Union[str, bytes]] = None): # RUF013 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF013 +187 | pass + | + = help: Convert to `Optional[T]` + +ℹ Suggested fix +183 183 | pass +184 184 | +185 185 | +186 |-def f(arg: Union[Annotated[int, ...], Union[str, bytes]] = None): # RUF013 + 186 |+def f(arg: Optional[Union[Annotated[int, ...], Union[str, bytes]]] = None): # RUF013 +187 187 | pass +188 188 | +189 189 | + +RUF013_0.py:193:13: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +193 | def f(arg: "int" = None): # RUF013 + | ^^^ RUF013 +194 | pass + | + = help: Convert to `Optional[T]` + +ℹ Suggested fix +190 190 | # Quoted +191 191 | +192 192 | +193 |-def f(arg: "int" = None): # RUF013 + 193 |+def f(arg: "Optional[int]" = None): # RUF013 +194 194 | pass +195 195 | +196 196 | + +RUF013_0.py:197:13: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +197 | def f(arg: "str" = None): # RUF013 + | ^^^ RUF013 +198 | pass + | + = help: Convert to `Optional[T]` + +ℹ Suggested fix +194 194 | pass +195 195 | +196 196 | +197 |-def f(arg: "str" = None): # RUF013 + 197 |+def f(arg: "Optional[str]" = None): # RUF013 +198 198 | pass +199 199 | +200 200 | + +RUF013_0.py:201:12: RUF013 PEP 484 prohibits implicit `Optional` + | +201 | def f(arg: "st" "r" = None): # RUF013 + | ^^^^^^^^ RUF013 +202 | pass + | + = help: Convert to `Optional[T]` + +RUF013_0.py:209:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +209 | def f(arg: Union["int", "str"] = None): # RUF013 + | ^^^^^^^^^^^^^^^^^^^ RUF013 +210 | pass + | + = help: Convert to `Optional[T]` + +ℹ Suggested fix +206 206 | pass +207 207 | +208 208 | +209 |-def f(arg: Union["int", "str"] = None): # RUF013 + 209 |+def f(arg: Optional[Union["int", "str"]] = None): # RUF013 +210 210 | pass +211 211 | +212 212 | + + diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__PY39_RUF013_RUF013_1.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__PY39_RUF013_RUF013_1.py.snap new file mode 100644 index 0000000000..ef30934200 --- /dev/null +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__PY39_RUF013_RUF013_1.py.snap @@ -0,0 +1,21 @@ +--- +source: crates/ruff/src/rules/ruff/mod.rs +--- +RUF013_1.py:4:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +4 | def f(arg: int = None): # RUF011 + | ^^^ RUF013 +5 | pass + | + = help: Convert to `Optional[T]` + +ℹ Suggested fix +1 1 | # No `typing.Optional` import + 2 |+from typing import Optional +2 3 | +3 4 | +4 |-def f(arg: int = None): # RUF011 + 5 |+def f(arg: Optional[int] = None): # RUF011 +5 6 | pass + + diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__ruff_pairwise_over_zipped.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF007_RUF007.py.snap similarity index 100% rename from crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__ruff_pairwise_over_zipped.snap rename to crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF007_RUF007.py.snap diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF008_RUF008.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF008_RUF008.py.snap index 36c054f145..e1d7054a57 100644 --- a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF008_RUF008.py.snap +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF008_RUF008.py.snap @@ -3,7 +3,7 @@ source: crates/ruff/src/rules/ruff/mod.rs --- RUF008.py:10:34: RUF008 Do not use mutable default values for dataclass attributes | - 8 | @dataclass() + 8 | @dataclass 9 | class A: 10 | mutable_default: list[int] = [] | ^^ RUF008 @@ -11,34 +11,14 @@ RUF008.py:10:34: RUF008 Do not use mutable default values for dataclass attribut 12 | without_annotation = [] | -RUF008.py:12:26: RUF008 Do not use mutable default values for dataclass attributes +RUF008.py:20:34: RUF008 Do not use mutable default values for dataclass attributes | -10 | mutable_default: list[int] = [] -11 | immutable_annotation: typing.Sequence[int] = [] -12 | without_annotation = [] - | ^^ RUF008 -13 | ignored_via_comment: list[int] = [] # noqa: RUF008 -14 | correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT - | - -RUF008.py:21:34: RUF008 Do not use mutable default values for dataclass attributes - | -19 | @dataclass -20 | class B: -21 | mutable_default: list[int] = [] +18 | @dataclass +19 | class B: +20 | mutable_default: list[int] = [] | ^^ RUF008 -22 | immutable_annotation: Sequence[int] = [] -23 | without_annotation = [] - | - -RUF008.py:23:26: RUF008 Do not use mutable default values for dataclass attributes - | -21 | mutable_default: list[int] = [] -22 | immutable_annotation: Sequence[int] = [] -23 | without_annotation = [] - | ^^ RUF008 -24 | ignored_via_comment: list[int] = [] # noqa: RUF008 -25 | correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT +21 | immutable_annotation: Sequence[int] = [] +22 | without_annotation = [] | diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF009_RUF009.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF009_RUF009.py.snap index ea6bb93008..64571bbe2d 100644 --- a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF009_RUF009.py.snap +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF009_RUF009.py.snap @@ -1,44 +1,44 @@ --- source: crates/ruff/src/rules/ruff/mod.rs --- -RUF009.py:19:41: RUF009 Do not perform function call `default_function` in dataclass defaults +RUF009.py:20:41: RUF009 Do not perform function call `default_function` in dataclass defaults | -17 | @dataclass() -18 | class A: -19 | hidden_mutable_default: list[int] = default_function() +18 | @dataclass() +19 | class A: +20 | hidden_mutable_default: list[int] = default_function() | ^^^^^^^^^^^^^^^^^^ RUF009 -20 | class_variable: typing.ClassVar[list[int]] = default_function() -21 | another_class_var: ClassVar[list[int]] = default_function() +21 | class_variable: typing.ClassVar[list[int]] = default_function() +22 | another_class_var: ClassVar[list[int]] = default_function() | -RUF009.py:41:41: RUF009 Do not perform function call `default_function` in dataclass defaults +RUF009.py:43:41: RUF009 Do not perform function call `default_function` in dataclass defaults | -39 | @dataclass -40 | class B: -41 | hidden_mutable_default: list[int] = default_function() +41 | @dataclass +42 | class B: +43 | hidden_mutable_default: list[int] = default_function() | ^^^^^^^^^^^^^^^^^^ RUF009 -42 | another_dataclass: A = A() -43 | not_optimal: ImmutableType = ImmutableType(20) +44 | another_dataclass: A = A() +45 | not_optimal: ImmutableType = ImmutableType(20) | -RUF009.py:42:28: RUF009 Do not perform function call `A` in dataclass defaults +RUF009.py:44:28: RUF009 Do not perform function call `A` in dataclass defaults | -40 | class B: -41 | hidden_mutable_default: list[int] = default_function() -42 | another_dataclass: A = A() +42 | class B: +43 | hidden_mutable_default: list[int] = default_function() +44 | another_dataclass: A = A() | ^^^ RUF009 -43 | not_optimal: ImmutableType = ImmutableType(20) -44 | good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES +45 | not_optimal: ImmutableType = ImmutableType(20) +46 | good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES | -RUF009.py:43:34: RUF009 Do not perform function call `ImmutableType` in dataclass defaults +RUF009.py:45:34: RUF009 Do not perform function call `ImmutableType` in dataclass defaults | -41 | hidden_mutable_default: list[int] = default_function() -42 | another_dataclass: A = A() -43 | not_optimal: ImmutableType = ImmutableType(20) +43 | hidden_mutable_default: list[int] = default_function() +44 | another_dataclass: A = A() +45 | not_optimal: ImmutableType = ImmutableType(20) | ^^^^^^^^^^^^^^^^^ RUF009 -44 | good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES -45 | okay_variant: A = DEFAULT_A_FOR_ALL_DATACLASSES +46 | good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES +47 | okay_variant: A = DEFAULT_A_FOR_ALL_DATACLASSES | diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF010_RUF010.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF010_RUF010.py.snap index 2f0133295c..da5f4232c5 100644 --- a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF010_RUF010.py.snap +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF010_RUF010.py.snap @@ -1,21 +1,21 @@ --- source: crates/ruff/src/rules/ruff/mod.rs --- -RUF010.py:9:4: RUF010 [*] Remove unnecessary `str` conversion +RUF010.py:9:4: RUF010 [*] Use explicit conversion flag | 9 | f"{str(bla)}, {repr(bla)}, {ascii(bla)}" # RUF010 | ^^^^^^^^ RUF010 10 | 11 | f"{str(d['a'])}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010 | - = help: Remove `str` call + = help: Replace with conversion flag ℹ Fix 6 6 | pass 7 7 | 8 8 | 9 |-f"{str(bla)}, {repr(bla)}, {ascii(bla)}" # RUF010 - 9 |+f"{bla}, {repr(bla)}, {ascii(bla)}" # RUF010 + 9 |+f"{bla!s}, {repr(bla)}, {ascii(bla)}" # RUF010 10 10 | 11 11 | f"{str(d['a'])}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010 12 12 | @@ -58,7 +58,7 @@ RUF010.py:9:29: RUF010 [*] Use explicit conversion flag 11 11 | f"{str(d['a'])}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010 12 12 | -RUF010.py:11:4: RUF010 [*] Remove unnecessary `str` conversion +RUF010.py:11:4: RUF010 [*] Use explicit conversion flag | 9 | f"{str(bla)}, {repr(bla)}, {ascii(bla)}" # RUF010 10 | @@ -67,14 +67,14 @@ RUF010.py:11:4: RUF010 [*] Remove unnecessary `str` conversion 12 | 13 | f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010 | - = help: Remove `str` call + = help: Replace with conversion flag ℹ Fix 8 8 | 9 9 | f"{str(bla)}, {repr(bla)}, {ascii(bla)}" # RUF010 10 10 | 11 |-f"{str(d['a'])}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010 - 11 |+f"{d['a']}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010 + 11 |+f"{d['a']!s}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010 12 12 | 13 13 | f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010 14 14 | @@ -121,7 +121,7 @@ RUF010.py:11:35: RUF010 [*] Use explicit conversion flag 13 13 | f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010 14 14 | -RUF010.py:13:5: RUF010 [*] Remove unnecessary `str` conversion +RUF010.py:13:5: RUF010 [*] Use explicit conversion flag | 11 | f"{str(d['a'])}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010 12 | @@ -130,14 +130,14 @@ RUF010.py:13:5: RUF010 [*] Remove unnecessary `str` conversion 14 | 15 | f"{bla!s}, {(repr(bla))}, {(ascii(bla))}" # RUF010 | - = help: Remove `str` call + = help: Replace with conversion flag ℹ Fix 10 10 | 11 11 | f"{str(d['a'])}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010 12 12 | 13 |-f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010 - 13 |+f"{bla}, {(repr(bla))}, {(ascii(bla))}" # RUF010 + 13 |+f"{bla!s}, {(repr(bla))}, {(ascii(bla))}" # RUF010 14 14 | 15 15 | f"{bla!s}, {(repr(bla))}, {(ascii(bla))}" # RUF010 16 16 | @@ -184,27 +184,6 @@ RUF010.py:13:34: RUF010 [*] Use explicit conversion flag 15 15 | f"{bla!s}, {(repr(bla))}, {(ascii(bla))}" # RUF010 16 16 | -RUF010.py:15:4: RUF010 [*] Remove unnecessary conversion flag - | -13 | f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010 -14 | -15 | f"{bla!s}, {(repr(bla))}, {(ascii(bla))}" # RUF010 - | ^^^ RUF010 -16 | -17 | f"{foo(bla)}" # OK - | - = help: Remove conversion flag - -ℹ Fix -12 12 | -13 13 | f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010 -14 14 | -15 |-f"{bla!s}, {(repr(bla))}, {(ascii(bla))}" # RUF010 - 15 |+f"{bla}, {(repr(bla))}, {(ascii(bla))}" # RUF010 -16 16 | -17 17 | f"{foo(bla)}" # OK -18 18 | - RUF010.py:15:14: RUF010 [*] Use explicit conversion flag | 13 | f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010 @@ -247,27 +226,6 @@ RUF010.py:15:29: RUF010 [*] Use explicit conversion flag 17 17 | f"{foo(bla)}" # OK 18 18 | -RUF010.py:21:4: RUF010 [*] Remove unnecessary conversion flag - | -19 | f"{str(bla, 'ascii')}, {str(bla, encoding='cp1255')}" # OK -20 | -21 | f"{bla!s} {[]!r} {'bar'!a}" # OK - | ^^^ RUF010 -22 | -23 | "Not an f-string {str(bla)}, {repr(bla)}, {ascii(bla)}" # OK - | - = help: Remove conversion flag - -ℹ Fix -18 18 | -19 19 | f"{str(bla, 'ascii')}, {str(bla, encoding='cp1255')}" # OK -20 20 | -21 |-f"{bla!s} {[]!r} {'bar'!a}" # OK - 21 |+f"{bla} {[]!r} {'bar'!a}" # OK -22 22 | -23 23 | "Not an f-string {str(bla)}, {repr(bla)}, {ascii(bla)}" # OK -24 24 | - RUF010.py:35:20: RUF010 [*] Use explicit conversion flag | 33 | f"Member of tuple mismatches type at index {i}. Expected {of_shape_i}. Got " @@ -288,64 +246,4 @@ RUF010.py:35:20: RUF010 [*] Use explicit conversion flag 37 37 | 38 38 | -RUF010.py:39:4: RUF010 [*] Remove unnecessary `str` conversion - | -39 | f"{str(bla)}" # RUF010 - | ^^^^^^^^ RUF010 -40 | -41 | f"{str(bla):20}" # RUF010 - | - = help: Remove `str` call - -ℹ Fix -36 36 | ) -37 37 | -38 38 | -39 |-f"{str(bla)}" # RUF010 - 39 |+f"{bla}" # RUF010 -40 40 | -41 41 | f"{str(bla):20}" # RUF010 -42 42 | - -RUF010.py:41:4: RUF010 [*] Use explicit conversion flag - | -39 | f"{str(bla)}" # RUF010 -40 | -41 | f"{str(bla):20}" # RUF010 - | ^^^^^^^^ RUF010 -42 | -43 | f"{bla!s}" # RUF010 - | - = help: Replace with conversion flag - -ℹ Fix -38 38 | -39 39 | f"{str(bla)}" # RUF010 -40 40 | -41 |-f"{str(bla):20}" # RUF010 - 41 |+f"{bla!s:20}" # RUF010 -42 42 | -43 43 | f"{bla!s}" # RUF010 -44 44 | - -RUF010.py:43:4: RUF010 [*] Remove unnecessary conversion flag - | -41 | f"{str(bla):20}" # RUF010 -42 | -43 | f"{bla!s}" # RUF010 - | ^^^ RUF010 -44 | -45 | f"{bla!s:20}" # OK - | - = help: Remove conversion flag - -ℹ Fix -40 40 | -41 41 | f"{str(bla):20}" # RUF010 -42 42 | -43 |-f"{bla!s}" # RUF010 - 43 |+f"{bla}" # RUF010 -44 44 | -45 45 | f"{bla!s:20}" # OK - diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF012_RUF012.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF012_RUF012.py.snap new file mode 100644 index 0000000000..676e2a1a03 --- /dev/null +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF012_RUF012.py.snap @@ -0,0 +1,34 @@ +--- +source: crates/ruff/src/rules/ruff/mod.rs +--- +RUF012.py:9:34: RUF012 Mutable class attributes should be annotated with `typing.ClassVar` + | + 7 | } + 8 | + 9 | mutable_default: list[int] = [] + | ^^ RUF012 +10 | immutable_annotation: Sequence[int] = [] +11 | without_annotation = [] + | + +RUF012.py:11:26: RUF012 Mutable class attributes should be annotated with `typing.ClassVar` + | + 9 | mutable_default: list[int] = [] +10 | immutable_annotation: Sequence[int] = [] +11 | without_annotation = [] + | ^^ RUF012 +12 | class_variable: ClassVar[list[int]] = [] +13 | final_variable: Final[list[int]] = [] + | + +RUF012.py:23:26: RUF012 Mutable class attributes should be annotated with `typing.ClassVar` + | +21 | mutable_default: list[int] = [] +22 | immutable_annotation: Sequence[int] = [] +23 | without_annotation = [] + | ^^ RUF012 +24 | perfectly_fine: list[int] = field(default_factory=list) +25 | class_variable: ClassVar[list[int]] = [] + | + + diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF013_RUF013_0.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF013_RUF013_0.py.snap new file mode 100644 index 0000000000..341aecce5e --- /dev/null +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF013_RUF013_0.py.snap @@ -0,0 +1,344 @@ +--- +source: crates/ruff/src/rules/ruff/mod.rs +--- +RUF013_0.py:21:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +21 | def f(arg: int = None): # RUF013 + | ^^^ RUF013 +22 | pass + | + = help: Convert to `T | None` + +ℹ Suggested fix +18 18 | pass +19 19 | +20 20 | +21 |-def f(arg: int = None): # RUF013 + 21 |+def f(arg: int | None = None): # RUF013 +22 22 | pass +23 23 | +24 24 | + +RUF013_0.py:25:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +25 | def f(arg: str = None): # RUF013 + | ^^^ RUF013 +26 | pass + | + = help: Convert to `T | None` + +ℹ Suggested fix +22 22 | pass +23 23 | +24 24 | +25 |-def f(arg: str = None): # RUF013 + 25 |+def f(arg: str | None = None): # RUF013 +26 26 | pass +27 27 | +28 28 | + +RUF013_0.py:67:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +67 | def f(arg: Union = None): # RUF013 + | ^^^^^ RUF013 +68 | pass + | + = help: Convert to `T | None` + +ℹ Suggested fix +64 64 | pass +65 65 | +66 66 | +67 |-def f(arg: Union = None): # RUF013 + 67 |+def f(arg: Union | None = None): # RUF013 +68 68 | pass +69 69 | +70 70 | + +RUF013_0.py:71:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +71 | def f(arg: Union[int, str] = None): # RUF013 + | ^^^^^^^^^^^^^^^ RUF013 +72 | pass + | + = help: Convert to `T | None` + +ℹ Suggested fix +68 68 | pass +69 69 | +70 70 | +71 |-def f(arg: Union[int, str] = None): # RUF013 + 71 |+def f(arg: Union[int, str] | None = None): # RUF013 +72 72 | pass +73 73 | +74 74 | + +RUF013_0.py:75:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +75 | def f(arg: typing.Union[int, str] = None): # RUF013 + | ^^^^^^^^^^^^^^^^^^^^^^ RUF013 +76 | pass + | + = help: Convert to `T | None` + +ℹ Suggested fix +72 72 | pass +73 73 | +74 74 | +75 |-def f(arg: typing.Union[int, str] = None): # RUF013 + 75 |+def f(arg: typing.Union[int, str] | None = None): # RUF013 +76 76 | pass +77 77 | +78 78 | + +RUF013_0.py:94:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +94 | def f(arg: int | float = None): # RUF013 + | ^^^^^^^^^^^ RUF013 +95 | pass + | + = help: Convert to `T | None` + +ℹ Suggested fix +91 91 | pass +92 92 | +93 93 | +94 |-def f(arg: int | float = None): # RUF013 + 94 |+def f(arg: int | float | None = None): # RUF013 +95 95 | pass +96 96 | +97 97 | + +RUF013_0.py:98:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +98 | def f(arg: int | float | str | bytes = None): # RUF013 + | ^^^^^^^^^^^^^^^^^^^^^^^^^ RUF013 +99 | pass + | + = help: Convert to `T | None` + +ℹ Suggested fix +95 95 | pass +96 96 | +97 97 | +98 |-def f(arg: int | float | str | bytes = None): # RUF013 + 98 |+def f(arg: int | float | str | bytes | None = None): # RUF013 +99 99 | pass +100 100 | +101 101 | + +RUF013_0.py:113:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +113 | def f(arg: Literal[1, "foo"] = None): # RUF013 + | ^^^^^^^^^^^^^^^^^ RUF013 +114 | pass + | + = help: Convert to `T | None` + +ℹ Suggested fix +110 110 | pass +111 111 | +112 112 | +113 |-def f(arg: Literal[1, "foo"] = None): # RUF013 + 113 |+def f(arg: Literal[1, "foo"] | None = None): # RUF013 +114 114 | pass +115 115 | +116 116 | + +RUF013_0.py:117:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +117 | def f(arg: typing.Literal[1, "foo", True] = None): # RUF013 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF013 +118 | pass + | + = help: Convert to `T | None` + +ℹ Suggested fix +114 114 | pass +115 115 | +116 116 | +117 |-def f(arg: typing.Literal[1, "foo", True] = None): # RUF013 + 117 |+def f(arg: typing.Literal[1, "foo", True] | None = None): # RUF013 +118 118 | pass +119 119 | +120 120 | + +RUF013_0.py:136:22: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +136 | def f(arg: Annotated[int, ...] = None): # RUF013 + | ^^^ RUF013 +137 | pass + | + = help: Convert to `T | None` + +ℹ Suggested fix +133 133 | pass +134 134 | +135 135 | +136 |-def f(arg: Annotated[int, ...] = None): # RUF013 + 136 |+def f(arg: Annotated[int | None, ...] = None): # RUF013 +137 137 | pass +138 138 | +139 139 | + +RUF013_0.py:140:32: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +140 | def f(arg: Annotated[Annotated[int | str, ...], ...] = None): # RUF013 + | ^^^^^^^^^ RUF013 +141 | pass + | + = help: Convert to `T | None` + +ℹ Suggested fix +137 137 | pass +138 138 | +139 139 | +140 |-def f(arg: Annotated[Annotated[int | str, ...], ...] = None): # RUF013 + 140 |+def f(arg: Annotated[Annotated[int | str | None, ...], ...] = None): # RUF013 +141 141 | pass +142 142 | +143 143 | + +RUF013_0.py:156:11: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +155 | def f( +156 | arg1: int = None, # RUF013 + | ^^^ RUF013 +157 | arg2: Union[int, float] = None, # RUF013 +158 | arg3: Literal[1, 2, 3] = None, # RUF013 + | + = help: Convert to `T | None` + +ℹ Suggested fix +153 153 | +154 154 | +155 155 | def f( +156 |- arg1: int = None, # RUF013 + 156 |+ arg1: int | None = None, # RUF013 +157 157 | arg2: Union[int, float] = None, # RUF013 +158 158 | arg3: Literal[1, 2, 3] = None, # RUF013 +159 159 | ): + +RUF013_0.py:157:11: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +155 | def f( +156 | arg1: int = None, # RUF013 +157 | arg2: Union[int, float] = None, # RUF013 + | ^^^^^^^^^^^^^^^^^ RUF013 +158 | arg3: Literal[1, 2, 3] = None, # RUF013 +159 | ): + | + = help: Convert to `T | None` + +ℹ Suggested fix +154 154 | +155 155 | def f( +156 156 | arg1: int = None, # RUF013 +157 |- arg2: Union[int, float] = None, # RUF013 + 157 |+ arg2: Union[int, float] | None = None, # RUF013 +158 158 | arg3: Literal[1, 2, 3] = None, # RUF013 +159 159 | ): +160 160 | pass + +RUF013_0.py:158:11: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +156 | arg1: int = None, # RUF013 +157 | arg2: Union[int, float] = None, # RUF013 +158 | arg3: Literal[1, 2, 3] = None, # RUF013 + | ^^^^^^^^^^^^^^^^ RUF013 +159 | ): +160 | pass + | + = help: Convert to `T | None` + +ℹ Suggested fix +155 155 | def f( +156 156 | arg1: int = None, # RUF013 +157 157 | arg2: Union[int, float] = None, # RUF013 +158 |- arg3: Literal[1, 2, 3] = None, # RUF013 + 158 |+ arg3: Literal[1, 2, 3] | None = None, # RUF013 +159 159 | ): +160 160 | pass +161 161 | + +RUF013_0.py:186:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +186 | def f(arg: Union[Annotated[int, ...], Union[str, bytes]] = None): # RUF013 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF013 +187 | pass + | + = help: Convert to `T | None` + +ℹ Suggested fix +183 183 | pass +184 184 | +185 185 | +186 |-def f(arg: Union[Annotated[int, ...], Union[str, bytes]] = None): # RUF013 + 186 |+def f(arg: Union[Annotated[int, ...], Union[str, bytes]] | None = None): # RUF013 +187 187 | pass +188 188 | +189 189 | + +RUF013_0.py:193:13: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +193 | def f(arg: "int" = None): # RUF013 + | ^^^ RUF013 +194 | pass + | + = help: Convert to `T | None` + +ℹ Suggested fix +190 190 | # Quoted +191 191 | +192 192 | +193 |-def f(arg: "int" = None): # RUF013 + 193 |+def f(arg: "int | None" = None): # RUF013 +194 194 | pass +195 195 | +196 196 | + +RUF013_0.py:197:13: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +197 | def f(arg: "str" = None): # RUF013 + | ^^^ RUF013 +198 | pass + | + = help: Convert to `T | None` + +ℹ Suggested fix +194 194 | pass +195 195 | +196 196 | +197 |-def f(arg: "str" = None): # RUF013 + 197 |+def f(arg: "str | None" = None): # RUF013 +198 198 | pass +199 199 | +200 200 | + +RUF013_0.py:201:12: RUF013 PEP 484 prohibits implicit `Optional` + | +201 | def f(arg: "st" "r" = None): # RUF013 + | ^^^^^^^^ RUF013 +202 | pass + | + = help: Convert to `T | None` + +RUF013_0.py:209:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +209 | def f(arg: Union["int", "str"] = None): # RUF013 + | ^^^^^^^^^^^^^^^^^^^ RUF013 +210 | pass + | + = help: Convert to `T | None` + +ℹ Suggested fix +206 206 | pass +207 207 | +208 208 | +209 |-def f(arg: Union["int", "str"] = None): # RUF013 + 209 |+def f(arg: Union["int", "str"] | None = None): # RUF013 +210 210 | pass +211 211 | +212 212 | + + diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF013_RUF013_1.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF013_RUF013_1.py.snap new file mode 100644 index 0000000000..65568b66a3 --- /dev/null +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF013_RUF013_1.py.snap @@ -0,0 +1,20 @@ +--- +source: crates/ruff/src/rules/ruff/mod.rs +--- +RUF013_1.py:4:12: RUF013 [*] PEP 484 prohibits implicit `Optional` + | +4 | def f(arg: int = None): # RUF011 + | ^^^ RUF013 +5 | pass + | + = help: Convert to `T | None` + +ℹ Suggested fix +1 1 | # No `typing.Optional` import +2 2 | +3 3 | +4 |-def f(arg: int = None): # RUF011 + 4 |+def f(arg: int | None = None): # RUF011 +5 5 | pass + + diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF014_RUF014.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF014_RUF014.py.snap new file mode 100644 index 0000000000..f2457017e3 --- /dev/null +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF014_RUF014.py.snap @@ -0,0 +1,249 @@ +--- +source: crates/ruff/src/rules/ruff/mod.rs +--- +RUF014.py:3:5: RUF014 Unreachable code in after_return + | +1 | def after_return(): +2 | return "reachable" +3 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +4 | +5 | async def also_works_on_async_functions(): + | + +RUF014.py:7:5: RUF014 Unreachable code in also_works_on_async_functions + | +5 | async def also_works_on_async_functions(): +6 | return "reachable" +7 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +8 | +9 | def if_always_true(): + | + +RUF014.py:12:5: RUF014 Unreachable code in if_always_true + | +10 | if True: +11 | return "reachable" +12 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +13 | +14 | def if_always_false(): + | + +RUF014.py:16:9: RUF014 Unreachable code in if_always_false + | +14 | def if_always_false(): +15 | if False: +16 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +17 | return "reachable" + | + +RUF014.py:21:9: RUF014 Unreachable code in if_elif_always_false + | +19 | def if_elif_always_false(): +20 | if False: +21 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +22 | elif False: +23 | return "also unreachable" + | + +RUF014.py:23:9: RUF014 Unreachable code in if_elif_always_false + | +21 | return "unreachable" +22 | elif False: +23 | return "also unreachable" + | ^^^^^^^^^^^^^^^^^^^^^^^^^ RUF014 +24 | return "reachable" + | + +RUF014.py:28:9: RUF014 Unreachable code in if_elif_always_true + | +26 | def if_elif_always_true(): +27 | if False: +28 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +29 | elif True: +30 | return "reachable" + | + +RUF014.py:31:5: RUF014 Unreachable code in if_elif_always_true + | +29 | elif True: +30 | return "reachable" +31 | return "also unreachable" + | ^^^^^^^^^^^^^^^^^^^^^^^^^ RUF014 +32 | +33 | def ends_with_if(): + | + +RUF014.py:35:9: RUF014 Unreachable code in ends_with_if + | +33 | def ends_with_if(): +34 | if False: +35 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +36 | else: +37 | return "reachable" + | + +RUF014.py:42:5: RUF014 Unreachable code in infinite_loop + | +40 | while True: +41 | continue +42 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +43 | +44 | ''' TODO: we could determine these, but we don't yet. + | + +RUF014.py:75:5: RUF014 Unreachable code in match_wildcard + | +73 | case _: +74 | return "reachable" +75 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +76 | +77 | def match_case_and_wildcard(status): + | + +RUF014.py:83:5: RUF014 Unreachable code in match_case_and_wildcard + | +81 | case _: +82 | return "reachable" +83 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +84 | +85 | def raise_exception(): + | + +RUF014.py:87:5: RUF014 Unreachable code in raise_exception + | +85 | def raise_exception(): +86 | raise Exception +87 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +88 | +89 | def while_false(): + | + +RUF014.py:91:9: RUF014 Unreachable code in while_false + | +89 | def while_false(): +90 | while False: +91 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +92 | return "reachable" + | + +RUF014.py:96:9: RUF014 Unreachable code in while_false_else + | +94 | def while_false_else(): +95 | while False: +96 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +97 | else: +98 | return "reachable" + | + +RUF014.py:102:9: RUF014 Unreachable code in while_false_else_return + | +100 | def while_false_else_return(): +101 | while False: +102 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +103 | else: +104 | return "reachable" + | + +RUF014.py:105:5: RUF014 Unreachable code in while_false_else_return + | +103 | else: +104 | return "reachable" +105 | return "also unreachable" + | ^^^^^^^^^^^^^^^^^^^^^^^^^ RUF014 +106 | +107 | def while_true(): + | + +RUF014.py:110:5: RUF014 Unreachable code in while_true + | +108 | while True: +109 | return "reachable" +110 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +111 | +112 | def while_true_else(): + | + +RUF014.py:116:9: RUF014 Unreachable code in while_true_else + | +114 | return "reachable" +115 | else: +116 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +117 | +118 | def while_true_else_return(): + | + +RUF014.py:122:9: RUF014 Unreachable code in while_true_else_return + | +120 | return "reachable" +121 | else: +122 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +123 | return "also unreachable" + | + +RUF014.py:123:5: RUF014 Unreachable code in while_true_else_return + | +121 | else: +122 | return "unreachable" +123 | return "also unreachable" + | ^^^^^^^^^^^^^^^^^^^^^^^^^ RUF014 +124 | +125 | def while_false_var_i(): + | + +RUF014.py:128:9: RUF014 Unreachable code in while_false_var_i + | +126 | i = 0 +127 | while False: +128 | i += 1 + | ^^^^^^ RUF014 +129 | return i + | + +RUF014.py:135:5: RUF014 Unreachable code in while_true_var_i + | +133 | while True: +134 | i += 1 +135 | return i + | ^^^^^^^^ RUF014 +136 | +137 | def while_infinite(): + | + +RUF014.py:140:5: RUF014 Unreachable code in while_infinite + | +138 | while True: +139 | pass +140 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +141 | +142 | def while_if_true(): + | + +RUF014.py:146:5: RUF014 Unreachable code in while_if_true + | +144 | if True: +145 | return "reachable" +146 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +147 | +148 | # Test case found in the Bokeh repository that trigger a false positive. + | + + diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF015_RUF015.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF015_RUF015.py.snap new file mode 100644 index 0000000000..4eb011f0fe --- /dev/null +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF015_RUF015.py.snap @@ -0,0 +1,338 @@ +--- +source: crates/ruff/src/rules/ruff/mod.rs +--- +RUF015.py:4:1: RUF015 [*] Prefer `next(iter(x))` over `list(x)[0]` + | +3 | # RUF015 +4 | list(x)[0] + | ^^^^^^^^^^ RUF015 +5 | list(x)[:1] +6 | list(x)[:1:1] + | + = help: Replace with `next(iter(x))` + +ℹ Suggested fix +1 1 | x = range(10) +2 2 | +3 3 | # RUF015 +4 |-list(x)[0] + 4 |+next(iter(x)) +5 5 | list(x)[:1] +6 6 | list(x)[:1:1] +7 7 | list(x)[:1:2] + +RUF015.py:5:1: RUF015 [*] Prefer `[next(iter(x))]` over `list(x)[:1]` + | +3 | # RUF015 +4 | list(x)[0] +5 | list(x)[:1] + | ^^^^^^^^^^^ RUF015 +6 | list(x)[:1:1] +7 | list(x)[:1:2] + | + = help: Replace with `[next(iter(x))] + +ℹ Suggested fix +2 2 | +3 3 | # RUF015 +4 4 | list(x)[0] +5 |-list(x)[:1] + 5 |+[next(iter(x))] +6 6 | list(x)[:1:1] +7 7 | list(x)[:1:2] +8 8 | tuple(x)[0] + +RUF015.py:6:1: RUF015 [*] Prefer `[next(iter(x))]` over `list(x)[:1]` + | +4 | list(x)[0] +5 | list(x)[:1] +6 | list(x)[:1:1] + | ^^^^^^^^^^^^^ RUF015 +7 | list(x)[:1:2] +8 | tuple(x)[0] + | + = help: Replace with `[next(iter(x))] + +ℹ Suggested fix +3 3 | # RUF015 +4 4 | list(x)[0] +5 5 | list(x)[:1] +6 |-list(x)[:1:1] + 6 |+[next(iter(x))] +7 7 | list(x)[:1:2] +8 8 | tuple(x)[0] +9 9 | tuple(x)[:1] + +RUF015.py:7:1: RUF015 [*] Prefer `[next(iter(x))]` over `list(x)[:1]` + | +5 | list(x)[:1] +6 | list(x)[:1:1] +7 | list(x)[:1:2] + | ^^^^^^^^^^^^^ RUF015 +8 | tuple(x)[0] +9 | tuple(x)[:1] + | + = help: Replace with `[next(iter(x))] + +ℹ Suggested fix +4 4 | list(x)[0] +5 5 | list(x)[:1] +6 6 | list(x)[:1:1] +7 |-list(x)[:1:2] + 7 |+[next(iter(x))] +8 8 | tuple(x)[0] +9 9 | tuple(x)[:1] +10 10 | tuple(x)[:1:1] + +RUF015.py:8:1: RUF015 [*] Prefer `next(iter(x))` over `list(x)[0]` + | + 6 | list(x)[:1:1] + 7 | list(x)[:1:2] + 8 | tuple(x)[0] + | ^^^^^^^^^^^ RUF015 + 9 | tuple(x)[:1] +10 | tuple(x)[:1:1] + | + = help: Replace with `next(iter(x))` + +ℹ Suggested fix +5 5 | list(x)[:1] +6 6 | list(x)[:1:1] +7 7 | list(x)[:1:2] +8 |-tuple(x)[0] + 8 |+next(iter(x)) +9 9 | tuple(x)[:1] +10 10 | tuple(x)[:1:1] +11 11 | tuple(x)[:1:2] + +RUF015.py:9:1: RUF015 [*] Prefer `[next(iter(x))]` over `list(x)[:1]` + | + 7 | list(x)[:1:2] + 8 | tuple(x)[0] + 9 | tuple(x)[:1] + | ^^^^^^^^^^^^ RUF015 +10 | tuple(x)[:1:1] +11 | tuple(x)[:1:2] + | + = help: Replace with `[next(iter(x))] + +ℹ Suggested fix +6 6 | list(x)[:1:1] +7 7 | list(x)[:1:2] +8 8 | tuple(x)[0] +9 |-tuple(x)[:1] + 9 |+[next(iter(x))] +10 10 | tuple(x)[:1:1] +11 11 | tuple(x)[:1:2] +12 12 | list(i for i in x)[0] + +RUF015.py:10:1: RUF015 [*] Prefer `[next(iter(x))]` over `list(x)[:1]` + | + 8 | tuple(x)[0] + 9 | tuple(x)[:1] +10 | tuple(x)[:1:1] + | ^^^^^^^^^^^^^^ RUF015 +11 | tuple(x)[:1:2] +12 | list(i for i in x)[0] + | + = help: Replace with `[next(iter(x))] + +ℹ Suggested fix +7 7 | list(x)[:1:2] +8 8 | tuple(x)[0] +9 9 | tuple(x)[:1] +10 |-tuple(x)[:1:1] + 10 |+[next(iter(x))] +11 11 | tuple(x)[:1:2] +12 12 | list(i for i in x)[0] +13 13 | list(i for i in x)[:1] + +RUF015.py:11:1: RUF015 [*] Prefer `[next(iter(x))]` over `list(x)[:1]` + | + 9 | tuple(x)[:1] +10 | tuple(x)[:1:1] +11 | tuple(x)[:1:2] + | ^^^^^^^^^^^^^^ RUF015 +12 | list(i for i in x)[0] +13 | list(i for i in x)[:1] + | + = help: Replace with `[next(iter(x))] + +ℹ Suggested fix +8 8 | tuple(x)[0] +9 9 | tuple(x)[:1] +10 10 | tuple(x)[:1:1] +11 |-tuple(x)[:1:2] + 11 |+[next(iter(x))] +12 12 | list(i for i in x)[0] +13 13 | list(i for i in x)[:1] +14 14 | list(i for i in x)[:1:1] + +RUF015.py:12:1: RUF015 [*] Prefer `next(iter(x))` over `list(x)[0]` + | +10 | tuple(x)[:1:1] +11 | tuple(x)[:1:2] +12 | list(i for i in x)[0] + | ^^^^^^^^^^^^^^^^^^^^^ RUF015 +13 | list(i for i in x)[:1] +14 | list(i for i in x)[:1:1] + | + = help: Replace with `next(iter(x))` + +ℹ Suggested fix +9 9 | tuple(x)[:1] +10 10 | tuple(x)[:1:1] +11 11 | tuple(x)[:1:2] +12 |-list(i for i in x)[0] + 12 |+next(iter(x)) +13 13 | list(i for i in x)[:1] +14 14 | list(i for i in x)[:1:1] +15 15 | list(i for i in x)[:1:2] + +RUF015.py:13:1: RUF015 [*] Prefer `[next(iter(x))]` over `list(x)[:1]` + | +11 | tuple(x)[:1:2] +12 | list(i for i in x)[0] +13 | list(i for i in x)[:1] + | ^^^^^^^^^^^^^^^^^^^^^^ RUF015 +14 | list(i for i in x)[:1:1] +15 | list(i for i in x)[:1:2] + | + = help: Replace with `[next(iter(x))] + +ℹ Suggested fix +10 10 | tuple(x)[:1:1] +11 11 | tuple(x)[:1:2] +12 12 | list(i for i in x)[0] +13 |-list(i for i in x)[:1] + 13 |+[next(iter(x))] +14 14 | list(i for i in x)[:1:1] +15 15 | list(i for i in x)[:1:2] +16 16 | [i for i in x][0] + +RUF015.py:14:1: RUF015 [*] Prefer `[next(iter(x))]` over `list(x)[:1]` + | +12 | list(i for i in x)[0] +13 | list(i for i in x)[:1] +14 | list(i for i in x)[:1:1] + | ^^^^^^^^^^^^^^^^^^^^^^^^ RUF015 +15 | list(i for i in x)[:1:2] +16 | [i for i in x][0] + | + = help: Replace with `[next(iter(x))] + +ℹ Suggested fix +11 11 | tuple(x)[:1:2] +12 12 | list(i for i in x)[0] +13 13 | list(i for i in x)[:1] +14 |-list(i for i in x)[:1:1] + 14 |+[next(iter(x))] +15 15 | list(i for i in x)[:1:2] +16 16 | [i for i in x][0] +17 17 | [i for i in x][:1] + +RUF015.py:15:1: RUF015 [*] Prefer `[next(iter(x))]` over `list(x)[:1]` + | +13 | list(i for i in x)[:1] +14 | list(i for i in x)[:1:1] +15 | list(i for i in x)[:1:2] + | ^^^^^^^^^^^^^^^^^^^^^^^^ RUF015 +16 | [i for i in x][0] +17 | [i for i in x][:1] + | + = help: Replace with `[next(iter(x))] + +ℹ Suggested fix +12 12 | list(i for i in x)[0] +13 13 | list(i for i in x)[:1] +14 14 | list(i for i in x)[:1:1] +15 |-list(i for i in x)[:1:2] + 15 |+[next(iter(x))] +16 16 | [i for i in x][0] +17 17 | [i for i in x][:1] +18 18 | [i for i in x][:1:1] + +RUF015.py:16:1: RUF015 [*] Prefer `next(iter(x))` over `list(x)[0]` + | +14 | list(i for i in x)[:1:1] +15 | list(i for i in x)[:1:2] +16 | [i for i in x][0] + | ^^^^^^^^^^^^^^^^^ RUF015 +17 | [i for i in x][:1] +18 | [i for i in x][:1:1] + | + = help: Replace with `next(iter(x))` + +ℹ Suggested fix +13 13 | list(i for i in x)[:1] +14 14 | list(i for i in x)[:1:1] +15 15 | list(i for i in x)[:1:2] +16 |-[i for i in x][0] + 16 |+next(iter(x)) +17 17 | [i for i in x][:1] +18 18 | [i for i in x][:1:1] +19 19 | [i for i in x][:1:2] + +RUF015.py:17:1: RUF015 [*] Prefer `[next(iter(x))]` over `list(x)[:1]` + | +15 | list(i for i in x)[:1:2] +16 | [i for i in x][0] +17 | [i for i in x][:1] + | ^^^^^^^^^^^^^^^^^^ RUF015 +18 | [i for i in x][:1:1] +19 | [i for i in x][:1:2] + | + = help: Replace with `[next(iter(x))] + +ℹ Suggested fix +14 14 | list(i for i in x)[:1:1] +15 15 | list(i for i in x)[:1:2] +16 16 | [i for i in x][0] +17 |-[i for i in x][:1] + 17 |+[next(iter(x))] +18 18 | [i for i in x][:1:1] +19 19 | [i for i in x][:1:2] +20 20 | + +RUF015.py:18:1: RUF015 [*] Prefer `[next(iter(x))]` over `list(x)[:1]` + | +16 | [i for i in x][0] +17 | [i for i in x][:1] +18 | [i for i in x][:1:1] + | ^^^^^^^^^^^^^^^^^^^^ RUF015 +19 | [i for i in x][:1:2] + | + = help: Replace with `[next(iter(x))] + +ℹ Suggested fix +15 15 | list(i for i in x)[:1:2] +16 16 | [i for i in x][0] +17 17 | [i for i in x][:1] +18 |-[i for i in x][:1:1] + 18 |+[next(iter(x))] +19 19 | [i for i in x][:1:2] +20 20 | +21 21 | # OK (not indexing (solely) the first element) + +RUF015.py:19:1: RUF015 [*] Prefer `[next(iter(x))]` over `list(x)[:1]` + | +17 | [i for i in x][:1] +18 | [i for i in x][:1:1] +19 | [i for i in x][:1:2] + | ^^^^^^^^^^^^^^^^^^^^ RUF015 +20 | +21 | # OK (not indexing (solely) the first element) + | + = help: Replace with `[next(iter(x))] + +ℹ Suggested fix +16 16 | [i for i in x][0] +17 17 | [i for i in x][:1] +18 18 | [i for i in x][:1:1] +19 |-[i for i in x][:1:2] + 19 |+[next(iter(x))] +20 20 | +21 21 | # OK (not indexing (solely) the first element) +22 22 | list(x) + + diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF016_RUF016.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF016_RUF016.py.snap new file mode 100644 index 0000000000..bf56f0d36c --- /dev/null +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF016_RUF016.py.snap @@ -0,0 +1,379 @@ +--- +source: crates/ruff/src/rules/ruff/mod.rs +--- +RUF016.py:20:13: RUF016 Indexed access to type `str` uses type `str` instead of an integer or slice. + | +19 | # Should emit for invalid access on strings +20 | var = "abc"["x"] + | ^^^ RUF016 +21 | var = f"abc"["x"] + | + +RUF016.py:21:14: RUF016 Indexed access to type `str` uses type `str` instead of an integer or slice. + | +19 | # Should emit for invalid access on strings +20 | var = "abc"["x"] +21 | var = f"abc"["x"] + | ^^^ RUF016 +22 | +23 | # Should emit for invalid access on bytes + | + +RUF016.py:24:14: RUF016 Indexed access to type `bytes` uses type `str` instead of an integer or slice. + | +23 | # Should emit for invalid access on bytes +24 | var = b"abc"["x"] + | ^^^ RUF016 +25 | +26 | # Should emit for invalid access on lists and tuples + | + +RUF016.py:27:17: RUF016 Indexed access to type `list` uses type `str` instead of an integer or slice. + | +26 | # Should emit for invalid access on lists and tuples +27 | var = [1, 2, 3]["x"] + | ^^^ RUF016 +28 | var = (1, 2, 3)["x"] + | + +RUF016.py:28:17: RUF016 Indexed access to type `tuple` uses type `str` instead of an integer or slice. + | +26 | # Should emit for invalid access on lists and tuples +27 | var = [1, 2, 3]["x"] +28 | var = (1, 2, 3)["x"] + | ^^^ RUF016 +29 | +30 | # Should emit for invalid access on list comprehensions + | + +RUF016.py:31:30: RUF016 Indexed access to type `list comprehension` uses type `str` instead of an integer or slice. + | +30 | # Should emit for invalid access on list comprehensions +31 | var = [x for x in range(10)]["x"] + | ^^^ RUF016 +32 | +33 | # Should emit for invalid access using tuple + | + +RUF016.py:34:13: RUF016 Indexed access to type `str` uses type `tuple` instead of an integer or slice. + | +33 | # Should emit for invalid access using tuple +34 | var = "abc"[1, 2] + | ^^^^ RUF016 +35 | +36 | # Should emit for invalid access using string + | + +RUF016.py:37:14: RUF016 Indexed access to type `list` uses type `str` instead of an integer or slice. + | +36 | # Should emit for invalid access using string +37 | var = [1, 2]["x"] + | ^^^ RUF016 +38 | +39 | # Should emit for invalid access using float + | + +RUF016.py:40:14: RUF016 Indexed access to type `list` uses type `float` instead of an integer or slice. + | +39 | # Should emit for invalid access using float +40 | var = [1, 2][0.25] + | ^^^^ RUF016 +41 | +42 | # Should emit for invalid access using dict + | + +RUF016.py:43:14: RUF016 Indexed access to type `list` uses type `dict` instead of an integer or slice. + | +42 | # Should emit for invalid access using dict +43 | var = [1, 2][{"x": "y"}] + | ^^^^^^^^^^ RUF016 +44 | +45 | # Should emit for invalid access using dict comp + | + +RUF016.py:46:14: RUF016 Indexed access to type `list` uses type `dict comprehension` instead of an integer or slice. + | +45 | # Should emit for invalid access using dict comp +46 | var = [1, 2][{x: "y" for x in range(2)}] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF016 +47 | +48 | # Should emit for invalid access using list + | + +RUF016.py:49:14: RUF016 Indexed access to type `list` uses type `tuple` instead of an integer or slice. + | +48 | # Should emit for invalid access using list +49 | var = [1, 2][2, 3] + | ^^^^ RUF016 +50 | +51 | # Should emit for invalid access using list comp + | + +RUF016.py:52:14: RUF016 Indexed access to type `list` uses type `list comprehension` instead of an integer or slice. + | +51 | # Should emit for invalid access using list comp +52 | var = [1, 2][[x for x in range(2)]] + | ^^^^^^^^^^^^^^^^^^^^^ RUF016 +53 | +54 | # Should emit on invalid access using set + | + +RUF016.py:55:14: RUF016 Indexed access to type `list` uses type `set` instead of an integer or slice. + | +54 | # Should emit on invalid access using set +55 | var = [1, 2][{"x", "y"}] + | ^^^^^^^^^^ RUF016 +56 | +57 | # Should emit on invalid access using set comp + | + +RUF016.py:58:14: RUF016 Indexed access to type `list` uses type `set comprehension` instead of an integer or slice. + | +57 | # Should emit on invalid access using set comp +58 | var = [1, 2][{x for x in range(2)}] + | ^^^^^^^^^^^^^^^^^^^^^ RUF016 +59 | +60 | # Should emit on invalid access using bytes + | + +RUF016.py:61:14: RUF016 Indexed access to type `list` uses type `bytes` instead of an integer or slice. + | +60 | # Should emit on invalid access using bytes +61 | var = [1, 2][b"x"] + | ^^^^ RUF016 +62 | +63 | # Should emit for non-integer slice start + | + +RUF016.py:64:17: RUF016 Slice in indexed access to type `list` uses type `str` instead of an integer. + | +63 | # Should emit for non-integer slice start +64 | var = [1, 2, 3]["x":2] + | ^^^ RUF016 +65 | var = [1, 2, 3][f"x":2] +66 | var = [1, 2, 3][1.2:2] + | + +RUF016.py:65:17: RUF016 Slice in indexed access to type `list` uses type `str` instead of an integer. + | +63 | # Should emit for non-integer slice start +64 | var = [1, 2, 3]["x":2] +65 | var = [1, 2, 3][f"x":2] + | ^^^^ RUF016 +66 | var = [1, 2, 3][1.2:2] +67 | var = [1, 2, 3][{"x"}:2] + | + +RUF016.py:66:17: RUF016 Slice in indexed access to type `list` uses type `float` instead of an integer. + | +64 | var = [1, 2, 3]["x":2] +65 | var = [1, 2, 3][f"x":2] +66 | var = [1, 2, 3][1.2:2] + | ^^^ RUF016 +67 | var = [1, 2, 3][{"x"}:2] +68 | var = [1, 2, 3][{x for x in range(2)}:2] + | + +RUF016.py:67:17: RUF016 Slice in indexed access to type `list` uses type `set` instead of an integer. + | +65 | var = [1, 2, 3][f"x":2] +66 | var = [1, 2, 3][1.2:2] +67 | var = [1, 2, 3][{"x"}:2] + | ^^^^^ RUF016 +68 | var = [1, 2, 3][{x for x in range(2)}:2] +69 | var = [1, 2, 3][{"x": x for x in range(2)}:2] + | + +RUF016.py:68:17: RUF016 Slice in indexed access to type `list` uses type `set comprehension` instead of an integer. + | +66 | var = [1, 2, 3][1.2:2] +67 | var = [1, 2, 3][{"x"}:2] +68 | var = [1, 2, 3][{x for x in range(2)}:2] + | ^^^^^^^^^^^^^^^^^^^^^ RUF016 +69 | var = [1, 2, 3][{"x": x for x in range(2)}:2] +70 | var = [1, 2, 3][[x for x in range(2)]:2] + | + +RUF016.py:69:17: RUF016 Slice in indexed access to type `list` uses type `dict comprehension` instead of an integer. + | +67 | var = [1, 2, 3][{"x"}:2] +68 | var = [1, 2, 3][{x for x in range(2)}:2] +69 | var = [1, 2, 3][{"x": x for x in range(2)}:2] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF016 +70 | var = [1, 2, 3][[x for x in range(2)]:2] + | + +RUF016.py:70:17: RUF016 Slice in indexed access to type `list` uses type `list comprehension` instead of an integer. + | +68 | var = [1, 2, 3][{x for x in range(2)}:2] +69 | var = [1, 2, 3][{"x": x for x in range(2)}:2] +70 | var = [1, 2, 3][[x for x in range(2)]:2] + | ^^^^^^^^^^^^^^^^^^^^^ RUF016 +71 | +72 | # Should emit for non-integer slice end + | + +RUF016.py:73:19: RUF016 Slice in indexed access to type `list` uses type `str` instead of an integer. + | +72 | # Should emit for non-integer slice end +73 | var = [1, 2, 3][0:"x"] + | ^^^ RUF016 +74 | var = [1, 2, 3][0:f"x"] +75 | var = [1, 2, 3][0:1.2] + | + +RUF016.py:74:19: RUF016 Slice in indexed access to type `list` uses type `str` instead of an integer. + | +72 | # Should emit for non-integer slice end +73 | var = [1, 2, 3][0:"x"] +74 | var = [1, 2, 3][0:f"x"] + | ^^^^ RUF016 +75 | var = [1, 2, 3][0:1.2] +76 | var = [1, 2, 3][0:{"x"}] + | + +RUF016.py:75:19: RUF016 Slice in indexed access to type `list` uses type `float` instead of an integer. + | +73 | var = [1, 2, 3][0:"x"] +74 | var = [1, 2, 3][0:f"x"] +75 | var = [1, 2, 3][0:1.2] + | ^^^ RUF016 +76 | var = [1, 2, 3][0:{"x"}] +77 | var = [1, 2, 3][0:{x for x in range(2)}] + | + +RUF016.py:76:19: RUF016 Slice in indexed access to type `list` uses type `set` instead of an integer. + | +74 | var = [1, 2, 3][0:f"x"] +75 | var = [1, 2, 3][0:1.2] +76 | var = [1, 2, 3][0:{"x"}] + | ^^^^^ RUF016 +77 | var = [1, 2, 3][0:{x for x in range(2)}] +78 | var = [1, 2, 3][0:{"x": x for x in range(2)}] + | + +RUF016.py:77:19: RUF016 Slice in indexed access to type `list` uses type `set comprehension` instead of an integer. + | +75 | var = [1, 2, 3][0:1.2] +76 | var = [1, 2, 3][0:{"x"}] +77 | var = [1, 2, 3][0:{x for x in range(2)}] + | ^^^^^^^^^^^^^^^^^^^^^ RUF016 +78 | var = [1, 2, 3][0:{"x": x for x in range(2)}] +79 | var = [1, 2, 3][0:[x for x in range(2)]] + | + +RUF016.py:78:19: RUF016 Slice in indexed access to type `list` uses type `dict comprehension` instead of an integer. + | +76 | var = [1, 2, 3][0:{"x"}] +77 | var = [1, 2, 3][0:{x for x in range(2)}] +78 | var = [1, 2, 3][0:{"x": x for x in range(2)}] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF016 +79 | var = [1, 2, 3][0:[x for x in range(2)]] + | + +RUF016.py:79:19: RUF016 Slice in indexed access to type `list` uses type `list comprehension` instead of an integer. + | +77 | var = [1, 2, 3][0:{x for x in range(2)}] +78 | var = [1, 2, 3][0:{"x": x for x in range(2)}] +79 | var = [1, 2, 3][0:[x for x in range(2)]] + | ^^^^^^^^^^^^^^^^^^^^^ RUF016 +80 | +81 | # Should emit for non-integer slice step + | + +RUF016.py:82:21: RUF016 Slice in indexed access to type `list` uses type `str` instead of an integer. + | +81 | # Should emit for non-integer slice step +82 | var = [1, 2, 3][0:1:"x"] + | ^^^ RUF016 +83 | var = [1, 2, 3][0:1:f"x"] +84 | var = [1, 2, 3][0:1:1.2] + | + +RUF016.py:83:21: RUF016 Slice in indexed access to type `list` uses type `str` instead of an integer. + | +81 | # Should emit for non-integer slice step +82 | var = [1, 2, 3][0:1:"x"] +83 | var = [1, 2, 3][0:1:f"x"] + | ^^^^ RUF016 +84 | var = [1, 2, 3][0:1:1.2] +85 | var = [1, 2, 3][0:1:{"x"}] + | + +RUF016.py:84:21: RUF016 Slice in indexed access to type `list` uses type `float` instead of an integer. + | +82 | var = [1, 2, 3][0:1:"x"] +83 | var = [1, 2, 3][0:1:f"x"] +84 | var = [1, 2, 3][0:1:1.2] + | ^^^ RUF016 +85 | var = [1, 2, 3][0:1:{"x"}] +86 | var = [1, 2, 3][0:1:{x for x in range(2)}] + | + +RUF016.py:85:21: RUF016 Slice in indexed access to type `list` uses type `set` instead of an integer. + | +83 | var = [1, 2, 3][0:1:f"x"] +84 | var = [1, 2, 3][0:1:1.2] +85 | var = [1, 2, 3][0:1:{"x"}] + | ^^^^^ RUF016 +86 | var = [1, 2, 3][0:1:{x for x in range(2)}] +87 | var = [1, 2, 3][0:1:{"x": x for x in range(2)}] + | + +RUF016.py:86:21: RUF016 Slice in indexed access to type `list` uses type `set comprehension` instead of an integer. + | +84 | var = [1, 2, 3][0:1:1.2] +85 | var = [1, 2, 3][0:1:{"x"}] +86 | var = [1, 2, 3][0:1:{x for x in range(2)}] + | ^^^^^^^^^^^^^^^^^^^^^ RUF016 +87 | var = [1, 2, 3][0:1:{"x": x for x in range(2)}] +88 | var = [1, 2, 3][0:1:[x for x in range(2)]] + | + +RUF016.py:87:21: RUF016 Slice in indexed access to type `list` uses type `dict comprehension` instead of an integer. + | +85 | var = [1, 2, 3][0:1:{"x"}] +86 | var = [1, 2, 3][0:1:{x for x in range(2)}] +87 | var = [1, 2, 3][0:1:{"x": x for x in range(2)}] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF016 +88 | var = [1, 2, 3][0:1:[x for x in range(2)]] + | + +RUF016.py:88:21: RUF016 Slice in indexed access to type `list` uses type `list comprehension` instead of an integer. + | +86 | var = [1, 2, 3][0:1:{x for x in range(2)}] +87 | var = [1, 2, 3][0:1:{"x": x for x in range(2)}] +88 | var = [1, 2, 3][0:1:[x for x in range(2)]] + | ^^^^^^^^^^^^^^^^^^^^^ RUF016 +89 | +90 | # Should emit for non-integer slice start and end; should emit twice with specific ranges + | + +RUF016.py:91:17: RUF016 Slice in indexed access to type `list` uses type `str` instead of an integer. + | +90 | # Should emit for non-integer slice start and end; should emit twice with specific ranges +91 | var = [1, 2, 3]["x":"y"] + | ^^^ RUF016 +92 | +93 | # Should emit once for repeated invalid access + | + +RUF016.py:91:21: RUF016 Slice in indexed access to type `list` uses type `str` instead of an integer. + | +90 | # Should emit for non-integer slice start and end; should emit twice with specific ranges +91 | var = [1, 2, 3]["x":"y"] + | ^^^ RUF016 +92 | +93 | # Should emit once for repeated invalid access + | + +RUF016.py:94:17: RUF016 Indexed access to type `list` uses type `str` instead of an integer or slice. + | +93 | # Should emit once for repeated invalid access +94 | var = [1, 2, 3]["x"]["y"]["z"] + | ^^^ RUF016 +95 | +96 | # Cannot emit on invalid access using variable in index + | + + diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__confusables.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__confusables.snap index ec47da8256..23057ef9eb 100644 --- a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__confusables.snap +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__confusables.snap @@ -9,7 +9,7 @@ confusables.py:1:6: RUF001 [*] String contains ambiguous `𝐁` (MATHEMATICAL BO | = help: Replace `𝐁` (MATHEMATICAL BOLD CAPITAL B) with `B` (LATIN CAPITAL LETTER B) -ℹ Suggested fix +ℹ Possible fix 1 |-x = "𝐁ad string" 1 |+x = "Bad string" 2 2 | y = "−" @@ -26,7 +26,7 @@ confusables.py:6:56: RUF002 [*] Docstring contains ambiguous `)` (FULLWIDTH RI | = help: Replace `)` (FULLWIDTH RIGHT PARENTHESIS) with `)` (RIGHT PARENTHESIS) -ℹ Suggested fix +ℹ Possible fix 3 3 | 4 4 | 5 5 | def f(): @@ -46,7 +46,7 @@ confusables.py:7:62: RUF003 [*] Comment contains ambiguous `᜵` (PHILIPPINE SIN | = help: Replace `᜵` (PHILIPPINE SINGLE PUNCTUATION) with `/` (SOLIDUS) -ℹ Suggested fix +ℹ Possible fix 4 4 | 5 5 | def f(): 6 6 | """Here's a docstring with an unusual parenthesis: )""" @@ -64,7 +64,7 @@ confusables.py:17:6: RUF001 [*] String contains ambiguous `𝐁` (MATHEMATICAL B | = help: Replace `𝐁` (MATHEMATICAL BOLD CAPITAL B) with `B` (LATIN CAPITAL LETTER B) -ℹ Suggested fix +ℹ Possible fix 14 14 | ... 15 15 | 16 16 | @@ -85,7 +85,7 @@ confusables.py:26:10: RUF001 [*] String contains ambiguous `α` (GREEK SMALL LET | = help: Replace `α` (GREEK SMALL LETTER ALPHA) with `a` (LATIN SMALL LETTER A) -ℹ Suggested fix +ℹ Possible fix 23 23 | 24 24 | # The first word should be ignored, while the second should be included, since it 25 25 | # contains ASCII. @@ -104,7 +104,7 @@ confusables.py:31:6: RUF001 [*] String contains ambiguous `Р` (CYRILLIC CAPITAL | = help: Replace `Р` (CYRILLIC CAPITAL LETTER ER) with `P` (LATIN CAPITAL LETTER P) -ℹ Suggested fix +ℹ Possible fix 28 28 | # The two characters should be flagged here. The first character is a "word" 29 29 | # consisting of a single ambiguous character, while the second character is a "word 30 30 | # boundary" (whitespace) that it itself ambiguous. @@ -120,7 +120,7 @@ confusables.py:31:7: RUF001 [*] String contains ambiguous ` ` (EN QUAD). Did y | = help: Replace ` ` (EN QUAD) with ` ` (SPACE) -ℹ Suggested fix +ℹ Possible fix 28 28 | # The two characters should be flagged here. The first character is a "word" 29 29 | # consisting of a single ambiguous character, while the second character is a "word 30 30 | # boundary" (whitespace) that it itself ambiguous. diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__ruf100_0.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__ruf100_0.snap index b581a21c34..96a0369c89 100644 --- a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__ruf100_0.snap +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__ruf100_0.snap @@ -80,7 +80,7 @@ RUF100_0.py:19:12: RUF100 [*] Unused `noqa` directive (unused: `F841`, `W191`; n 21 21 | # Invalid (but external) 22 22 | d = 1 # noqa: F841, V101 -RUF100_0.py:22:12: RUF100 [*] Unused `noqa` directive (unused: `F841`; unknown: `V101`) +RUF100_0.py:22:12: RUF100 [*] Unused `noqa` directive (unused: `F841`) | 21 | # Invalid (but external) 22 | d = 1 # noqa: F841, V101 @@ -95,7 +95,7 @@ RUF100_0.py:22:12: RUF100 [*] Unused `noqa` directive (unused: `F841`; unknown: 20 20 | 21 21 | # Invalid (but external) 22 |- d = 1 # noqa: F841, V101 - 22 |+ d = 1 + 22 |+ d = 1 # noqa: V101 23 23 | 24 24 | # fmt: off 25 25 | # Invalid - no space before # diff --git a/crates/ruff/src/rules/ruff/typing.rs b/crates/ruff/src/rules/ruff/typing.rs new file mode 100644 index 0000000000..127c213fef --- /dev/null +++ b/crates/ruff/src/rules/ruff/typing.rs @@ -0,0 +1,330 @@ +use rustpython_parser::ast::{self, Constant, Expr, Operator}; + +use ruff_python_ast::call_path::CallPath; +use ruff_python_ast::source_code::Locator; +use ruff_python_ast::typing::parse_type_annotation; +use ruff_python_semantic::SemanticModel; +use ruff_python_stdlib::sys::is_known_standard_library; + +/// Custom iterator to collect all the `|` separated expressions in a PEP 604 +/// union type. +struct PEP604UnionIterator<'a> { + stack: Vec<&'a Expr>, +} + +impl<'a> PEP604UnionIterator<'a> { + fn new(expr: &'a Expr) -> Self { + Self { stack: vec![expr] } + } +} + +impl<'a> Iterator for PEP604UnionIterator<'a> { + type Item = &'a Expr; + + fn next(&mut self) -> Option { + while let Some(expr) = self.stack.pop() { + match expr { + Expr::BinOp(ast::ExprBinOp { + left, + op: Operator::BitOr, + right, + .. + }) => { + self.stack.push(left); + self.stack.push(right); + } + _ => return Some(expr), + } + } + None + } +} + +/// Returns `true` if the given call path is a known type. +/// +/// A known type is either a builtin type, any object from the standard library, +/// or a type from the `typing_extensions` module. +fn is_known_type(call_path: &CallPath, minor_version: u32) -> bool { + match call_path.as_slice() { + ["" | "typing_extensions", ..] => true, + [module, ..] => is_known_standard_library(minor_version, module), + _ => false, + } +} + +#[derive(Debug)] +enum TypingTarget<'a> { + /// Literal `None` type. + None, + + /// A `typing.Any` type. + Any, + + /// Literal `object` type. + Object, + + /// Forward reference to a type e.g., `"List[str]"`. + ForwardReference(Expr), + + /// A `typing.Union` type or `|` separated types e.g., `Union[int, str]` + /// or `int | str`. + Union(Vec<&'a Expr>), + + /// A `typing.Literal` type e.g., `Literal[1, 2, 3]`. + Literal(Vec<&'a Expr>), + + /// A `typing.Optional` type e.g., `Optional[int]`. + Optional(&'a Expr), + + /// A `typing.Annotated` type e.g., `Annotated[int, ...]`. + Annotated(&'a Expr), + + /// Special type used to represent an unknown type (and not a typing target) + /// which could be a type alias. + Unknown, + + /// Special type used to represent a known type (and not a typing target). + /// A known type is either a builtin type, any object from the standard + /// library, or a type from the `typing_extensions` module. + Known, +} + +impl<'a> TypingTarget<'a> { + fn try_from_expr( + expr: &'a Expr, + semantic: &SemanticModel, + locator: &Locator, + minor_version: u32, + ) -> Option { + match expr { + Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => { + if semantic.match_typing_expr(value, "Optional") { + return Some(TypingTarget::Optional(slice.as_ref())); + } + let Expr::Tuple(ast::ExprTuple { elts: elements, .. }) = slice.as_ref() else { + return None; + }; + if semantic.match_typing_expr(value, "Literal") { + Some(TypingTarget::Literal(elements.iter().collect())) + } else if semantic.match_typing_expr(value, "Union") { + Some(TypingTarget::Union(elements.iter().collect())) + } else if semantic.match_typing_expr(value, "Annotated") { + elements.first().map(TypingTarget::Annotated) + } else { + semantic.resolve_call_path(value).map_or( + // If we can't resolve the call path, it must be defined + // in the same file and could be a type alias. + Some(TypingTarget::Unknown), + |call_path| { + if is_known_type(&call_path, minor_version) { + Some(TypingTarget::Known) + } else { + Some(TypingTarget::Unknown) + } + }, + ) + } + } + Expr::BinOp(..) => Some(TypingTarget::Union( + PEP604UnionIterator::new(expr).collect(), + )), + Expr::Constant(ast::ExprConstant { + value: Constant::None, + .. + }) => Some(TypingTarget::None), + Expr::Constant(ast::ExprConstant { + value: Constant::Str(string), + range, + .. + }) => parse_type_annotation(string, *range, locator) + .map_or(None, |(expr, _)| Some(TypingTarget::ForwardReference(expr))), + _ => semantic.resolve_call_path(expr).map_or( + // If we can't resolve the call path, it must be defined in the + // same file, so we assume it's `Any` as it could be a type alias. + Some(TypingTarget::Unknown), + |call_path| { + if semantic.match_typing_call_path(&call_path, "Any") { + Some(TypingTarget::Any) + } else if matches!(call_path.as_slice(), ["" | "builtins", "object"]) { + Some(TypingTarget::Object) + } else if !is_known_type(&call_path, minor_version) { + // If it's not a known type, we assume it's `Any`. + Some(TypingTarget::Unknown) + } else { + Some(TypingTarget::Known) + } + }, + ), + } + } + + /// Check if the [`TypingTarget`] explicitly allows `None`. + fn contains_none( + &self, + semantic: &SemanticModel, + locator: &Locator, + minor_version: u32, + ) -> bool { + match self { + TypingTarget::None + | TypingTarget::Optional(_) + | TypingTarget::Any + | TypingTarget::Object + | TypingTarget::Unknown => true, + TypingTarget::Known => false, + TypingTarget::Literal(elements) => elements.iter().any(|element| { + // Literal can only contain `None`, a literal value, other `Literal` + // or an enum value. + match TypingTarget::try_from_expr(element, semantic, locator, minor_version) { + None | Some(TypingTarget::None) => true, + Some(new_target @ TypingTarget::Literal(_)) => { + new_target.contains_none(semantic, locator, minor_version) + } + _ => false, + } + }), + TypingTarget::Union(elements) => elements.iter().any(|element| { + TypingTarget::try_from_expr(element, semantic, locator, minor_version) + .map_or(true, |new_target| { + new_target.contains_none(semantic, locator, minor_version) + }) + }), + TypingTarget::Annotated(element) => { + TypingTarget::try_from_expr(element, semantic, locator, minor_version) + .map_or(true, |new_target| { + new_target.contains_none(semantic, locator, minor_version) + }) + } + TypingTarget::ForwardReference(expr) => { + TypingTarget::try_from_expr(expr, semantic, locator, minor_version) + .map_or(true, |new_target| { + new_target.contains_none(semantic, locator, minor_version) + }) + } + } + } + + /// Check if the [`TypingTarget`] explicitly allows `Any`. + fn contains_any( + &self, + semantic: &SemanticModel, + locator: &Locator, + minor_version: u32, + ) -> bool { + match self { + TypingTarget::Any => true, + // `Literal` cannot contain `Any` as it's a dynamic value. + TypingTarget::Literal(_) + | TypingTarget::None + | TypingTarget::Object + | TypingTarget::Known + | TypingTarget::Unknown => false, + TypingTarget::Union(elements) => elements.iter().any(|element| { + TypingTarget::try_from_expr(element, semantic, locator, minor_version) + .map_or(true, |new_target| { + new_target.contains_any(semantic, locator, minor_version) + }) + }), + TypingTarget::Annotated(element) | TypingTarget::Optional(element) => { + TypingTarget::try_from_expr(element, semantic, locator, minor_version) + .map_or(true, |new_target| { + new_target.contains_any(semantic, locator, minor_version) + }) + } + TypingTarget::ForwardReference(expr) => { + TypingTarget::try_from_expr(expr, semantic, locator, minor_version) + .map_or(true, |new_target| { + new_target.contains_any(semantic, locator, minor_version) + }) + } + } + } +} + +/// Check if the given annotation [`Expr`] explicitly allows `None`. +/// +/// This function will return `None` if the annotation explicitly allows `None` +/// otherwise it will return the annotation itself. If it's a `Annotated` type, +/// then the inner type will be checked. +/// +/// This function assumes that the annotation is a valid typing annotation expression. +pub(crate) fn type_hint_explicitly_allows_none<'a>( + annotation: &'a Expr, + semantic: &SemanticModel, + locator: &Locator, + minor_version: u32, +) -> Option<&'a Expr> { + match TypingTarget::try_from_expr(annotation, semantic, locator, minor_version) { + None | + // Short circuit on top level `None`, `Any` or `Optional` + Some(TypingTarget::None | TypingTarget::Optional(_) | TypingTarget::Any) => None, + // Top-level `Annotated` node should check for the inner type and + // return the inner type if it doesn't allow `None`. If `Annotated` + // is found nested inside another type, then the outer type should + // be returned. + Some(TypingTarget::Annotated(expr)) => { + type_hint_explicitly_allows_none(expr, semantic, locator, minor_version) + } + Some(target) => { + if target.contains_none(semantic, locator, minor_version) { + None + } else { + Some(annotation) + } + } + } +} + +/// Check if the given annotation [`Expr`] resolves to `Any`. +/// +/// This function assumes that the annotation is a valid typing annotation expression. +pub(crate) fn type_hint_resolves_to_any( + annotation: &Expr, + semantic: &SemanticModel, + locator: &Locator, + minor_version: u32, +) -> bool { + match TypingTarget::try_from_expr(annotation, semantic, locator, minor_version) { + None | + // Short circuit on top level `Any` + Some(TypingTarget::Any) => true, + // Top-level `Annotated` node should check if the inner type resolves + // to `Any`. + Some(TypingTarget::Annotated(expr)) => { + type_hint_resolves_to_any(expr, semantic, locator, minor_version) + } + Some(target) => target.contains_any(semantic, locator, minor_version), + } +} + +#[cfg(test)] +mod tests { + use ruff_python_ast::call_path::CallPath; + + use super::is_known_type; + + #[test] + fn test_is_known_type() { + assert!(is_known_type(&CallPath::from_slice(&["", "int"]), 11)); + assert!(is_known_type( + &CallPath::from_slice(&["builtins", "int"]), + 11 + )); + assert!(is_known_type( + &CallPath::from_slice(&["typing", "Optional"]), + 11 + )); + assert!(is_known_type( + &CallPath::from_slice(&["typing_extensions", "Literal"]), + 11 + )); + assert!(is_known_type( + &CallPath::from_slice(&["zoneinfo", "ZoneInfo"]), + 11 + )); + assert!(!is_known_type( + &CallPath::from_slice(&["zoneinfo", "ZoneInfo"]), + 8 + )); + } +} diff --git a/crates/ruff/src/rules/tryceratops/helpers.rs b/crates/ruff/src/rules/tryceratops/helpers.rs index e7067a9eee..d4a5655893 100644 --- a/crates/ruff/src/rules/tryceratops/helpers.rs +++ b/crates/ruff/src/rules/tryceratops/helpers.rs @@ -3,18 +3,18 @@ use rustpython_parser::ast::{self, Expr}; use ruff_python_ast::visitor; use ruff_python_ast::visitor::Visitor; use ruff_python_semantic::analyze::logging; -use ruff_python_semantic::model::SemanticModel; +use ruff_python_semantic::SemanticModel; /// Collect `logging`-like calls from an AST. pub(super) struct LoggerCandidateVisitor<'a, 'b> { - semantic_model: &'a SemanticModel<'b>, + semantic: &'a SemanticModel<'b>, pub(super) calls: Vec<&'b ast::ExprCall>, } impl<'a, 'b> LoggerCandidateVisitor<'a, 'b> { - pub(super) fn new(semantic_model: &'a SemanticModel<'b>) -> Self { + pub(super) fn new(semantic: &'a SemanticModel<'b>) -> Self { LoggerCandidateVisitor { - semantic_model, + semantic, calls: Vec::new(), } } @@ -23,7 +23,7 @@ impl<'a, 'b> LoggerCandidateVisitor<'a, 'b> { impl<'a, 'b> Visitor<'b> for LoggerCandidateVisitor<'a, 'b> { fn visit_expr(&mut self, expr: &'b Expr) { if let Expr::Call(call) = expr { - if logging::is_logger_candidate(&call.func, self.semantic_model) { + if logging::is_logger_candidate(&call.func, self.semantic) { self.calls.push(call); } } diff --git a/crates/ruff/src/rules/tryceratops/mod.rs b/crates/ruff/src/rules/tryceratops/mod.rs index bf88fb825b..cd1075ba86 100644 --- a/crates/ruff/src/rules/tryceratops/mod.rs +++ b/crates/ruff/src/rules/tryceratops/mod.rs @@ -1,4 +1,4 @@ -//! Rules from [tryceratops](https://pypi.org/project/tryceratops/1.1.0/). +//! Rules from [tryceratops](https://pypi.org/project/tryceratops/). pub(crate) mod helpers; pub(crate) mod rules; diff --git a/crates/ruff/src/rules/tryceratops/rules/error_instead_of_exception.rs b/crates/ruff/src/rules/tryceratops/rules/error_instead_of_exception.rs index 09af41b601..4e111e7e99 100644 --- a/crates/ruff/src/rules/tryceratops/rules/error_instead_of_exception.rs +++ b/crates/ruff/src/rules/tryceratops/rules/error_instead_of_exception.rs @@ -1,4 +1,4 @@ -use rustpython_parser::ast::{self, Excepthandler, Expr, Ranged}; +use rustpython_parser::ast::{self, ExceptHandler, Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -54,18 +54,18 @@ impl Violation for ErrorInsteadOfException { } /// TRY400 -pub(crate) fn error_instead_of_exception(checker: &mut Checker, handlers: &[Excepthandler]) { +pub(crate) fn error_instead_of_exception(checker: &mut Checker, handlers: &[ExceptHandler]) { for handler in handlers { - let Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { body, .. }) = handler; + let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { body, .. }) = handler; let calls = { - let mut visitor = LoggerCandidateVisitor::new(checker.semantic_model()); + let mut visitor = LoggerCandidateVisitor::new(checker.semantic()); visitor.visit_body(body); visitor.calls }; for expr in calls { if let Expr::Attribute(ast::ExprAttribute { attr, .. }) = expr.func.as_ref() { if attr == "error" { - if exc_info(&expr.keywords, checker.semantic_model()).is_none() { + if exc_info(&expr.keywords, checker.semantic()).is_none() { checker .diagnostics .push(Diagnostic::new(ErrorInsteadOfException, expr.range())); diff --git a/crates/ruff/src/rules/tryceratops/rules/mod.rs b/crates/ruff/src/rules/tryceratops/rules/mod.rs index c833503d30..9d004969c0 100644 --- a/crates/ruff/src/rules/tryceratops/rules/mod.rs +++ b/crates/ruff/src/rules/tryceratops/rules/mod.rs @@ -1,15 +1,13 @@ -pub(crate) use error_instead_of_exception::{error_instead_of_exception, ErrorInsteadOfException}; -pub(crate) use raise_vanilla_args::{raise_vanilla_args, RaiseVanillaArgs}; -pub(crate) use raise_vanilla_class::{raise_vanilla_class, RaiseVanillaClass}; -pub(crate) use raise_within_try::{raise_within_try, RaiseWithinTry}; -pub(crate) use reraise_no_cause::{reraise_no_cause, ReraiseNoCause}; -pub(crate) use try_consider_else::{try_consider_else, TryConsiderElse}; -pub(crate) use type_check_without_type_error::{ - type_check_without_type_error, TypeCheckWithoutTypeError, -}; -pub(crate) use useless_try_except::{useless_try_except, UselessTryExcept}; -pub(crate) use verbose_log_message::{verbose_log_message, VerboseLogMessage}; -pub(crate) use verbose_raise::{verbose_raise, VerboseRaise}; +pub(crate) use error_instead_of_exception::*; +pub(crate) use raise_vanilla_args::*; +pub(crate) use raise_vanilla_class::*; +pub(crate) use raise_within_try::*; +pub(crate) use reraise_no_cause::*; +pub(crate) use try_consider_else::*; +pub(crate) use type_check_without_type_error::*; +pub(crate) use useless_try_except::*; +pub(crate) use verbose_log_message::*; +pub(crate) use verbose_raise::*; mod error_instead_of_exception; mod raise_vanilla_args; diff --git a/crates/ruff/src/rules/tryceratops/rules/raise_vanilla_class.rs b/crates/ruff/src/rules/tryceratops/rules/raise_vanilla_class.rs index 46024b2db3..e12cce062f 100644 --- a/crates/ruff/src/rules/tryceratops/rules/raise_vanilla_class.rs +++ b/crates/ruff/src/rules/tryceratops/rules/raise_vanilla_class.rs @@ -63,13 +63,15 @@ impl Violation for RaiseVanillaClass { /// TRY002 pub(crate) fn raise_vanilla_class(checker: &mut Checker, expr: &Expr) { if checker - .semantic_model() + .semantic() .resolve_call_path(if let Expr::Call(ast::ExprCall { func, .. }) = expr { func } else { expr }) - .map_or(false, |call_path| call_path.as_slice() == ["", "Exception"]) + .map_or(false, |call_path| { + matches!(call_path.as_slice(), ["", "Exception"]) + }) { checker .diagnostics diff --git a/crates/ruff/src/rules/tryceratops/rules/raise_within_try.rs b/crates/ruff/src/rules/tryceratops/rules/raise_within_try.rs index 5dcdcc117e..90baf84a06 100644 --- a/crates/ruff/src/rules/tryceratops/rules/raise_within_try.rs +++ b/crates/ruff/src/rules/tryceratops/rules/raise_within_try.rs @@ -1,13 +1,18 @@ -use rustpython_parser::ast::{Excepthandler, Ranged, Stmt}; +use rustpython_parser::ast::{self, ExceptHandler, Ranged, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::statement_visitor::{walk_stmt, StatementVisitor}; +use ruff_python_ast::{ + comparable::ComparableExpr, + helpers::{self, map_callable}, + statement_visitor::{walk_stmt, StatementVisitor}, +}; use crate::checkers::ast::Checker; /// ## What it does -/// Checks for `raise` statements within `try` blocks. +/// Checks for `raise` statements within `try` blocks. The only `raise`s +/// caught are those that throw exceptions caught by the `try` statement itself. /// /// ## Why is this bad? /// Raising and catching exceptions within the same `try` block is redundant, @@ -72,7 +77,7 @@ where } /// TRY301 -pub(crate) fn raise_within_try(checker: &mut Checker, body: &[Stmt], handlers: &[Excepthandler]) { +pub(crate) fn raise_within_try(checker: &mut Checker, body: &[Stmt], handlers: &[ExceptHandler]) { if handlers.is_empty() { return; } @@ -83,9 +88,40 @@ pub(crate) fn raise_within_try(checker: &mut Checker, body: &[Stmt], handlers: & visitor.raises }; + if raises.is_empty() { + return; + } + + let handled_exceptions = helpers::extract_handled_exceptions(handlers); + let comparables: Vec = handled_exceptions + .iter() + .map(|handler| ComparableExpr::from(*handler)) + .collect(); + for stmt in raises { - checker - .diagnostics - .push(Diagnostic::new(RaiseWithinTry, stmt.range())); + let Stmt::Raise(ast::StmtRaise { + exc: Some(exception), + .. + }) = stmt + else { + continue; + }; + + // We can't check exception sub-classes without a type-checker implementation, so let's + // just catch the blanket `Exception` for now. + if comparables.contains(&ComparableExpr::from(map_callable(exception))) + || handled_exceptions.iter().any(|expr| { + checker + .semantic() + .resolve_call_path(expr) + .map_or(false, |call_path| { + matches!(call_path.as_slice(), ["", "Exception" | "BaseException"]) + }) + }) + { + checker + .diagnostics + .push(Diagnostic::new(RaiseWithinTry, stmt.range())); + } } } diff --git a/crates/ruff/src/rules/tryceratops/rules/reraise_no_cause.rs b/crates/ruff/src/rules/tryceratops/rules/reraise_no_cause.rs index 259dfac223..a8a74e85b6 100644 --- a/crates/ruff/src/rules/tryceratops/rules/reraise_no_cause.rs +++ b/crates/ruff/src/rules/tryceratops/rules/reraise_no_cause.rs @@ -35,7 +35,7 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/exceptions.html#exception-context) +/// - [Python documentation: Exception context](https://docs.python.org/3/library/exceptions.html#exception-context) #[violation] pub struct ReraiseNoCause; diff --git a/crates/ruff/src/rules/tryceratops/rules/try_consider_else.rs b/crates/ruff/src/rules/tryceratops/rules/try_consider_else.rs index 91e7624f94..0ed6b3b27c 100644 --- a/crates/ruff/src/rules/tryceratops/rules/try_consider_else.rs +++ b/crates/ruff/src/rules/tryceratops/rules/try_consider_else.rs @@ -1,4 +1,4 @@ -use rustpython_parser::ast::{self, Excepthandler, Ranged, Stmt}; +use rustpython_parser::ast::{self, ExceptHandler, Ranged, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -44,7 +44,7 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/tutorial/errors.html) +/// - [Python documentation: Errors and Exceptions](https://docs.python.org/3/tutorial/errors.html) #[violation] pub struct TryConsiderElse; @@ -60,13 +60,13 @@ pub(crate) fn try_consider_else( checker: &mut Checker, body: &[Stmt], orelse: &[Stmt], - handler: &[Excepthandler], + handler: &[ExceptHandler], ) { if body.len() > 1 && orelse.is_empty() && !handler.is_empty() { if let Some(stmt) = body.last() { if let Stmt::Return(ast::StmtReturn { value, range: _ }) = stmt { if let Some(value) = value { - if contains_effect(value, |id| checker.semantic_model().is_builtin(id)) { + if contains_effect(value, |id| checker.semantic().is_builtin(id)) { return; } } diff --git a/crates/ruff/src/rules/tryceratops/rules/type_check_without_type_error.rs b/crates/ruff/src/rules/tryceratops/rules/type_check_without_type_error.rs index f87d7b010a..193da3a5c9 100644 --- a/crates/ruff/src/rules/tryceratops/rules/type_check_without_type_error.rs +++ b/crates/ruff/src/rules/tryceratops/rules/type_check_without_type_error.rs @@ -32,7 +32,7 @@ use crate::checkers::ast::Checker; /// ``` /// /// ## References -/// - [Python documentation](https://docs.python.org/3/library/exceptions.html#TypeError) +/// - [Python documentation: `TypeError`](https://docs.python.org/3/library/exceptions.html#TypeError) #[violation] pub struct TypeCheckWithoutTypeError; @@ -77,12 +77,13 @@ fn has_control_flow(stmt: &Stmt) -> bool { /// Returns `true` if an [`Expr`] is a call to check types. fn check_type_check_call(checker: &mut Checker, call: &Expr) -> bool { checker - .semantic_model() + .semantic() .resolve_call_path(call) .map_or(false, |call_path| { - call_path.as_slice() == ["", "isinstance"] - || call_path.as_slice() == ["", "issubclass"] - || call_path.as_slice() == ["", "callable"] + matches!( + call_path.as_slice(), + ["", "isinstance" | "issubclass" | "callable"] + ) }) } @@ -101,28 +102,30 @@ fn check_type_check_test(checker: &mut Checker, test: &Expr) -> bool { /// Returns `true` if `exc` is a reference to a builtin exception. fn is_builtin_exception(checker: &mut Checker, exc: &Expr) -> bool { return checker - .semantic_model() + .semantic() .resolve_call_path(exc) .map_or(false, |call_path| { - [ - "ArithmeticError", - "AssertionError", - "AttributeError", - "BufferError", - "EOFError", - "Exception", - "ImportError", - "LookupError", - "MemoryError", - "NameError", - "ReferenceError", - "RuntimeError", - "SyntaxError", - "SystemError", - "ValueError", - ] - .iter() - .any(|target| call_path.as_slice() == ["", target]) + matches!( + call_path.as_slice(), + [ + "", + "ArithmeticError" + | "AssertionError" + | "AttributeError" + | "BufferError" + | "EOFError" + | "Exception" + | "ImportError" + | "LookupError" + | "MemoryError" + | "NameError" + | "ReferenceError" + | "RuntimeError" + | "SyntaxError" + | "SystemError" + | "ValueError" + ] + ) }); } diff --git a/crates/ruff/src/rules/tryceratops/rules/useless_try_except.rs b/crates/ruff/src/rules/tryceratops/rules/useless_try_except.rs index 8869e4eccd..f256ff0f91 100644 --- a/crates/ruff/src/rules/tryceratops/rules/useless_try_except.rs +++ b/crates/ruff/src/rules/tryceratops/rules/useless_try_except.rs @@ -1,5 +1,4 @@ -use rustpython_parser::ast::Excepthandler::ExceptHandler; -use rustpython_parser::ast::{self, Excepthandler, ExcepthandlerExceptHandler, Expr, Ranged, Stmt}; +use rustpython_parser::ast::{self, ExceptHandler, ExceptHandlerExceptHandler, Expr, Ranged, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -39,18 +38,22 @@ impl Violation for UselessTryExcept { } /// TRY302 -pub(crate) fn useless_try_except(checker: &mut Checker, handlers: &[Excepthandler]) { +pub(crate) fn useless_try_except(checker: &mut Checker, handlers: &[ExceptHandler]) { if let Some(diagnostics) = handlers .iter() .map(|handler| { - let ExceptHandler(ExcepthandlerExceptHandler { name, body, .. }) = handler; - let Some(Stmt::Raise(ast::StmtRaise { exc, cause: None, .. })) = &body.first() else { + let ExceptHandler::ExceptHandler(ExceptHandlerExceptHandler { name, body, .. }) = + handler; + let Some(Stmt::Raise(ast::StmtRaise { + exc, cause: None, .. + })) = &body.first() + else { return None; }; if let Some(expr) = exc { // E.g., `except ... as e: raise e` if let Expr::Name(ast::ExprName { id, .. }) = expr.as_ref() { - if Some(id) == name.as_ref() { + if name.as_ref().map_or(false, |name| name.as_str() == id) { return Some(Diagnostic::new(UselessTryExcept, handler.range())); } } diff --git a/crates/ruff/src/rules/tryceratops/rules/verbose_log_message.rs b/crates/ruff/src/rules/tryceratops/rules/verbose_log_message.rs index dd1dd7d34c..da2652d9ed 100644 --- a/crates/ruff/src/rules/tryceratops/rules/verbose_log_message.rs +++ b/crates/ruff/src/rules/tryceratops/rules/verbose_log_message.rs @@ -1,4 +1,4 @@ -use rustpython_parser::ast::{self, Excepthandler, Expr, Ranged}; +use rustpython_parser::ast::{self, ExceptHandler, Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -57,9 +57,9 @@ impl<'a> Visitor<'a> for NameVisitor<'a> { } /// TRY401 -pub(crate) fn verbose_log_message(checker: &mut Checker, handlers: &[Excepthandler]) { +pub(crate) fn verbose_log_message(checker: &mut Checker, handlers: &[ExceptHandler]) { for handler in handlers { - let Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { name, body, .. }) = + let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { name, body, .. }) = handler; let Some(target) = name else { continue; @@ -67,7 +67,7 @@ pub(crate) fn verbose_log_message(checker: &mut Checker, handlers: &[Excepthandl // Find all calls to `logging.exception`. let calls = { - let mut visitor = LoggerCandidateVisitor::new(checker.semantic_model()); + let mut visitor = LoggerCandidateVisitor::new(checker.semantic()); visitor.visit_body(body); visitor.calls }; @@ -86,7 +86,7 @@ pub(crate) fn verbose_log_message(checker: &mut Checker, handlers: &[Excepthandl names }; for expr in names { - if expr.id == *target { + if expr.id == target.as_str() { checker .diagnostics .push(Diagnostic::new(VerboseLogMessage, expr.range())); diff --git a/crates/ruff/src/rules/tryceratops/rules/verbose_raise.rs b/crates/ruff/src/rules/tryceratops/rules/verbose_raise.rs index b740212215..c2829ca707 100644 --- a/crates/ruff/src/rules/tryceratops/rules/verbose_raise.rs +++ b/crates/ruff/src/rules/tryceratops/rules/verbose_raise.rs @@ -1,4 +1,4 @@ -use rustpython_parser::ast::{self, Excepthandler, Expr, Ranged, Stmt}; +use rustpython_parser::ast::{self, ExceptHandler, Expr, Ranged, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -74,10 +74,10 @@ where } /// TRY201 -pub(crate) fn verbose_raise(checker: &mut Checker, handlers: &[Excepthandler]) { +pub(crate) fn verbose_raise(checker: &mut Checker, handlers: &[ExceptHandler]) { for handler in handlers { // If the handler assigned a name to the exception... - if let Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { + if let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { name: Some(exception_name), body, .. @@ -95,7 +95,7 @@ pub(crate) fn verbose_raise(checker: &mut Checker, handlers: &[Excepthandler]) { if let Some(exc) = exc { // ...and the raised object is bound to the same name... if let Expr::Name(ast::ExprName { id, .. }) = exc { - if id == exception_name { + if id == exception_name.as_str() { checker .diagnostics .push(Diagnostic::new(VerboseRaise, exc.range())); diff --git a/crates/ruff/src/settings/configuration.rs b/crates/ruff/src/settings/configuration.rs index 16acabe30a..d385444d6e 100644 --- a/crates/ruff/src/settings/configuration.rs +++ b/crates/ruff/src/settings/configuration.rs @@ -17,9 +17,10 @@ use crate::line_width::{LineLength, TabSize}; use crate::rule_selector::RuleSelector; use crate::rules::{ flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, flake8_comprehensions, - flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, flake8_import_conventions, - flake8_pytest_style, flake8_quotes, flake8_self, flake8_tidy_imports, flake8_type_checking, - flake8_unused_arguments, isort, mccabe, pep8_naming, pycodestyle, pydocstyle, pyflakes, pylint, + flake8_copyright, flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, + flake8_import_conventions, flake8_pytest_style, flake8_quotes, flake8_self, + flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, isort, mccabe, pep8_naming, + pycodestyle, pydocstyle, pyflakes, pylint, pyupgrade, }; use crate::settings::options::Options; use crate::settings::types::{ @@ -74,13 +75,14 @@ pub struct Configuration { pub flake8_bugbear: Option, pub flake8_builtins: Option, pub flake8_comprehensions: Option, + pub flake8_copyright: Option, pub flake8_errmsg: Option, + pub flake8_gettext: Option, pub flake8_implicit_str_concat: Option, pub flake8_import_conventions: Option, pub flake8_pytest_style: Option, pub flake8_quotes: Option, pub flake8_self: Option, - pub flake8_gettext: Option, pub flake8_tidy_imports: Option, pub flake8_type_checking: Option, pub flake8_unused_arguments: Option, @@ -91,6 +93,7 @@ pub struct Configuration { pub pydocstyle: Option, pub pyflakes: Option, pub pylint: Option, + pub pyupgrade: Option, } impl Configuration { @@ -227,6 +230,7 @@ impl Configuration { flake8_bugbear: options.flake8_bugbear, flake8_builtins: options.flake8_builtins, flake8_comprehensions: options.flake8_comprehensions, + flake8_copyright: options.flake8_copyright, flake8_errmsg: options.flake8_errmsg, flake8_gettext: options.flake8_gettext, flake8_implicit_str_concat: options.flake8_implicit_str_concat, @@ -244,6 +248,7 @@ impl Configuration { pydocstyle: options.pydocstyle, pyflakes: options.pyflakes, pylint: options.pylint, + pyupgrade: options.pyupgrade, }) } @@ -253,7 +258,7 @@ impl Configuration { rule_selections: config .rule_selections .into_iter() - .chain(self.rule_selections.into_iter()) + .chain(self.rule_selections) .collect(), allowed_confusables: self.allowed_confusables.or(config.allowed_confusables), builtins: self.builtins.or(config.builtins), @@ -264,17 +269,17 @@ impl Configuration { extend_exclude: config .extend_exclude .into_iter() - .chain(self.extend_exclude.into_iter()) + .chain(self.extend_exclude) .collect(), extend_include: config .extend_include .into_iter() - .chain(self.extend_include.into_iter()) + .chain(self.extend_include) .collect(), extend_per_file_ignores: config .extend_per_file_ignores .into_iter() - .chain(self.extend_per_file_ignores.into_iter()) + .chain(self.extend_per_file_ignores) .collect(), external: self.external.or(config.external), fix: self.fix.or(config.fix), @@ -305,6 +310,7 @@ impl Configuration { flake8_comprehensions: self .flake8_comprehensions .combine(config.flake8_comprehensions), + flake8_copyright: self.flake8_copyright.combine(config.flake8_copyright), flake8_errmsg: self.flake8_errmsg.combine(config.flake8_errmsg), flake8_gettext: self.flake8_gettext.combine(config.flake8_gettext), flake8_implicit_str_concat: self @@ -330,6 +336,7 @@ impl Configuration { pydocstyle: self.pydocstyle.combine(config.pydocstyle), pyflakes: self.pyflakes.combine(config.pyflakes), pylint: self.pylint.combine(config.pylint), + pyupgrade: self.pyupgrade.combine(config.pyupgrade), } } } diff --git a/crates/ruff/src/settings/defaults.rs b/crates/ruff/src/settings/defaults.rs index aa5007789d..0893fa7543 100644 --- a/crates/ruff/src/settings/defaults.rs +++ b/crates/ruff/src/settings/defaults.rs @@ -1,25 +1,24 @@ -use std::collections::HashSet; - use once_cell::sync::Lazy; use path_absolutize::path_dedot; use regex::Regex; use rustc_hash::FxHashSet; +use std::collections::HashSet; +use super::types::{FilePattern, PythonVersion}; +use super::Settings; use crate::codes::{self, RuleCodePrefix}; use crate::line_width::{LineLength, TabSize}; use crate::registry::Linter; use crate::rule_selector::{prefix_to_selector, RuleSelector}; use crate::rules::{ flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, flake8_comprehensions, - flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, flake8_import_conventions, - flake8_pytest_style, flake8_quotes, flake8_self, flake8_tidy_imports, flake8_type_checking, - flake8_unused_arguments, isort, mccabe, pep8_naming, pycodestyle, pydocstyle, pyflakes, pylint, + flake8_copyright, flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, + flake8_import_conventions, flake8_pytest_style, flake8_quotes, flake8_self, + flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, isort, mccabe, pep8_naming, + pycodestyle, pydocstyle, pyflakes, pylint, pyupgrade, }; use crate::settings::types::FilePatternSet; -use super::types::{FilePattern, PythonVersion}; -use super::Settings; - pub const PREFIXES: &[RuleSelector] = &[ prefix_to_selector(RuleCodePrefix::Pycodestyle(codes::Pycodestyle::E)), RuleSelector::Linter(Linter::Pyflakes), @@ -40,14 +39,18 @@ pub static EXCLUDE: Lazy> = Lazy::new(|| { FilePattern::Builtin(".git"), FilePattern::Builtin(".git-rewrite"), FilePattern::Builtin(".hg"), + FilePattern::Builtin(".ipynb_checkpoints"), FilePattern::Builtin(".mypy_cache"), FilePattern::Builtin(".nox"), FilePattern::Builtin(".pants.d"), + FilePattern::Builtin(".pyenv"), + FilePattern::Builtin(".pytest_cache"), FilePattern::Builtin(".pytype"), FilePattern::Builtin(".ruff_cache"), FilePattern::Builtin(".svn"), FilePattern::Builtin(".tox"), FilePattern::Builtin(".venv"), + FilePattern::Builtin(".vscode"), FilePattern::Builtin("__pypackages__"), FilePattern::Builtin("_build"), FilePattern::Builtin("buck-out"), @@ -95,6 +98,7 @@ impl Default for Settings { flake8_bugbear: flake8_bugbear::settings::Settings::default(), flake8_builtins: flake8_builtins::settings::Settings::default(), flake8_comprehensions: flake8_comprehensions::settings::Settings::default(), + flake8_copyright: flake8_copyright::settings::Settings::default(), flake8_errmsg: flake8_errmsg::settings::Settings::default(), flake8_implicit_str_concat: flake8_implicit_str_concat::settings::Settings::default(), flake8_import_conventions: flake8_import_conventions::settings::Settings::default(), @@ -112,6 +116,7 @@ impl Default for Settings { pydocstyle: pydocstyle::settings::Settings::default(), pyflakes: pyflakes::settings::Settings::default(), pylint: pylint::settings::Settings::default(), + pyupgrade: pyupgrade::settings::Settings::default(), } } } diff --git a/crates/ruff/src/settings/flags.rs b/crates/ruff/src/settings/flags.rs index f7e761ade1..a1ce403194 100644 --- a/crates/ruff/src/settings/flags.rs +++ b/crates/ruff/src/settings/flags.rs @@ -1,19 +1,8 @@ -#[derive(Debug, Copy, Clone, Hash)] +#[derive(Debug, Copy, Clone, Hash, is_macro::Is)] pub enum FixMode { Generate, Apply, Diff, - None, -} - -impl From for FixMode { - fn from(value: bool) -> Self { - if value { - Self::Apply - } else { - Self::None - } - } } #[derive(Debug, Copy, Clone, Hash, result_like::BoolLike)] diff --git a/crates/ruff/src/settings/mod.rs b/crates/ruff/src/settings/mod.rs index f2e2aec2db..b398292771 100644 --- a/crates/ruff/src/settings/mod.rs +++ b/crates/ruff/src/settings/mod.rs @@ -17,9 +17,10 @@ use crate::registry::{Rule, RuleNamespace, RuleSet, INCOMPATIBLE_CODES}; use crate::rule_selector::{RuleSelector, Specificity}; use crate::rules::{ flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, flake8_comprehensions, - flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, flake8_import_conventions, - flake8_pytest_style, flake8_quotes, flake8_self, flake8_tidy_imports, flake8_type_checking, - flake8_unused_arguments, isort, mccabe, pep8_naming, pycodestyle, pydocstyle, pyflakes, pylint, + flake8_copyright, flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, + flake8_import_conventions, flake8_pytest_style, flake8_quotes, flake8_self, + flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, isort, mccabe, pep8_naming, + pycodestyle, pydocstyle, pyflakes, pylint, pyupgrade, }; use crate::settings::configuration::Configuration; use crate::settings::types::{FilePatternSet, PerFileIgnore, PythonVersion, SerializationFormat}; @@ -39,7 +40,7 @@ pub mod types; const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); -#[derive(Debug)] +#[derive(Debug, Default)] pub struct AllSettings { pub cli: CliSettings, pub lib: Settings, @@ -111,10 +112,11 @@ pub struct Settings { pub flake8_bugbear: flake8_bugbear::settings::Settings, pub flake8_builtins: flake8_builtins::settings::Settings, pub flake8_comprehensions: flake8_comprehensions::settings::Settings, + pub flake8_copyright: flake8_copyright::settings::Settings, pub flake8_errmsg: flake8_errmsg::settings::Settings, + pub flake8_gettext: flake8_gettext::settings::Settings, pub flake8_implicit_str_concat: flake8_implicit_str_concat::settings::Settings, pub flake8_import_conventions: flake8_import_conventions::settings::Settings, - pub flake8_gettext: flake8_gettext::settings::Settings, pub flake8_pytest_style: flake8_pytest_style::settings::Settings, pub flake8_quotes: flake8_quotes::settings::Settings, pub flake8_self: flake8_self::settings::Settings, @@ -128,6 +130,7 @@ pub struct Settings { pub pydocstyle: pydocstyle::settings::Settings, pub pyflakes: pyflakes::settings::Settings, pub pylint: pylint::settings::Settings, + pub pyupgrade: pyupgrade::settings::Settings, } impl Settings { @@ -190,50 +193,103 @@ impl Settings { // Plugins flake8_annotations: config .flake8_annotations - .map(Into::into) + .map(flake8_annotations::settings::Settings::from) + .unwrap_or_default(), + flake8_bandit: config + .flake8_bandit + .map(flake8_bandit::settings::Settings::from) + .unwrap_or_default(), + flake8_bugbear: config + .flake8_bugbear + .map(flake8_bugbear::settings::Settings::from) + .unwrap_or_default(), + flake8_builtins: config + .flake8_builtins + .map(flake8_builtins::settings::Settings::from) .unwrap_or_default(), - flake8_bandit: config.flake8_bandit.map(Into::into).unwrap_or_default(), - flake8_bugbear: config.flake8_bugbear.map(Into::into).unwrap_or_default(), - flake8_builtins: config.flake8_builtins.map(Into::into).unwrap_or_default(), flake8_comprehensions: config .flake8_comprehensions - .map(Into::into) + .map(flake8_comprehensions::settings::Settings::from) + .unwrap_or_default(), + flake8_copyright: config + .flake8_copyright + .map(flake8_copyright::settings::Settings::try_from) + .transpose()? + .unwrap_or_default(), + flake8_errmsg: config + .flake8_errmsg + .map(flake8_errmsg::settings::Settings::from) .unwrap_or_default(), - flake8_errmsg: config.flake8_errmsg.map(Into::into).unwrap_or_default(), flake8_implicit_str_concat: config .flake8_implicit_str_concat - .map(Into::into) + .map(flake8_implicit_str_concat::settings::Settings::from) .unwrap_or_default(), flake8_import_conventions: config .flake8_import_conventions - .map(Into::into) + .map(flake8_import_conventions::settings::Settings::from) .unwrap_or_default(), flake8_pytest_style: config .flake8_pytest_style - .map(Into::into) + .map(flake8_pytest_style::settings::Settings::from) + .unwrap_or_default(), + flake8_quotes: config + .flake8_quotes + .map(flake8_quotes::settings::Settings::from) + .unwrap_or_default(), + flake8_self: config + .flake8_self + .map(flake8_self::settings::Settings::from) .unwrap_or_default(), - flake8_quotes: config.flake8_quotes.map(Into::into).unwrap_or_default(), - flake8_self: config.flake8_self.map(Into::into).unwrap_or_default(), flake8_tidy_imports: config .flake8_tidy_imports - .map(Into::into) + .map(flake8_tidy_imports::settings::Settings::from) .unwrap_or_default(), flake8_type_checking: config .flake8_type_checking - .map(Into::into) + .map(flake8_type_checking::settings::Settings::from) .unwrap_or_default(), flake8_unused_arguments: config .flake8_unused_arguments - .map(Into::into) + .map(flake8_unused_arguments::settings::Settings::from) + .unwrap_or_default(), + flake8_gettext: config + .flake8_gettext + .map(flake8_gettext::settings::Settings::from) + .unwrap_or_default(), + isort: config + .isort + .map(isort::settings::Settings::try_from) + .transpose()? + .unwrap_or_default(), + mccabe: config + .mccabe + .map(mccabe::settings::Settings::from) + .unwrap_or_default(), + pep8_naming: config + .pep8_naming + .map(pep8_naming::settings::Settings::try_from) + .transpose()? + .unwrap_or_default(), + pycodestyle: config + .pycodestyle + .map(pycodestyle::settings::Settings::from) + .unwrap_or_default(), + pydocstyle: config + .pydocstyle + .map(pydocstyle::settings::Settings::from) + .unwrap_or_default(), + pyflakes: config + .pyflakes + .map(pyflakes::settings::Settings::from) + .unwrap_or_default(), + pylint: config + .pylint + .map(pylint::settings::Settings::from) + .unwrap_or_default(), + pyupgrade: config + .pyupgrade + .map(pyupgrade::settings::Settings::from) .unwrap_or_default(), - flake8_gettext: config.flake8_gettext.map(Into::into).unwrap_or_default(), - isort: config.isort.map(Into::into).unwrap_or_default(), - mccabe: config.mccabe.map(Into::into).unwrap_or_default(), - pep8_naming: config.pep8_naming.map(Into::into).unwrap_or_default(), - pycodestyle: config.pycodestyle.map(Into::into).unwrap_or_default(), - pydocstyle: config.pydocstyle.map(Into::into).unwrap_or_default(), - pyflakes: config.pyflakes.map(Into::into).unwrap_or_default(), - pylint: config.pylint.map(Into::into).unwrap_or_default(), }) } diff --git a/crates/ruff/src/settings/options.rs b/crates/ruff/src/settings/options.rs index 0a7462cc8f..697b630491 100644 --- a/crates/ruff/src/settings/options.rs +++ b/crates/ruff/src/settings/options.rs @@ -9,9 +9,10 @@ use crate::line_width::{LineLength, TabSize}; use crate::rule_selector::RuleSelector; use crate::rules::{ flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, flake8_comprehensions, - flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, flake8_import_conventions, - flake8_pytest_style, flake8_quotes, flake8_self, flake8_tidy_imports, flake8_type_checking, - flake8_unused_arguments, isort, mccabe, pep8_naming, pycodestyle, pydocstyle, pyflakes, pylint, + flake8_copyright, flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, + flake8_import_conventions, flake8_pytest_style, flake8_quotes, flake8_self, + flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, isort, mccabe, pep8_naming, + pycodestyle, pydocstyle, pyflakes, pylint, pyupgrade, }; use crate::settings::types::{PythonVersion, SerializationFormat, Version}; @@ -287,7 +288,7 @@ pub struct Options { /// re-exported with a redundant alias (e.g., `import os as os`). pub ignore_init_module_imports: Option, #[option( - default = r#"["*.py", "*.pyi"]"#, + default = r#"["*.py", "*.pyi", "**/pyproject.toml"]"#, value_type = "list[str]", example = r#" include = ["*.py"] @@ -296,7 +297,9 @@ pub struct Options { /// A list of file patterns to include when linting. /// /// Inclusion are based on globs, and should be single-path patterns, like - /// `*.pyw`, to include any file with the `.pyw` extension. + /// `*.pyw`, to include any file with the `.pyw` extension. `pyproject.toml` is + /// included here not for configuration but because we lint whether e.g. the + /// `[project]` matches the schema. /// /// For more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax). pub include: Option>, @@ -430,7 +433,7 @@ pub struct Options { pub namespace_packages: Option>, #[option( default = r#""py310""#, - value_type = r#""py37" | "py38" | "py39" | "py310" | "py311""#, + value_type = r#""py37" | "py38" | "py39" | "py310" | "py311" | "py312""#, example = r#" # Always generate Python 3.7-compatible code. target-version = "py37" @@ -495,6 +498,9 @@ pub struct Options { /// Options for the `flake8-comprehensions` plugin. pub flake8_comprehensions: Option, #[option_group] + /// Options for the `flake8-copyright` plugin. + pub flake8_copyright: Option, + #[option_group] /// Options for the `flake8-errmsg` plugin. pub flake8_errmsg: Option, #[option_group] @@ -545,6 +551,9 @@ pub struct Options { #[option_group] /// Options for the `pylint` plugin. pub pylint: Option, + #[option_group] + /// Options for the `pyupgrade` plugin. + pub pyupgrade: Option, // Tables are required to go last. #[option( default = "{}", diff --git a/crates/ruff/src/settings/options_base.rs b/crates/ruff/src/settings/options_base.rs index 2450f73bfb..b15f2b7fd0 100644 --- a/crates/ruff/src/settings/options_base.rs +++ b/crates/ruff/src/settings/options_base.rs @@ -70,7 +70,7 @@ impl OptionGroup { /// /// ### Find a nested options /// - ///```rust + /// ```rust /// # use ruff::settings::options_base::{OptionGroup, OptionEntry, OptionField}; /// /// const ignore_options: [(&'static str, OptionEntry); 2] = [ @@ -134,8 +134,8 @@ impl OptionGroup { } impl<'a> IntoIterator for &'a OptionGroup { - type Item = &'a (&'a str, OptionEntry); type IntoIter = std::slice::Iter<'a, (&'a str, OptionEntry)>; + type Item = &'a (&'a str, OptionEntry); fn into_iter(self) -> Self::IntoIter { self.0.iter() @@ -143,8 +143,8 @@ impl<'a> IntoIterator for &'a OptionGroup { } impl IntoIterator for OptionGroup { - type Item = &'static (&'static str, OptionEntry); type IntoIter = std::slice::Iter<'static, (&'static str, OptionEntry)>; + type Item = &'static (&'static str, OptionEntry); fn into_iter(self) -> Self::IntoIter { self.0.iter() @@ -153,7 +153,7 @@ impl IntoIterator for OptionGroup { impl Display for OptionGroup { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - for (name, _) in self.iter() { + for (name, _) in self { writeln!(f, "{name}")?; } diff --git a/crates/ruff/src/settings/pyproject.rs b/crates/ruff/src/settings/pyproject.rs index 6577dedd5c..6c6df45ed7 100644 --- a/crates/ruff/src/settings/pyproject.rs +++ b/crates/ruff/src/settings/pyproject.rs @@ -152,12 +152,6 @@ mod tests { use crate::codes::{self, RuleCodePrefix}; use crate::line_width::LineLength; - use crate::rules::flake8_quotes::settings::Quote; - use crate::rules::flake8_tidy_imports::settings::{ApiBan, Strictness}; - use crate::rules::{ - flake8_bugbear, flake8_builtins, flake8_errmsg, flake8_import_conventions, - flake8_pytest_style, flake8_quotes, flake8_tidy_imports, mccabe, pep8_naming, - }; use crate::settings::pyproject::{ find_settings_toml, parse_pyproject_toml, Options, Pyproject, Tools, }; @@ -300,95 +294,16 @@ other-attribute = 1 assert_eq!( config, Options { - allowed_confusables: Some(vec!['−', 'ρ', '∗']), line_length: Some(LineLength::from(88)), extend_exclude: Some(vec![ "excluded_file.py".to_string(), "migrations".to_string(), "with_excluded_file/other_excluded_file.py".to_string(), ]), - external: Some(vec!["V101".to_string()]), per_file_ignores: Some(FxHashMap::from_iter([( "__init__.py".to_string(), vec![RuleCodePrefix::Pyflakes(codes::Pyflakes::_401).into()] )])), - flake8_bugbear: Some(flake8_bugbear::settings::Options { - extend_immutable_calls: Some(vec![ - "fastapi.Depends".to_string(), - "fastapi.Query".to_string(), - ]), - }), - flake8_builtins: Some(flake8_builtins::settings::Options { - builtins_ignorelist: Some(vec!["id".to_string(), "dir".to_string(),]), - }), - flake8_errmsg: Some(flake8_errmsg::settings::Options { - max_string_length: Some(20), - }), - flake8_pytest_style: Some(flake8_pytest_style::settings::Options { - fixture_parentheses: Some(false), - parametrize_names_type: Some( - flake8_pytest_style::types::ParametrizeNameType::Csv - ), - parametrize_values_type: Some( - flake8_pytest_style::types::ParametrizeValuesType::Tuple, - ), - parametrize_values_row_type: Some( - flake8_pytest_style::types::ParametrizeValuesRowType::List, - ), - raises_require_match_for: Some(vec![ - "Exception".to_string(), - "TypeError".to_string(), - "KeyError".to_string(), - ]), - raises_extend_require_match_for: Some(vec![ - "requests.RequestException".to_string(), - ]), - mark_parentheses: Some(false), - }), - flake8_implicit_str_concat: None, - flake8_quotes: Some(flake8_quotes::settings::Options { - inline_quotes: Some(Quote::Single), - multiline_quotes: Some(Quote::Double), - docstring_quotes: Some(Quote::Double), - avoid_escape: Some(true), - }), - flake8_tidy_imports: Some(flake8_tidy_imports::options::Options { - ban_relative_imports: Some(Strictness::Parents), - banned_api: Some(FxHashMap::from_iter([ - ( - "cgi".to_string(), - ApiBan { - msg: "The cgi module is deprecated.".to_string() - } - ), - ( - "typing.TypedDict".to_string(), - ApiBan { - msg: "Use typing_extensions.TypedDict instead.".to_string() - } - ) - ])) - }), - flake8_import_conventions: Some(flake8_import_conventions::settings::Options { - aliases: Some(FxHashMap::from_iter([( - "pandas".to_string(), - "pd".to_string(), - )])), - extend_aliases: Some(FxHashMap::from_iter([( - "dask.dataframe".to_string(), - "dd".to_string(), - )])), - banned_aliases: None, - banned_from: None, - }), - mccabe: Some(mccabe::settings::Options { - max_complexity: Some(10), - }), - pep8_naming: Some(pep8_naming::settings::Options { - ignore_names: None, - classmethod_decorators: Some(vec!["pydantic.validator".to_string()]), - staticmethod_decorators: None, - }), ..Options::default() } ); diff --git a/crates/ruff/src/settings/types.rs b/crates/ruff/src/settings/types.rs index 49689d8948..82dc6fe34c 100644 --- a/crates/ruff/src/settings/types.rs +++ b/crates/ruff/src/settings/types.rs @@ -30,6 +30,7 @@ pub enum PythonVersion { Py39, Py310, Py311, + Py312, } impl From for Pep440Version { @@ -47,9 +48,18 @@ impl PythonVersion { Self::Py39 => (3, 9), Self::Py310 => (3, 10), Self::Py311 => (3, 11), + Self::Py312 => (3, 12), } } + pub const fn major(&self) -> u32 { + self.as_tuple().0 + } + + pub const fn minor(&self) -> u32 { + self.as_tuple().1 + } + pub fn get_minimum_supported_version(requires_version: &VersionSpecifiers) -> Option { let mut minimum_version = None; for python_version in PythonVersion::iter() { @@ -214,6 +224,7 @@ impl FromStr for PatternPrefixPair { pub enum SerializationFormat { Text, Json, + JsonLines, Junit, Grouped, Github, @@ -248,3 +259,16 @@ impl Deref for Version { &self.0 } } + +/// Pattern to match an identifier. +/// +/// # Notes +/// +/// [`glob::Pattern`] matches a little differently than we ideally want to. +/// Specifically it uses `**` to match an arbitrary number of subdirectories, +/// luckily this not relevant since identifiers don't contains slashes. +/// +/// For reference pep8-naming uses +/// [`fnmatch`](https://docs.python.org/3/library/fnmatch.html) for +/// pattern matching. +pub type IdentifierPattern = glob::Pattern; diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_all.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_all.snap new file mode 100644 index 0000000000..55b08ffd6f --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_all.snap @@ -0,0 +1,9 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "ParsedFileExemption::try_extract(source)" +--- +Ok( + Some( + All, + ), +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_all_case_insensitive.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_all_case_insensitive.snap new file mode 100644 index 0000000000..55b08ffd6f --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_all_case_insensitive.snap @@ -0,0 +1,9 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "ParsedFileExemption::try_extract(source)" +--- +Ok( + Some( + All, + ), +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_all_no_space.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_all_no_space.snap new file mode 100644 index 0000000000..55b08ffd6f --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_all_no_space.snap @@ -0,0 +1,9 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "ParsedFileExemption::try_extract(source)" +--- +Ok( + Some( + All, + ), +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_codes.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_codes.snap new file mode 100644 index 0000000000..b45c9ca1ae --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__flake8_exemption_codes.snap @@ -0,0 +1,14 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "ParsedFileExemption::try_extract(source)" +--- +Ok( + Some( + Codes( + [ + "F401", + "F841", + ], + ), + ), +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all.snap new file mode 100644 index 0000000000..806987e18b --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all.snap @@ -0,0 +1,13 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "Directive::try_extract(source, TextSize::default())" +--- +Ok( + Some( + All( + All { + range: 0..6, + }, + ), + ), +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_case_insensitive.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_case_insensitive.snap new file mode 100644 index 0000000000..806987e18b --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_case_insensitive.snap @@ -0,0 +1,13 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "Directive::try_extract(source, TextSize::default())" +--- +Ok( + Some( + All( + All { + range: 0..6, + }, + ), + ), +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_leading_comment.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_leading_comment.snap new file mode 100644 index 0000000000..bd4fea2744 --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_leading_comment.snap @@ -0,0 +1,13 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "Directive::try_extract(source, TextSize::default())" +--- +Ok( + Some( + All( + All { + range: 35..41, + }, + ), + ), +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_multi_space.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_multi_space.snap new file mode 100644 index 0000000000..b7fa476cc1 --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_multi_space.snap @@ -0,0 +1,13 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "Directive::try_extract(source, TextSize::default())" +--- +Ok( + Some( + All( + All { + range: 0..7, + }, + ), + ), +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_no_space.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_no_space.snap new file mode 100644 index 0000000000..b5dcc889b8 --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_no_space.snap @@ -0,0 +1,13 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "Directive::try_extract(source, TextSize::default())" +--- +Ok( + Some( + All( + All { + range: 0..5, + }, + ), + ), +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_trailing_comment.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_trailing_comment.snap new file mode 100644 index 0000000000..806987e18b --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_all_trailing_comment.snap @@ -0,0 +1,13 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "Directive::try_extract(source, TextSize::default())" +--- +Ok( + Some( + All( + All { + range: 0..6, + }, + ), + ), +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code.snap new file mode 100644 index 0000000000..bee06c0364 --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code.snap @@ -0,0 +1,16 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "Directive::try_extract(source, TextSize::default())" +--- +Ok( + Some( + Codes( + Codes { + range: 0..12, + codes: [ + "F401", + ], + }, + ), + ), +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_case_insensitive.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_case_insensitive.snap new file mode 100644 index 0000000000..bee06c0364 --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_case_insensitive.snap @@ -0,0 +1,16 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "Directive::try_extract(source, TextSize::default())" +--- +Ok( + Some( + Codes( + Codes { + range: 0..12, + codes: [ + "F401", + ], + }, + ), + ), +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_leading_comment.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_leading_comment.snap new file mode 100644 index 0000000000..4577119d57 --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_leading_comment.snap @@ -0,0 +1,16 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "Directive::try_extract(source, TextSize::default())" +--- +Ok( + Some( + Codes( + Codes { + range: 35..47, + codes: [ + "F401", + ], + }, + ), + ), +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_multi_space.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_multi_space.snap new file mode 100644 index 0000000000..4b6104870f --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_multi_space.snap @@ -0,0 +1,16 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "Directive::try_extract(source, TextSize::default())" +--- +Ok( + Some( + Codes( + Codes { + range: 0..13, + codes: [ + "F401", + ], + }, + ), + ), +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_no_space.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_no_space.snap new file mode 100644 index 0000000000..5f3e8124ff --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_no_space.snap @@ -0,0 +1,16 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "Directive::try_extract(source, TextSize::default())" +--- +Ok( + Some( + Codes( + Codes { + range: 0..10, + codes: [ + "F401", + ], + }, + ), + ), +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_trailing_comment.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_trailing_comment.snap new file mode 100644 index 0000000000..bee06c0364 --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_code_trailing_comment.snap @@ -0,0 +1,16 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "Directive::try_extract(source, TextSize::default())" +--- +Ok( + Some( + Codes( + Codes { + range: 0..12, + codes: [ + "F401", + ], + }, + ), + ), +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes.snap new file mode 100644 index 0000000000..65e429c7af --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes.snap @@ -0,0 +1,17 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "Directive::try_extract(source, TextSize::default())" +--- +Ok( + Some( + Codes( + Codes { + range: 0..18, + codes: [ + "F401", + "F841", + ], + }, + ), + ), +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_case_insensitive.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_case_insensitive.snap new file mode 100644 index 0000000000..65e429c7af --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_case_insensitive.snap @@ -0,0 +1,17 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "Directive::try_extract(source, TextSize::default())" +--- +Ok( + Some( + Codes( + Codes { + range: 0..18, + codes: [ + "F401", + "F841", + ], + }, + ), + ), +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_leading_comment.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_leading_comment.snap new file mode 100644 index 0000000000..9f72b14ab0 --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_leading_comment.snap @@ -0,0 +1,17 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "Directive::try_extract(source, TextSize::default())" +--- +Ok( + Some( + Codes( + Codes { + range: 35..53, + codes: [ + "F401", + "F841", + ], + }, + ), + ), +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_multi_space.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_multi_space.snap new file mode 100644 index 0000000000..c4e9c8643c --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_multi_space.snap @@ -0,0 +1,17 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "Directive::try_extract(source, TextSize::default())" +--- +Ok( + Some( + Codes( + Codes { + range: 0..20, + codes: [ + "F401", + "F841", + ], + }, + ), + ), +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_no_space.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_no_space.snap new file mode 100644 index 0000000000..4c699e253f --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_no_space.snap @@ -0,0 +1,17 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "Directive::try_extract(source, TextSize::default())" +--- +Ok( + Some( + Codes( + Codes { + range: 0..15, + codes: [ + "F401", + "F841", + ], + }, + ), + ), +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_trailing_comment.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_trailing_comment.snap new file mode 100644 index 0000000000..65e429c7af --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_codes_trailing_comment.snap @@ -0,0 +1,17 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "Directive::try_extract(source, TextSize::default())" +--- +Ok( + Some( + Codes( + Codes { + range: 0..18, + codes: [ + "F401", + "F841", + ], + }, + ), + ), +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_invalid_codes.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_invalid_codes.snap new file mode 100644 index 0000000000..deb7795314 --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_invalid_codes.snap @@ -0,0 +1,7 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "Directive::try_extract(source, TextSize::default())" +--- +Err( + MissingCodes, +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_leading_space.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_leading_space.snap new file mode 100644 index 0000000000..47f626e011 --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_leading_space.snap @@ -0,0 +1,16 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "Directive::try_extract(source, TextSize::default())" +--- +Ok( + Some( + Codes( + Codes { + range: 4..16, + codes: [ + "F401", + ], + }, + ), + ), +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_trailing_space.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_trailing_space.snap new file mode 100644 index 0000000000..bee06c0364 --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__noqa_trailing_space.snap @@ -0,0 +1,16 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "Directive::try_extract(source, TextSize::default())" +--- +Ok( + Some( + Codes( + Codes { + range: 0..12, + codes: [ + "F401", + ], + }, + ), + ), +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__ruff_exemption_all.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__ruff_exemption_all.snap new file mode 100644 index 0000000000..55b08ffd6f --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__ruff_exemption_all.snap @@ -0,0 +1,9 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "ParsedFileExemption::try_extract(source)" +--- +Ok( + Some( + All, + ), +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__ruff_exemption_all_case_insensitive.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__ruff_exemption_all_case_insensitive.snap new file mode 100644 index 0000000000..55b08ffd6f --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__ruff_exemption_all_case_insensitive.snap @@ -0,0 +1,9 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "ParsedFileExemption::try_extract(source)" +--- +Ok( + Some( + All, + ), +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__ruff_exemption_all_no_space.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__ruff_exemption_all_no_space.snap new file mode 100644 index 0000000000..55b08ffd6f --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__ruff_exemption_all_no_space.snap @@ -0,0 +1,9 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "ParsedFileExemption::try_extract(source)" +--- +Ok( + Some( + All, + ), +) diff --git a/crates/ruff/src/snapshots/ruff__noqa__tests__ruff_exemption_codes.snap b/crates/ruff/src/snapshots/ruff__noqa__tests__ruff_exemption_codes.snap new file mode 100644 index 0000000000..b45c9ca1ae --- /dev/null +++ b/crates/ruff/src/snapshots/ruff__noqa__tests__ruff_exemption_codes.snap @@ -0,0 +1,14 @@ +--- +source: crates/ruff/src/noqa.rs +expression: "ParsedFileExemption::try_extract(source)" +--- +Ok( + Some( + Codes( + [ + "F401", + "F841", + ], + ), + ), +) diff --git a/crates/ruff/src/source_kind.rs b/crates/ruff/src/source_kind.rs new file mode 100644 index 0000000000..ab63e89c28 --- /dev/null +++ b/crates/ruff/src/source_kind.rs @@ -0,0 +1,26 @@ +use crate::jupyter::Notebook; + +#[derive(Clone, Debug, PartialEq, is_macro::Is)] +pub enum SourceKind { + Python(String), + Jupyter(Notebook), +} + +impl SourceKind { + /// Return the source content. + pub fn content(&self) -> &str { + match self { + SourceKind::Python(content) => content, + SourceKind::Jupyter(notebook) => notebook.content(), + } + } + + /// Return the [`Notebook`] if the source kind is [`SourceKind::Jupyter`]. + pub fn notebook(&self) -> Option<&Notebook> { + if let Self::Jupyter(notebook) = self { + Some(notebook) + } else { + None + } + } +} diff --git a/crates/ruff/src/test.rs b/crates/ruff/src/test.rs index 60a4eb9739..1c2eb7aefb 100644 --- a/crates/ruff/src/test.rs +++ b/crates/ruff/src/test.rs @@ -13,14 +13,29 @@ use rustpython_parser::lexer::LexResult; use ruff_diagnostics::{AutofixKind, Diagnostic}; use ruff_python_ast::source_code::{Indexer, Locator, SourceFileBuilder, Stylist}; -use crate::autofix::fix_file; +use crate::autofix::{fix_file, FixResult}; use crate::directives; +#[cfg(not(fuzzing))] +use crate::jupyter::Notebook; use crate::linter::{check_path, LinterResult}; use crate::message::{Emitter, EmitterContext, Message, TextEmitter}; use crate::packaging::detect_package_root; use crate::registry::AsRule; use crate::rules::pycodestyle::rules::syntax_error; use crate::settings::{flags, Settings}; +use crate::source_kind::SourceKind; + +#[cfg(not(fuzzing))] +pub(crate) fn read_jupyter_notebook(path: &Path) -> Result { + let path = test_resource_path("fixtures/jupyter").join(path); + Notebook::read(&path).map_err(|err| { + anyhow::anyhow!( + "Failed to read notebook file `{}`: {:?}", + path.display(), + err + ) + }) +} #[cfg(not(fuzzing))] pub(crate) fn test_resource_path(path: impl AsRef) -> std::path::PathBuf { @@ -32,14 +47,39 @@ pub(crate) fn test_resource_path(path: impl AsRef) -> std::path::PathBuf { pub(crate) fn test_path(path: impl AsRef, settings: &Settings) -> Result> { let path = test_resource_path("fixtures").join(path); let contents = std::fs::read_to_string(&path)?; - Ok(test_contents(&contents, &path, settings)) + Ok(test_contents( + &mut SourceKind::Python(contents), + &path, + settings, + )) +} + +#[cfg(not(fuzzing))] +pub(crate) fn test_notebook_path( + path: impl AsRef, + expected: impl AsRef, + settings: &Settings, +) -> Result<(Vec, SourceKind)> { + let mut source_kind = SourceKind::Jupyter(read_jupyter_notebook(path.as_ref())?); + let messages = test_contents(&mut source_kind, path.as_ref(), settings); + let expected_notebook = read_jupyter_notebook(expected.as_ref())?; + if let SourceKind::Jupyter(notebook) = &source_kind { + assert_eq!(notebook.cell_offsets(), expected_notebook.cell_offsets()); + assert_eq!(notebook.index(), expected_notebook.index()); + assert_eq!(notebook.content(), expected_notebook.content()); + }; + Ok((messages, source_kind)) } /// Run [`check_path`] on a snippet of Python code. pub fn test_snippet(contents: &str, settings: &Settings) -> Vec { let path = Path::new(""); let contents = dedent(contents); - test_contents(&contents, path, settings) + test_contents( + &mut SourceKind::Python(contents.to_string()), + path, + settings, + ) } thread_local! { @@ -56,9 +96,10 @@ pub(crate) fn max_iterations() -> usize { /// A convenient wrapper around [`check_path`], that additionally /// asserts that autofixes converge after a fixed number of iterations. -fn test_contents(contents: &str, path: &Path, settings: &Settings) -> Vec { - let tokens: Vec = ruff_rustpython::tokenize(contents); - let locator = Locator::new(contents); +fn test_contents(source_kind: &mut SourceKind, path: &Path, settings: &Settings) -> Vec { + let contents = source_kind.content().to_string(); + let tokens: Vec = ruff_rustpython::tokenize(&contents); + let locator = Locator::new(&contents); let stylist = Stylist::from_tokens(&tokens, &locator); let indexer = Indexer::from_tokens(&tokens, &locator); let directives = directives::extract_directives( @@ -81,6 +122,7 @@ fn test_contents(contents: &str, path: &Path, settings: &Settings) -> Vec Vec Vec = ruff_rustpython::tokenize(&fixed_contents); let locator = Locator::new(&fixed_contents); let stylist = Stylist::from_tokens(&tokens, &locator); @@ -133,17 +184,18 @@ fn test_contents(contents: &str, path: &Path, settings: &Settings) -> Vec, file_path: &Path, source: &str) -> String { - let source_file = SourceFileBuilder::new( - file_path.file_name().unwrap().to_string_lossy().as_ref(), - source, - ) - .finish(); +fn print_diagnostics( + diagnostics: Vec, + file_path: &Path, + source: &str, + source_kind: &SourceKind, +) -> String { + let filename = file_path.file_name().unwrap().to_string_lossy(); + let source_file = SourceFileBuilder::new(filename.as_ref(), source).finish(); let messages: Vec<_> = diagnostics .into_iter() @@ -213,7 +267,35 @@ fn print_diagnostics(diagnostics: Vec, file_path: &Path, source: &st }) .collect(); - print_messages(&messages) + if source_kind.is_jupyter() { + print_jupyter_messages(&messages, &filename, source_kind) + } else { + print_messages(&messages) + } +} + +pub(crate) fn print_jupyter_messages( + messages: &[Message], + filename: &str, + source_kind: &SourceKind, +) -> String { + let mut output = Vec::new(); + + TextEmitter::default() + .with_show_fix_status(true) + .with_show_fix_diff(true) + .with_show_source(true) + .emit( + &mut output, + messages, + &EmitterContext::new(&FxHashMap::from_iter([( + filename.to_string(), + source_kind.clone(), + )])), + ) + .unwrap(); + + String::from_utf8(output).unwrap() } pub(crate) fn print_messages(messages: &[Message]) -> String { @@ -235,6 +317,13 @@ pub(crate) fn print_messages(messages: &[Message]) -> String { #[macro_export] macro_rules! assert_messages { + ($value:expr, $path:expr, $source_kind:expr) => {{ + insta::with_settings!({ omit_expression => true }, { + insta::assert_snapshot!( + $crate::test::print_jupyter_messages(&$value, &$path, &$source_kind) + ); + }); + }}; ($value:expr, @$snapshot:literal) => {{ insta::with_settings!({ omit_expression => true }, { insta::assert_snapshot!($crate::test::print_messages(&$value), $snapshot); diff --git a/crates/ruff/src/upstream_categories.rs b/crates/ruff/src/upstream_categories.rs new file mode 100644 index 0000000000..a00bfff3f0 --- /dev/null +++ b/crates/ruff/src/upstream_categories.rs @@ -0,0 +1,79 @@ +//! This module should probably not exist in this shape or form. +use crate::codes::Rule; +use crate::registry::Linter; + +#[derive(Hash, Eq, PartialEq, Copy, Clone, Debug)] +pub struct UpstreamCategoryAndPrefix { + pub category: &'static str, + pub prefix: &'static str, +} + +const PLC: UpstreamCategoryAndPrefix = UpstreamCategoryAndPrefix { + category: "Convention", + prefix: "PLC", +}; + +const PLE: UpstreamCategoryAndPrefix = UpstreamCategoryAndPrefix { + category: "Error", + prefix: "PLE", +}; + +const PLR: UpstreamCategoryAndPrefix = UpstreamCategoryAndPrefix { + category: "Refactor", + prefix: "PLR", +}; + +const PLW: UpstreamCategoryAndPrefix = UpstreamCategoryAndPrefix { + category: "Warning", + prefix: "PLW", +}; + +const E: UpstreamCategoryAndPrefix = UpstreamCategoryAndPrefix { + category: "Error", + prefix: "E", +}; + +const W: UpstreamCategoryAndPrefix = UpstreamCategoryAndPrefix { + category: "Warning", + prefix: "W", +}; + +impl Rule { + pub fn upstream_category(&self, linter: &Linter) -> Option { + let code = linter.code_for_rule(*self).unwrap(); + match linter { + Linter::Pycodestyle => { + if code.starts_with('E') { + Some(E) + } else if code.starts_with('W') { + Some(W) + } else { + None + } + } + Linter::Pylint => { + if code.starts_with("PLC") { + Some(PLC) + } else if code.starts_with("PLE") { + Some(PLE) + } else if code.starts_with("PLR") { + Some(PLR) + } else if code.starts_with("PLW") { + Some(PLW) + } else { + None + } + } + _ => None, + } + } +} +impl Linter { + pub const fn upstream_categories(&self) -> Option<&'static [UpstreamCategoryAndPrefix]> { + match self { + Linter::Pycodestyle => Some(&[E, W]), + Linter::Pylint => Some(&[PLC, PLE, PLR, PLW]), + _ => None, + } + } +} diff --git a/crates/ruff_benchmark/Cargo.toml b/crates/ruff_benchmark/Cargo.toml index f3d63336e0..56617c41d6 100644 --- a/crates/ruff_benchmark/Cargo.toml +++ b/crates/ruff_benchmark/Cargo.toml @@ -1,13 +1,15 @@ [package] name = "ruff_benchmark" version = "0.0.0" -publish = false -edition.workspace = true -authors.workspace = true -homepage.workspace = true -documentation.workspace = true -repository.workspace = true description = "Ruff Micro-benchmarks" +publish = false +authors = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } [lib] bench = false diff --git a/crates/ruff_benchmark/README.md b/crates/ruff_benchmark/README.md index 3151b03b95..02a683b19e 100644 --- a/crates/ruff_benchmark/README.md +++ b/crates/ruff_benchmark/README.md @@ -1,91 +1,5 @@ -# Ruff Micro-benchmarks +# Ruff Benchmarks -Benchmarks for the different Ruff-tools. +The `ruff_benchmark` crate benchmarks the linter and the formatter on individual files. -## Run Benchmark - -You can run the benchmarks with - -```shell -cargo benchmark -``` - -## Benchmark driven Development - -You can use `--save-baseline=` to store an initial baseline benchmark (e.g. on `main`) and -then use `--benchmark=` to compare against that benchmark. Criterion will print a message -telling you if the benchmark improved/regressed compared to that baseline. - -```shell -# Run once on your "baseline" code -cargo benchmark --save-baseline=main - -# Then iterate with -cargo benchmark --baseline=main -``` - -## PR Summary - -You can use `--save-baseline` and `critcmp` to get a pretty comparison between two recordings. -This is useful to illustrate the improvements of a PR. - -```shell -# On main -cargo benchmark --save-baseline=main - -# After applying your changes -cargo benchmark --save-baseline=pr - -critcmp main pr -``` - -You must install [`critcmp`](https://github.com/BurntSushi/critcmp) for the comparison. - -```bash -cargo install critcmp -``` - -## Tips - -- Use `cargo benchmark ` to only run specific benchmarks. For example: `cargo benchmark linter/pydantic` - to only run the pydantic tests. -- Use `cargo benchmark --quiet` for a more cleaned up output (without statistical relevance) -- Use `cargo benchmark --quick` to get faster results (more prone to noise) - -## Profiling - -### Linux - -Install `perf` and build `ruff_benchmark` with the `release-debug` profile and then run it with perf - -```shell -cargo bench -p ruff_benchmark --no-run --profile=release-debug && perf record -g -F 9999 cargo bench -p ruff_benchmark --profile=release-debug -- --profile-time=1 -``` - -Then convert the recorded profile - -```shell -perf script -F +pid > /tmp/test.perf -``` - -You can now view the converted file with [firefox profiler](https://profiler.firefox.com/) - -You can find a more in-depth guide [here](https://profiler.firefox.com/docs/#/./guide-perf-profiling) - -### Mac - -Install [`cargo-instruments`](https://crates.io/crates/cargo-instruments): - -```shell -cargo install cargo-instruments -``` - -Then run the profiler with - -```shell -cargo instruments -t time --bench linter --profile release-debug -p ruff_benchmark -- --profile-time=1 -``` - -- `-t`: Specifies what to profile. Useful options are `time` to profile the wall time and `alloc` - for profiling the allocations. -- You may want to pass an additional filter to run a single test file +See [CONTRIBUTING.md](../../CONTRIBUTING.md) on how to use these benchmarks. diff --git a/crates/ruff_benchmark/benches/formatter.rs b/crates/ruff_benchmark/benches/formatter.rs index 15698dbba0..74688bed09 100644 --- a/crates/ruff_benchmark/benches/formatter.rs +++ b/crates/ruff_benchmark/benches/formatter.rs @@ -1,6 +1,6 @@ use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; use ruff_benchmark::{TestCase, TestCaseSpeed, TestFile, TestFileDownloadError}; -use ruff_python_formatter::format_module; +use ruff_python_formatter::{format_module, PyFormatOptions}; use std::time::Duration; #[cfg(target_os = "windows")] @@ -50,7 +50,10 @@ fn benchmark_formatter(criterion: &mut Criterion) { BenchmarkId::from_parameter(case.name()), &case, |b, case| { - b.iter(|| format_module(case.code()).expect("Formatting to succeed")); + b.iter(|| { + format_module(case.code(), PyFormatOptions::default()) + .expect("Formatting to succeed") + }); }, ); } diff --git a/crates/ruff_cache/Cargo.toml b/crates/ruff_cache/Cargo.toml index b38e4cba04..b166bbf36a 100644 --- a/crates/ruff_cache/Cargo.toml +++ b/crates/ruff_cache/Cargo.toml @@ -2,11 +2,17 @@ name = "ruff_cache" version = "0.0.0" publish = false +authors = { workspace = true } edition = { workspace = true } rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } [dependencies] itertools = { workspace = true } +glob = { workspace = true } globset = { workspace = true } regex = { workspace = true } filetime = { workspace = true } diff --git a/crates/ruff_cache/src/cache_key.rs b/crates/ruff_cache/src/cache_key.rs index c05bf48ead..e015112bda 100644 --- a/crates/ruff_cache/src/cache_key.rs +++ b/crates/ruff_cache/src/cache_key.rs @@ -5,6 +5,7 @@ use std::hash::{Hash, Hasher}; use std::ops::{Deref, DerefMut}; use std::path::{Path, PathBuf}; +use glob::Pattern; use itertools::Itertools; use regex::Regex; @@ -23,6 +24,7 @@ impl CacheKeyHasher { impl Deref for CacheKeyHasher { type Target = DefaultHasher; + fn deref(&self) -> &Self::Target { &self.inner } @@ -375,3 +377,9 @@ impl CacheKey for Regex { self.as_str().cache_key(state); } } + +impl CacheKey for Pattern { + fn cache_key(&self, state: &mut CacheKeyHasher) { + self.as_str().cache_key(state); + } +} diff --git a/crates/ruff_cache/src/lib.rs b/crates/ruff_cache/src/lib.rs index 3d17f88336..c07c415134 100644 --- a/crates/ruff_cache/src/lib.rs +++ b/crates/ruff_cache/src/lib.rs @@ -8,8 +8,7 @@ pub mod globset; pub const CACHE_DIR_NAME: &str = ".ruff_cache"; -/// Return the cache directory for a given project root. Defers to the -/// `RUFF_CACHE_DIR` environment variable, if set. +/// Return the cache directory for a given project root. pub fn cache_dir(project_root: &Path) -> PathBuf { project_root.join(CACHE_DIR_NAME) } diff --git a/crates/ruff_cli/Cargo.toml b/crates/ruff_cli/Cargo.toml index b45d8557e4..ca58bdcb42 100644 --- a/crates/ruff_cli/Cargo.toml +++ b/crates/ruff_cli/Cargo.toml @@ -1,14 +1,15 @@ [package] name = "ruff_cli" -version = "0.0.272" -authors = ["Charlie Marsh "] +version = "0.0.278" +publish = false +authors = { workspace = true } edition = { workspace = true } rust-version = { workspace = true } -documentation = "https://github.com/astral-sh/ruff" -homepage = "https://github.com/astral-sh/ruff" -repository = "https://github.com/astral-sh/ruff" +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } readme = "../../README.md" -license = "MIT" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -33,7 +34,6 @@ ruff_textwrap = { path = "../ruff_textwrap" } annotate-snippets = { version = "0.9.1", features = ["color"] } anyhow = { workspace = true } argfile = { version = "0.1.5" } -atty = { version = "0.2.14" } bincode = { version = "1.3.3" } bitflags = { workspace = true } cachedir = { version = "0.3.0" } @@ -46,6 +46,7 @@ filetime = { workspace = true } glob = { workspace = true } ignore = { workspace = true } itertools = { workspace = true } +itoa = { version = "1.0.6" } log = { workspace = true } notify = { version = "5.1.0" } path-absolutize = { workspace = true, features = ["once_cell_cache"] } @@ -65,9 +66,6 @@ wild = { version = "2" } assert_cmd = { version = "2.0.8" } ureq = { version = "2.6.2", features = [] } -[features] -jupyter_notebook = ["ruff/jupyter_notebook"] - [target.'cfg(target_os = "windows")'.dependencies] mimalloc = "0.1.34" diff --git a/crates/ruff_cli/resources/test/fixtures/cache_mutable/.gitignore b/crates/ruff_cli/resources/test/fixtures/cache_mutable/.gitignore new file mode 100644 index 0000000000..92d4d36a24 --- /dev/null +++ b/crates/ruff_cli/resources/test/fixtures/cache_mutable/.gitignore @@ -0,0 +1,2 @@ +# Modified by the cache tests. +source.py diff --git a/crates/ruff_cli/resources/test/fixtures/cache_mutable/source.py b/crates/ruff_cli/resources/test/fixtures/cache_mutable/source.py new file mode 100644 index 0000000000..7e397f06e5 --- /dev/null +++ b/crates/ruff_cli/resources/test/fixtures/cache_mutable/source.py @@ -0,0 +1,4 @@ +# NOTE: sync with cache::invalidation test +a = 1 + +__all__ = list(["a", "b"]) diff --git a/crates/ruff_cli/resources/test/fixtures/cache_remove_old_files/source.py b/crates/ruff_cli/resources/test/fixtures/cache_remove_old_files/source.py new file mode 100644 index 0000000000..7e397f06e5 --- /dev/null +++ b/crates/ruff_cli/resources/test/fixtures/cache_remove_old_files/source.py @@ -0,0 +1,4 @@ +# NOTE: sync with cache::invalidation test +a = 1 + +__all__ = list(["a", "b"]) diff --git a/crates/ruff_cli/src/args.rs b/crates/ruff_cli/src/args.rs index 09662c8272..aa299cad60 100644 --- a/crates/ruff_cli/src/args.rs +++ b/crates/ruff_cli/src/args.rs @@ -35,11 +35,17 @@ pub struct Args { pub enum Command { /// Run Ruff on the given files or directories (default). Check(CheckArgs), - /// Explain a rule. + /// Explain a rule (or all rules). #[clap(alias = "--explain")] + #[command(group = clap::ArgGroup::new("selector").multiple(false).required(true))] Rule { - #[arg(value_parser=Rule::from_code)] - rule: Rule, + /// Rule to explain + #[arg(value_parser=Rule::from_code, group = "selector")] + rule: Option, + + /// Explain all rules + #[arg(long, conflicts_with = "rule", group = "selector")] + all: bool, /// Output format #[arg(long, value_enum, default_value = "text")] @@ -68,7 +74,8 @@ pub enum Command { }, } -#[derive(Debug, clap::Args)] +// The `Parser` derive is for ruff_dev, for ruff_cli `Args` would be sufficient +#[derive(Clone, Debug, clap::Parser)] #[allow(clippy::struct_excessive_bools, clippy::module_name_repetitions)] pub struct CheckArgs { /// List of files or directories to check. @@ -107,6 +114,9 @@ pub struct CheckArgs { /// Output serialization format for violations. #[arg(long, value_enum, env = "RUFF_FORMAT")] pub format: Option, + /// Specify file to write the linter output to (default: stdout). + #[arg(short, long)] + pub output_file: Option, /// The minimum Python version that should be supported. #[arg(long, value_enum)] pub target_version: Option, @@ -399,6 +409,7 @@ impl CheckArgs { ignore_noqa: self.ignore_noqa, isolated: self.isolated, no_cache: self.no_cache, + output_file: self.output_file, show_files: self.show_files, show_settings: self.show_settings, statistics: self.statistics, @@ -465,6 +476,7 @@ pub struct Arguments { pub ignore_noqa: bool, pub isolated: bool, pub no_cache: bool, + pub output_file: Option, pub show_files: bool, pub show_settings: bool, pub statistics: bool, diff --git a/crates/ruff_cli/src/cache.rs b/crates/ruff_cli/src/cache.rs index 0147d9ccb5..fe614e7f8a 100644 --- a/crates/ruff_cli/src/cache.rs +++ b/crates/ruff_cli/src/cache.rs @@ -1,171 +1,314 @@ -use std::cell::RefCell; -use std::fs; +use std::collections::HashMap; +use std::fs::{self, File}; use std::hash::Hasher; -use std::io::Write; -#[cfg(unix)] -use std::os::unix::fs::PermissionsExt; -use std::path::Path; +use std::io::{self, BufReader, BufWriter, Write}; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::Mutex; +use std::time::{Duration, SystemTime}; -use anyhow::Result; -use filetime::FileTime; -use log::error; -use path_absolutize::Absolutize; -use ruff_text_size::{TextRange, TextSize}; -use serde::ser::{SerializeSeq, SerializeStruct}; -use serde::{Deserialize, Serialize, Serializer}; +use anyhow::{Context, Result}; +use serde::{Deserialize, Serialize}; use ruff::message::Message; -use ruff::settings::{AllSettings, Settings}; +use ruff::settings::Settings; +use ruff::warn_user; use ruff_cache::{CacheKey, CacheKeyHasher}; use ruff_diagnostics::{DiagnosticKind, Fix}; use ruff_python_ast::imports::ImportMap; use ruff_python_ast::source_code::SourceFileBuilder; +use ruff_text_size::{TextRange, TextSize}; -const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); +use crate::diagnostics::Diagnostics; -/// Vec storing all source files. The tuple is (filename, source code). -type Files<'a> = Vec<(&'a str, &'a str)>; -type FilesBuf = Vec<(String, String)>; +/// Maximum duration for which we keep a file in cache that hasn't been seen. +const MAX_LAST_SEEN: Duration = Duration::from_secs(30 * 24 * 60 * 60); // 30 days. -struct CheckResultRef<'a> { - imports: &'a ImportMap, - messages: &'a [Message], +/// [`Path`] that is relative to the package root in [`PackageCache`]. +pub(crate) type RelativePath = Path; +/// [`PathBuf`] that is relative to the package root in [`PackageCache`]. +pub(crate) type RelativePathBuf = PathBuf; + +/// Cache. +/// +/// `Cache` holds everything required to display the diagnostics for a single +/// package. The on-disk representation is represented in [`PackageCache`] (and +/// related) types. +/// +/// This type manages the cache file, reading it from disk and writing it back +/// to disk (if required). +#[derive(Debug)] +pub(crate) struct Cache { + /// Location of the cache. + path: PathBuf, + /// Package cache read from disk. + package: PackageCache, + /// Changes made compared to the (current) `package`. + /// + /// Files that are linted, but are not in `package.files` or are in + /// `package.files` but are outdated. This gets merged with `package.files` + /// when the cache is written back to disk in [`Cache::store`]. + new_files: Mutex>, + /// The "current" timestamp used as cache for the updates of + /// [`FileCache::last_seen`] + last_seen_cache: u64, } -impl Serialize for CheckResultRef<'_> { - fn serialize(&self, serializer: S) -> std::result::Result - where - S: Serializer, - { - let mut s = serializer.serialize_struct("CheckResultRef", 3)?; +impl Cache { + /// Open or create a new cache. + /// + /// `cache_dir` is considered the root directory of the cache, which can be + /// local to the project, global or otherwise set by the user. + /// + /// `package_root` is the path to root of the package that is contained + /// within this cache and must be canonicalized (to avoid considering `./` + /// and `../project` being different). + /// + /// Finally `settings` is used to ensure we don't open a cache for different + /// settings. + pub(crate) fn open(cache_dir: &Path, package_root: PathBuf, settings: &Settings) -> Cache { + debug_assert!(package_root.is_absolute(), "package root not canonicalized"); - s.serialize_field("imports", &self.imports)?; + let mut buf = itoa::Buffer::new(); + let key = Path::new(buf.format(cache_key(&package_root, settings))); + let path = PathBuf::from_iter([cache_dir, Path::new("content"), key]); - let serialize_messages = SerializeMessages { - messages: self.messages, - files: RefCell::default(), + let file = match File::open(&path) { + Ok(file) => file, + Err(err) if err.kind() == io::ErrorKind::NotFound => { + // No cache exist yet, return an empty cache. + return Cache::empty(path, package_root); + } + Err(err) => { + warn_user!("Failed to open cache file '{}': {err}", path.display()); + return Cache::empty(path, package_root); + } }; - s.serialize_field("messages", &serialize_messages)?; + let mut package: PackageCache = match bincode::deserialize_from(BufReader::new(file)) { + Ok(package) => package, + Err(err) => { + warn_user!("Failed parse cache file '{}': {err}", path.display()); + return Cache::empty(path, package_root); + } + }; - let files = serialize_messages.files.take(); - - s.serialize_field("files", &files)?; - - s.end() + // Sanity check. + if package.package_root != package_root { + warn_user!( + "Different package root in cache: expected '{}', got '{}'", + package_root.display(), + package.package_root.display(), + ); + package.files.clear(); + } + Cache::new(path, package) } -} -struct SerializeMessages<'a> { - messages: &'a [Message], - files: RefCell>, -} + /// Create an empty `Cache`. + fn empty(path: PathBuf, package_root: PathBuf) -> Cache { + let package = PackageCache { + package_root, + files: HashMap::new(), + }; + Cache::new(path, package) + } -impl Serialize for SerializeMessages<'_> { - fn serialize(&self, serializer: S) -> std::result::Result - where - S: Serializer, - { - let mut s = serializer.serialize_seq(Some(self.messages.len()))?; - let mut files = self.files.borrow_mut(); + #[allow(clippy::cast_possible_truncation)] + fn new(path: PathBuf, package: PackageCache) -> Cache { + Cache { + path, + package, + new_files: Mutex::new(HashMap::new()), + // SAFETY: this will be truncated to the year ~2554 (so don't use + // this code after that!). + last_seen_cache: SystemTime::UNIX_EPOCH.elapsed().unwrap().as_millis() as u64, + } + } - for message in self.messages { - // Using a Vec instead of a HashMap because the cache is per file and the large majority of - // files have exactly one source file. - let file_id = if let Some(position) = files - .iter() - .position(|(filename, _)| *filename == message.filename()) - { - position - } else { - let index = files.len(); - files.push((message.filename(), message.file.source_text())); - index - }; - - s.serialize_element(&SerializeMessage { message, file_id })?; + /// Store the cache to disk, if it has been changed. + #[allow(clippy::cast_possible_truncation)] + pub(crate) fn store(mut self) -> Result<()> { + let new_files = self.new_files.into_inner().unwrap(); + if new_files.is_empty() { + // No changes made, no need to write the same cache file back to + // disk. + return Ok(()); } - s.end() + // Remove cached files that we haven't seen in a while. + let now = self.last_seen_cache; + self.package.files.retain(|_, file| { + // SAFETY: this will be truncated to the year ~2554. + (now - *file.last_seen.get_mut()) <= MAX_LAST_SEEN.as_millis() as u64 + }); + + // Apply any changes made and keep track of when we last saw files. + self.package.files.extend(new_files); + + let file = File::create(&self.path) + .with_context(|| format!("Failed to create cache file '{}'", self.path.display()))?; + let writer = BufWriter::new(file); + bincode::serialize_into(writer, &self.package).with_context(|| { + format!( + "Failed to serialise cache to file '{}'", + self.path.display() + ) + }) + } + + /// Returns the relative path based on `path` and the package root. + /// + /// Returns `None` if `path` is not within the package. + pub(crate) fn relative_path<'a>(&self, path: &'a Path) -> Option<&'a RelativePath> { + path.strip_prefix(&self.package.package_root).ok() + } + + /// Get the cached results for a single file at relative `path`. This uses + /// `file_last_modified` to determine if the results are still accurate + /// (i.e. if the file hasn't been modified since the cached run). + /// + /// This returns `None` if `file_last_modified` differs from the cached + /// timestamp or if the cache doesn't contain results for the file. + pub(crate) fn get( + &self, + path: &RelativePath, + file_last_modified: SystemTime, + ) -> Option<&FileCache> { + let file = self.package.files.get(path)?; + + // Make sure the file hasn't changed since the cached run. + if file.last_modified != file_last_modified { + return None; + } + + file.last_seen.store(self.last_seen_cache, Ordering::SeqCst); + + Some(file) + } + + /// Add or update a file cache at `path` relative to the package root. + pub(crate) fn update( + &self, + path: RelativePathBuf, + last_modified: SystemTime, + messages: &[Message], + imports: &ImportMap, + ) { + let source = if let Some(msg) = messages.first() { + msg.file.source_text().to_owned() + } else { + String::new() // No messages, no need to keep the source! + }; + + let messages = messages + .iter() + .map(|msg| { + // Make sure that all message use the same source file. + assert!( + msg.file == messages.first().unwrap().file, + "message uses a different source file" + ); + CacheMessage { + kind: msg.kind.clone(), + range: msg.range, + fix: msg.fix.clone(), + noqa_offset: msg.noqa_offset, + } + }) + .collect(); + + let file = FileCache { + last_modified, + last_seen: AtomicU64::new(self.last_seen_cache), + imports: imports.clone(), + messages, + source, + }; + self.new_files.lock().unwrap().insert(path, file); } } -struct SerializeMessage<'a> { - message: &'a Message, - file_id: usize, +/// On disk representation of a cache of a package. +#[derive(Deserialize, Debug, Serialize)] +struct PackageCache { + /// Path to the root of the package. + /// + /// Usually this is a directory, but it can also be a single file in case of + /// single file "packages", e.g. scripts. + package_root: PathBuf, + /// Mapping of source file path to it's cached data. + files: HashMap, } -impl Serialize for SerializeMessage<'_> { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let Message { - kind, - range, - fix, - // Serialized manually for all files - file: _, - noqa_offset: noqa_row, - } = self.message; +/// On disk representation of the cache per source file. +#[derive(Deserialize, Debug, Serialize)] +pub(crate) struct FileCache { + /// Timestamp when the file was last modified before the (cached) check. + last_modified: SystemTime, + /// Timestamp when we last linted this file. + /// + /// Represented as the number of milliseconds since Unix epoch. This will + /// break in 1970 + ~584 years (~2554). + last_seen: AtomicU64, + /// Imports made. + imports: ImportMap, + /// Diagnostic messages. + messages: Vec, + /// Source code of the file. + /// + /// # Notes + /// + /// This will be empty if `messages` is empty. + source: String, +} - let mut s = serializer.serialize_struct("Message", 5)?; - - s.serialize_field("kind", &kind)?; - s.serialize_field("range", &range)?; - s.serialize_field("fix", &fix)?; - s.serialize_field("file_id", &self.file_id)?; - s.serialize_field("noqa_row", &noqa_row)?; - - s.end() +impl FileCache { + /// Convert the file cache into `Diagnostics`, using `path` as file name. + pub(crate) fn as_diagnostics(&self, path: &Path) -> Diagnostics { + let messages = if self.messages.is_empty() { + Vec::new() + } else { + let file = SourceFileBuilder::new(path.to_string_lossy(), &*self.source).finish(); + self.messages + .iter() + .map(|msg| Message { + kind: msg.kind.clone(), + range: msg.range, + fix: msg.fix.clone(), + file: file.clone(), + noqa_offset: msg.noqa_offset, + }) + .collect() + }; + Diagnostics::new(messages, self.imports.clone()) } } -#[derive(Deserialize)] -struct MessageHeader { +/// On disk representation of a diagnostic message. +#[derive(Deserialize, Debug, Serialize)] +struct CacheMessage { kind: DiagnosticKind, + /// Range into the message's [`FileCache::source`]. range: TextRange, fix: Option, - file_id: usize, - noqa_row: TextSize, + noqa_offset: TextSize, } -#[derive(Deserialize)] -struct CheckResult { - imports: ImportMap, - messages: Vec, - files: FilesBuf, -} - -fn content_dir() -> &'static Path { - Path::new("content") -} - -fn cache_key( - path: &Path, - package: Option<&Path>, - metadata: &fs::Metadata, - settings: &Settings, -) -> u64 { +/// Returns a hash key based on the `package_root`, `settings` and the crate +/// version. +fn cache_key(package_root: &Path, settings: &Settings) -> u64 { let mut hasher = CacheKeyHasher::new(); - CARGO_PKG_VERSION.cache_key(&mut hasher); - path.absolutize().unwrap().cache_key(&mut hasher); - package - .as_ref() - .map(|path| path.absolutize().unwrap()) - .cache_key(&mut hasher); - FileTime::from_last_modification_time(metadata).cache_key(&mut hasher); - #[cfg(unix)] - metadata.permissions().mode().cache_key(&mut hasher); + env!("CARGO_PKG_VERSION").cache_key(&mut hasher); + package_root.cache_key(&mut hasher); settings.cache_key(&mut hasher); hasher.finish() } -#[allow(dead_code)] /// Initialize the cache at the specified `Path`. pub(crate) fn init(path: &Path) -> Result<()> { // Create the cache directories. - fs::create_dir_all(path.join(content_dir()))?; + fs::create_dir_all(path.join("content"))?; // Add the CACHEDIR.TAG. if !cachedir::is_tagged(path)? { @@ -182,98 +325,256 @@ pub(crate) fn init(path: &Path) -> Result<()> { Ok(()) } -fn write_sync(cache_dir: &Path, key: u64, value: &[u8]) -> Result<(), std::io::Error> { - fs::write( - cache_dir.join(content_dir()).join(format!("{key:x}")), - value, - ) -} +#[cfg(test)] +mod tests { + use std::env::temp_dir; + use std::fs; + use std::io::{self, Write}; + use std::path::{Path, PathBuf}; + use std::sync::atomic::AtomicU64; + use std::time::SystemTime; -fn read_sync(cache_dir: &Path, key: u64) -> Result, std::io::Error> { - fs::read(cache_dir.join(content_dir()).join(format!("{key:x}"))) -} + use ruff::settings::{flags, AllSettings}; + use ruff_cache::CACHE_DIR_NAME; + use ruff_python_ast::imports::ImportMap; -fn del_sync(cache_dir: &Path, key: u64) -> Result<(), std::io::Error> { - fs::remove_file(cache_dir.join(content_dir()).join(format!("{key:x}"))) -} + use crate::cache::{self, Cache, FileCache}; + use crate::diagnostics::{lint_path, Diagnostics}; -/// Get a value from the cache. -pub(crate) fn get( - path: &Path, - package: Option<&Path>, - metadata: &fs::Metadata, - settings: &AllSettings, -) -> Option<(Vec, ImportMap)> { - let encoded = read_sync( - &settings.cli.cache_dir, - cache_key(path, package, metadata, &settings.lib), - ) - .ok()?; - match bincode::deserialize::(&encoded[..]) { - Ok(CheckResult { - messages: headers, - imports, - files: sources, - }) => { - let mut messages = Vec::with_capacity(headers.len()); + #[test] + fn same_results() { + let mut cache_dir = temp_dir(); + cache_dir.push("ruff_tests/cache_same_results"); + let _ = fs::remove_dir_all(&cache_dir); + cache::init(&cache_dir).unwrap(); - let source_files: Vec<_> = sources - .into_iter() - .map(|(filename, text)| SourceFileBuilder::new(filename, text).finish()) - .collect(); + let settings = AllSettings::default(); - for header in headers { - let Some(source_file) = source_files.get(header.file_id) else { - error!("Failed to retrieve source file for cached entry"); - return None; - }; + let package_root = fs::canonicalize("../ruff/resources/test/fixtures").unwrap(); + let cache = Cache::open(&cache_dir, package_root.clone(), &settings.lib); + assert_eq!(cache.new_files.lock().unwrap().len(), 0); - messages.push(Message { - kind: header.kind, - range: header.range, - fix: header.fix, - file: source_file.clone(), - noqa_offset: header.noqa_row, - }); + let mut paths = Vec::new(); + let mut parse_errors = Vec::new(); + let mut expected_diagnostics = Diagnostics::default(); + for entry in fs::read_dir(&package_root).unwrap() { + let entry = entry.unwrap(); + if !entry.file_type().unwrap().is_dir() { + continue; } - Some((messages, imports)) + let dir_path = entry.path(); + if dir_path.ends_with(CACHE_DIR_NAME) { + continue; + } + + for entry in fs::read_dir(dir_path).unwrap() { + let entry = entry.unwrap(); + if !entry.file_type().unwrap().is_file() { + continue; + } + + let path = entry.path(); + if path.ends_with("pyproject.toml") || path.ends_with("R.ipynb") { + continue; + } + + let diagnostics = lint_path( + &path, + Some(&package_root), + &settings, + Some(&cache), + flags::Noqa::Enabled, + flags::FixMode::Generate, + ) + .unwrap(); + if diagnostics + .messages + .iter() + .any(|m| m.kind.name == "SyntaxError") + { + parse_errors.push(path.clone()); + } + paths.push(path); + expected_diagnostics += diagnostics; + } } - Err(e) => { - error!("Failed to deserialize encoded cache entry: {e:?}"); - None + assert_ne!(paths, &[] as &[std::path::PathBuf], "no files checked"); + + cache.store().unwrap(); + + let cache = Cache::open(&cache_dir, package_root.clone(), &settings.lib); + assert_ne!(cache.package.files.len(), 0); + + parse_errors.sort(); + + for path in &paths { + if parse_errors.binary_search(path).is_ok() { + continue; // We don't cache parsing errors. + } + + let relative_path = cache.relative_path(path).unwrap(); + + assert!( + cache.package.files.contains_key(relative_path), + "missing file from cache: '{}'", + relative_path.display() + ); + } + + let mut got_diagnostics = Diagnostics::default(); + for path in paths { + got_diagnostics += lint_path( + &path, + Some(&package_root), + &settings, + Some(&cache), + flags::Noqa::Enabled, + flags::FixMode::Generate, + ) + .unwrap(); + } + + // Not stored in the cache. + expected_diagnostics.source_kind.clear(); + got_diagnostics.source_kind.clear(); + assert!(expected_diagnostics == got_diagnostics); + } + + #[test] + fn invalidation() { + // NOTE: keep in sync with actual file. + const SOURCE: &[u8] = b"# NOTE: sync with cache::invalidation test\na = 1\n\n__all__ = list([\"a\", \"b\"])\n"; + + let mut cache_dir = temp_dir(); + cache_dir.push("ruff_tests/cache_invalidation"); + let _ = fs::remove_dir_all(&cache_dir); + cache::init(&cache_dir).unwrap(); + + let settings = AllSettings::default(); + let package_root = fs::canonicalize("resources/test/fixtures/cache_mutable").unwrap(); + let cache = Cache::open(&cache_dir, package_root.clone(), &settings.lib); + assert_eq!(cache.new_files.lock().unwrap().len(), 0); + + let path = package_root.join("source.py"); + let mut expected_diagnostics = lint_path( + &path, + Some(&package_root), + &settings, + Some(&cache), + flags::Noqa::Enabled, + flags::FixMode::Generate, + ) + .unwrap(); + assert_eq!(cache.new_files.lock().unwrap().len(), 1); + + cache.store().unwrap(); + + let tests = [ + // File change. + (|path| { + let mut file = fs::OpenOptions::new() + .write(true) + .truncate(true) + .open(path)?; + file.write_all(SOURCE)?; + file.sync_data()?; + Ok(|_| Ok(())) + }) as fn(&Path) -> io::Result io::Result<()>>, + // Regression for issue #3086. + #[cfg(unix)] + |path| { + flip_execute_permission_bit(path)?; + Ok(flip_execute_permission_bit) + }, + ]; + + #[cfg(unix)] + #[allow(clippy::items_after_statements)] + fn flip_execute_permission_bit(path: &Path) -> io::Result<()> { + use std::os::unix::fs::PermissionsExt; + let file = fs::OpenOptions::new().write(true).open(path)?; + let perms = file.metadata()?.permissions(); + file.set_permissions(PermissionsExt::from_mode(perms.mode() ^ 0o111)) + } + + for change_file in tests { + let cleanup = change_file(&path).unwrap(); + + let cache = Cache::open(&cache_dir, package_root.clone(), &settings.lib); + + let mut got_diagnostics = lint_path( + &path, + Some(&package_root), + &settings, + Some(&cache), + flags::Noqa::Enabled, + flags::FixMode::Generate, + ) + .unwrap(); + + cleanup(&path).unwrap(); + + assert_eq!( + cache.new_files.lock().unwrap().len(), + 1, + "cache must not be used" + ); + + // Not store in the cache. + expected_diagnostics.source_kind.clear(); + got_diagnostics.source_kind.clear(); + assert!(expected_diagnostics == got_diagnostics); } } -} -/// Set a value in the cache. -pub(crate) fn set( - path: &Path, - package: Option<&Path>, - metadata: &fs::Metadata, - settings: &AllSettings, - messages: &[Message], - imports: &ImportMap, -) { - let check_result = CheckResultRef { imports, messages }; - if let Err(e) = write_sync( - &settings.cli.cache_dir, - cache_key(path, package, metadata, &settings.lib), - &bincode::serialize(&check_result).unwrap(), - ) { - error!("Failed to write to cache: {e:?}"); + #[test] + fn remove_old_files() { + let mut cache_dir = temp_dir(); + cache_dir.push("ruff_tests/cache_remove_old_files"); + let _ = fs::remove_dir_all(&cache_dir); + cache::init(&cache_dir).unwrap(); + + let settings = AllSettings::default(); + let package_root = + fs::canonicalize("resources/test/fixtures/cache_remove_old_files").unwrap(); + let mut cache = Cache::open(&cache_dir, package_root.clone(), &settings.lib); + assert_eq!(cache.new_files.lock().unwrap().len(), 0); + + // Add a file to the cache that hasn't been linted or seen since the + // '70s! + cache.package.files.insert( + PathBuf::from("old.py"), + FileCache { + last_modified: SystemTime::UNIX_EPOCH, + last_seen: AtomicU64::new(123), + imports: ImportMap::new(), + messages: Vec::new(), + source: String::new(), + }, + ); + + // Now actually lint a file. + let path = package_root.join("source.py"); + lint_path( + &path, + Some(&package_root), + &settings, + Some(&cache), + flags::Noqa::Enabled, + flags::FixMode::Generate, + ) + .unwrap(); + + // Storing the cache should remove the old (`old.py`) file. + cache.store().unwrap(); + // So we when we open the cache again it shouldn't contain `old.py`. + let cache = Cache::open(&cache_dir, package_root, &settings.lib); + + assert_eq!(cache.package.files.len(), 1, "didn't remove the old file"); + assert!( + !cache.package.files.contains_key(&path), + "removed the wrong file" + ); } } - -/// Delete a value from the cache. -pub(crate) fn del( - path: &Path, - package: Option<&Path>, - metadata: &fs::Metadata, - settings: &AllSettings, -) { - drop(del_sync( - &settings.cli.cache_dir, - cache_key(path, package, metadata, &settings.lib), - )); -} diff --git a/crates/ruff_cli/src/commands/add_noqa.rs b/crates/ruff_cli/src/commands/add_noqa.rs index 54d6417524..06cc9b13f1 100644 --- a/crates/ruff_cli/src/commands/add_noqa.rs +++ b/crates/ruff_cli/src/commands/add_noqa.rs @@ -9,6 +9,7 @@ use rayon::prelude::*; use ruff::linter::add_noqa_to_path; use ruff::resolver::PyprojectConfig; use ruff::{packaging, resolver, warn_user_once}; +use ruff_python_stdlib::path::{is_jupyter_notebook, is_project_toml}; use crate::args::Overrides; @@ -46,6 +47,9 @@ pub(crate) fn add_noqa( .flatten() .filter_map(|entry| { let path = entry.path(); + if is_project_toml(path) || is_jupyter_notebook(path) { + return None; + } let package = path .parent() .and_then(|parent| package_roots.get(parent)) diff --git a/crates/ruff_cli/src/commands/linter.rs b/crates/ruff_cli/src/commands/linter.rs index ccbeb5ba45..76d6845b06 100644 --- a/crates/ruff_cli/src/commands/linter.rs +++ b/crates/ruff_cli/src/commands/linter.rs @@ -7,7 +7,7 @@ use itertools::Itertools; use serde::Serialize; use strum::IntoEnumIterator; -use ruff::registry::{Linter, RuleNamespace, UpstreamCategory}; +use ruff::registry::{Linter, RuleNamespace}; use crate::args::HelpFormat; @@ -37,7 +37,7 @@ pub(crate) fn linter(format: HelpFormat) -> Result<()> { .upstream_categories() .unwrap() .iter() - .map(|UpstreamCategory(prefix, ..)| prefix.short_code()) + .map(|c| c.prefix) .join("/"), prefix => prefix.to_string(), }; @@ -52,9 +52,9 @@ pub(crate) fn linter(format: HelpFormat) -> Result<()> { name: linter_info.name(), categories: linter_info.upstream_categories().map(|cats| { cats.iter() - .map(|UpstreamCategory(prefix, name)| LinterCategoryInfo { - prefix: prefix.short_code(), - name, + .map(|c| LinterCategoryInfo { + prefix: c.prefix, + name: c.category, }) .collect() }), diff --git a/crates/ruff_cli/src/commands/rule.rs b/crates/ruff_cli/src/commands/rule.rs index ddb8e4ff21..a16ec4622b 100644 --- a/crates/ruff_cli/src/commands/rule.rs +++ b/crates/ruff_cli/src/commands/rule.rs @@ -1,7 +1,9 @@ use std::io::{self, BufWriter, Write}; use anyhow::Result; -use serde::Serialize; +use serde::ser::SerializeSeq; +use serde::{Serialize, Serializer}; +use strum::IntoEnumIterator; use ruff::registry::{Linter, Rule, RuleNamespace}; use ruff_diagnostics::AutofixKind; @@ -11,73 +13,106 @@ use crate::args::HelpFormat; #[derive(Serialize)] struct Explanation<'a> { name: &'a str, - code: &'a str, + code: String, linter: &'a str, summary: &'a str, message_formats: &'a [&'a str], - autofix: &'a str, + autofix: String, explanation: Option<&'a str>, + nursery: bool, +} + +impl<'a> Explanation<'a> { + fn from_rule(rule: &'a Rule) -> Self { + let code = rule.noqa_code().to_string(); + let (linter, _) = Linter::parse_code(&code).unwrap(); + let autofix = rule.autofixable().to_string(); + Self { + name: rule.as_ref(), + code, + linter: linter.name(), + summary: rule.message_formats()[0], + message_formats: rule.message_formats(), + autofix, + explanation: rule.explanation(), + nursery: rule.is_nursery(), + } + } +} + +fn format_rule_text(rule: Rule) -> String { + let mut output = String::new(); + output.push_str(&format!("# {} ({})", rule.as_ref(), rule.noqa_code())); + output.push('\n'); + output.push('\n'); + + let (linter, _) = Linter::parse_code(&rule.noqa_code().to_string()).unwrap(); + output.push_str(&format!("Derived from the **{}** linter.", linter.name())); + output.push('\n'); + output.push('\n'); + + let autofix = rule.autofixable(); + if matches!(autofix, AutofixKind::Always | AutofixKind::Sometimes) { + output.push_str(&autofix.to_string()); + output.push('\n'); + output.push('\n'); + } + + if rule.is_nursery() { + output.push_str(&format!( + r#"This rule is part of the **nursery**, a collection of newer lints that are +still under development. As such, it must be enabled by explicitly selecting +{}."#, + rule.noqa_code() + )); + output.push('\n'); + output.push('\n'); + } + + if let Some(explanation) = rule.explanation() { + output.push_str(explanation.trim()); + } else { + output.push_str("Message formats:"); + for format in rule.message_formats() { + output.push('\n'); + output.push_str(&format!("* {format}")); + } + } + output } /// Explain a `Rule` to the user. pub(crate) fn rule(rule: Rule, format: HelpFormat) -> Result<()> { - let (linter, _) = Linter::parse_code(&rule.noqa_code().to_string()).unwrap(); let mut stdout = BufWriter::new(io::stdout().lock()); - let mut output = String::new(); - match format { HelpFormat::Text => { - output.push_str(&format!("# {} ({})", rule.as_ref(), rule.noqa_code())); - output.push('\n'); - output.push('\n'); + writeln!(stdout, "{}", format_rule_text(rule))?; + } + HelpFormat::Json => { + serde_json::to_writer_pretty(stdout, &Explanation::from_rule(&rule))?; + } + }; + Ok(()) +} - let (linter, _) = Linter::parse_code(&rule.noqa_code().to_string()).unwrap(); - output.push_str(&format!("Derived from the **{}** linter.", linter.name())); - output.push('\n'); - output.push('\n'); - - let autofix = rule.autofixable(); - if matches!(autofix, AutofixKind::Always | AutofixKind::Sometimes) { - output.push_str(&autofix.to_string()); - output.push('\n'); - output.push('\n'); - } - - if rule.is_nursery() { - output.push_str(&format!( - r#"This rule is part of the **nursery**, a collection of newer lints that are -still under development. As such, it must be enabled by explicitly selecting -{}."#, - rule.noqa_code() - )); - output.push('\n'); - output.push('\n'); - } - - if let Some(explanation) = rule.explanation() { - output.push_str(explanation.trim()); - } else { - output.push_str("Message formats:"); - for format in rule.message_formats() { - output.push('\n'); - output.push_str(&format!("* {format}")); - } +/// Explain all rules to the user. +pub(crate) fn rules(format: HelpFormat) -> Result<()> { + let mut stdout = BufWriter::new(io::stdout().lock()); + match format { + HelpFormat::Text => { + for rule in Rule::iter() { + writeln!(stdout, "{}", format_rule_text(rule))?; + writeln!(stdout)?; } } HelpFormat::Json => { - output.push_str(&serde_json::to_string_pretty(&Explanation { - name: rule.as_ref(), - code: &rule.noqa_code().to_string(), - linter: linter.name(), - summary: rule.message_formats()[0], - message_formats: rule.message_formats(), - autofix: &rule.autofixable().to_string(), - explanation: rule.explanation(), - })?); + let mut serializer = serde_json::Serializer::pretty(stdout); + let mut seq = serializer.serialize_seq(None)?; + for rule in Rule::iter() { + seq.serialize_element(&Explanation::from_rule(&rule))?; + } + seq.end()?; } - }; - - writeln!(stdout, "{output}")?; - + } Ok(()) } diff --git a/crates/ruff_cli/src/commands/run.rs b/crates/ruff_cli/src/commands/run.rs index 78ef048870..61cdf6bf7a 100644 --- a/crates/ruff_cli/src/commands/run.rs +++ b/crates/ruff_cli/src/commands/run.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; +use std::fmt::Write; use std::io; use std::path::{Path, PathBuf}; use std::time::Instant; @@ -5,6 +7,7 @@ use std::time::Instant; use anyhow::Result; use colored::Colorize; use ignore::Error; +use itertools::Itertools; use log::{debug, error, warn}; #[cfg(not(target_family = "wasm"))] use rayon::prelude::*; @@ -20,7 +23,7 @@ use ruff_python_ast::imports::ImportMap; use ruff_python_ast::source_code::SourceFileBuilder; use crate::args::Overrides; -use crate::cache; +use crate::cache::{self, Cache}; use crate::diagnostics::Diagnostics; use crate::panic::catch_unwind; @@ -75,6 +78,25 @@ pub(crate) fn run( pyproject_config, ); + // Load the caches. + let caches = bool::from(cache).then(|| { + package_roots + .iter() + .map(|(package, package_root)| package_root.unwrap_or(package)) + .unique() + .par_bridge() + .map(|cache_root| { + let settings = resolver.resolve_all(cache_root, pyproject_config); + let cache = Cache::open( + &settings.cli.cache_dir, + cache_root.to_path_buf(), + &settings.lib, + ); + (cache_root, cache) + }) + .collect::>() + }); + let start = Instant::now(); let mut diagnostics: Diagnostics = paths .par_iter() @@ -86,13 +108,24 @@ pub(crate) fn run( .parent() .and_then(|parent| package_roots.get(parent)) .and_then(|package| *package); + let settings = resolver.resolve_all(path, pyproject_config); + let cache_root = package.unwrap_or_else(|| path.parent().unwrap_or(path)); + let cache = caches.as_ref().and_then(|caches| { + if let Some(cache) = caches.get(&cache_root) { + Some(cache) + } else { + debug!("No cache found for {}", cache_root.display()); + None + } + }); + lint_path(path, package, settings, cache, noqa, autofix).map_err(|e| { (Some(path.to_owned()), { let mut error = e.to_string(); for cause in e.chain() { - error += &format!("\n Caused by: {cause}"); + write!(&mut error, "\n Caused by: {cause}").unwrap(); } error }) @@ -145,6 +178,13 @@ pub(crate) fn run( diagnostics.messages.sort(); + // Store the caches. + if let Some(caches) = caches { + caches + .into_par_iter() + .try_for_each(|(_, cache)| cache.store())?; + } + let duration = start.elapsed(); debug!("Checked {:?} files in: {:?}", paths.len(), duration); @@ -157,7 +197,7 @@ fn lint_path( path: &Path, package: Option<&Path>, settings: &AllSettings, - cache: flags::Cache, + cache: Option<&Cache>, noqa: flags::Noqa, autofix: flags::FixMode, ) -> Result { @@ -186,93 +226,3 @@ with the relevant file contents, the `pyproject.toml` settings, and the followin } } } - -#[cfg(test)] -#[cfg(feature = "jupyter_notebook")] -mod test { - use std::path::PathBuf; - use std::str::FromStr; - - use anyhow::Result; - use path_absolutize::Absolutize; - - use ruff::logging::LogLevel; - use ruff::resolver::{PyprojectConfig, PyprojectDiscoveryStrategy}; - use ruff::settings::configuration::{Configuration, RuleSelection}; - use ruff::settings::flags::FixMode; - use ruff::settings::flags::{Cache, Noqa}; - use ruff::settings::types::SerializationFormat; - use ruff::settings::AllSettings; - use ruff::RuleSelector; - - use crate::args::Overrides; - use crate::printer::{Flags, Printer}; - - use super::run; - - #[test] - fn test_jupyter_notebook_integration() -> Result<()> { - let overrides: Overrides = Overrides { - select: Some(vec![ - RuleSelector::from_str("B")?, - RuleSelector::from_str("F")?, - ]), - ..Default::default() - }; - - let mut configuration = Configuration::default(); - configuration.rule_selections.push(RuleSelection { - select: Some(vec![ - RuleSelector::from_str("B")?, - RuleSelector::from_str("F")?, - ]), - ..Default::default() - }); - - let root_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("..") - .join("ruff") - .join("resources") - .join("test") - .join("fixtures") - .join("jupyter"); - - let diagnostics = run( - &[root_path.join("valid.ipynb")], - &PyprojectConfig::new( - PyprojectDiscoveryStrategy::Fixed, - AllSettings::from_configuration(configuration, &root_path)?, - None, - ), - &overrides, - Cache::Disabled, - Noqa::Enabled, - FixMode::None, - )?; - - let printer = Printer::new( - SerializationFormat::Text, - LogLevel::Default, - FixMode::None, - Flags::SHOW_VIOLATIONS, - ); - let mut writer: Vec = Vec::new(); - // Mute the terminal color codes - colored::control::set_override(false); - printer.write_once(&diagnostics, &mut writer)?; - // TODO(konstin): Set jupyter notebooks as none-fixable for now - // TODO(konstin) 2: Make jupyter notebooks fixable - let expected = format!( - "{valid_ipynb}:cell 1:2:5: F841 [*] Local variable `x` is assigned to but never used -{valid_ipynb}:cell 3:1:24: B006 Do not use mutable data structures for argument defaults -Found 2 errors. -[*] 1 potentially fixable with the --fix option. -", - valid_ipynb = root_path.join("valid.ipynb").absolutize()?.display() - ); - - assert_eq!(expected, String::from_utf8(writer)?); - - Ok(()) - } -} diff --git a/crates/ruff_cli/src/commands/show_files.rs b/crates/ruff_cli/src/commands/show_files.rs index 802501b578..7b13474e93 100644 --- a/crates/ruff_cli/src/commands/show_files.rs +++ b/crates/ruff_cli/src/commands/show_files.rs @@ -1,4 +1,4 @@ -use std::io::{self, BufWriter, Write}; +use std::io::Write; use std::path::PathBuf; use anyhow::Result; @@ -14,6 +14,7 @@ pub(crate) fn show_files( files: &[PathBuf], pyproject_config: &PyprojectConfig, overrides: &Overrides, + writer: &mut impl Write, ) -> Result<()> { // Collect all files in the hierarchy. let (paths, _resolver) = resolver::python_files_in_path(files, pyproject_config, overrides)?; @@ -24,13 +25,12 @@ pub(crate) fn show_files( } // Print the list of files. - let mut stdout = BufWriter::new(io::stdout().lock()); for entry in paths .iter() .flatten() .sorted_by(|a, b| a.path().cmp(b.path())) { - writeln!(stdout, "{}", entry.path().to_string_lossy())?; + writeln!(writer, "{}", entry.path().to_string_lossy())?; } Ok(()) diff --git a/crates/ruff_cli/src/commands/show_settings.rs b/crates/ruff_cli/src/commands/show_settings.rs index 48a88b811d..52f8a65dc1 100644 --- a/crates/ruff_cli/src/commands/show_settings.rs +++ b/crates/ruff_cli/src/commands/show_settings.rs @@ -1,4 +1,4 @@ -use std::io::{self, BufWriter, Write}; +use std::io::Write; use std::path::PathBuf; use anyhow::{bail, Result}; @@ -14,6 +14,7 @@ pub(crate) fn show_settings( files: &[PathBuf], pyproject_config: &PyprojectConfig, overrides: &Overrides, + writer: &mut impl Write, ) -> Result<()> { // Collect all files in the hierarchy. let (paths, resolver) = resolver::python_files_in_path(files, pyproject_config, overrides)?; @@ -22,18 +23,19 @@ pub(crate) fn show_settings( let Some(entry) = paths .iter() .flatten() - .sorted_by(|a, b| a.path().cmp(b.path())).next() else { + .sorted_by(|a, b| a.path().cmp(b.path())) + .next() + else { bail!("No files found under the given path"); }; let path = entry.path(); let settings = resolver.resolve(path, pyproject_config); - let mut stdout = BufWriter::new(io::stdout().lock()); - writeln!(stdout, "Resolved settings for: {path:?}")?; + writeln!(writer, "Resolved settings for: {path:?}")?; if let Some(settings_path) = pyproject_config.path.as_ref() { - writeln!(stdout, "Settings path: {settings_path:?}")?; + writeln!(writer, "Settings path: {settings_path:?}")?; } - writeln!(stdout, "{settings:#?}")?; + writeln!(writer, "{settings:#?}")?; Ok(()) } diff --git a/crates/ruff_cli/src/diagnostics.rs b/crates/ruff_cli/src/diagnostics.rs index 1cf5268c88..f994b7a055 100644 --- a/crates/ruff_cli/src/diagnostics.rs +++ b/crates/ruff_cli/src/diagnostics.rs @@ -14,26 +14,25 @@ use rustc_hash::FxHashMap; use similar::TextDiff; use ruff::fs; -use ruff::jupyter::{is_jupyter_notebook, JupyterIndex, JupyterNotebook}; +use ruff::jupyter::Notebook; use ruff::linter::{lint_fix, lint_only, FixTable, FixerResult, LinterResult}; use ruff::logging::DisplayParseError; use ruff::message::Message; use ruff::pyproject_toml::lint_pyproject_toml; use ruff::settings::{flags, AllSettings, Settings}; +use ruff::source_kind::SourceKind; use ruff_python_ast::imports::ImportMap; use ruff_python_ast::source_code::{LineIndex, SourceCode, SourceFileBuilder}; -use ruff_python_stdlib::path::is_project_toml; +use ruff_python_stdlib::path::{is_jupyter_notebook, is_project_toml}; -use crate::cache; +use crate::cache::Cache; #[derive(Debug, Default, PartialEq)] pub(crate) struct Diagnostics { pub(crate) messages: Vec, pub(crate) fixed: FxHashMap, pub(crate) imports: ImportMap, - /// Jupyter notebook indexing table for each input file that is a jupyter notebook - /// so we can rewrite the diagnostics in the end - pub(crate) jupyter_index: FxHashMap, + pub(crate) source_kind: FxHashMap, } impl Diagnostics { @@ -42,7 +41,7 @@ impl Diagnostics { messages, fixed: FxHashMap::default(), imports, - jupyter_index: FxHashMap::default(), + source_kind: FxHashMap::default(), } } } @@ -62,20 +61,15 @@ impl AddAssign for Diagnostics { } } } - self.jupyter_index.extend(other.jupyter_index); + self.source_kind.extend(other.source_kind); } } /// Returns either an indexed python jupyter notebook or a diagnostic (which is empty if we skip) -fn load_jupyter_notebook(path: &Path) -> Result<(String, JupyterIndex), Box> { - let notebook = match JupyterNotebook::read(path) { +fn load_jupyter_notebook(path: &Path) -> Result> { + let notebook = match Notebook::read(path) { Ok(notebook) => { - if !notebook - .metadata - .language_info - .as_ref() - .map_or(true, |language| language.name == "python") - { + if !notebook.is_python_notebook() { // Not a python notebook, this could e.g. be an R notebook which we want to just skip debug!( "Skipping {} because it's not a Python notebook", @@ -98,7 +92,7 @@ fn load_jupyter_notebook(path: &Path) -> Result<(String, JupyterIndex), Box, settings: &AllSettings, - cache: flags::Cache, + cache: Option<&Cache>, noqa: flags::Noqa, autofix: flags::FixMode, ) -> Result { @@ -116,27 +110,37 @@ pub(crate) fn lint_path( // to cache `fixer::Mode::Apply`, since a file either has no fixes, or we'll // write the fixes to disk, thus invalidating the cache. But it's a bit hard // to reason about. We need to come up with a better solution here.) - let metadata = if cache.into() - && noqa.into() - && matches!(autofix, flags::FixMode::None | flags::FixMode::Generate) - { - let metadata = path.metadata()?; - if let Some((messages, imports)) = cache::get(path, package, &metadata, settings) { - debug!("Cache hit for: {}", path.display()); - return Ok(Diagnostics::new(messages, imports)); + let caching = match cache { + Some(cache) if noqa.into() && autofix.is_generate() => { + let relative_path = cache + .relative_path(path) + .expect("wrong package cache for file"); + let last_modified = path.metadata()?.modified()?; + if let Some(cache) = cache.get(relative_path, last_modified) { + return Ok(cache.as_diagnostics(path)); + } + + Some((cache, relative_path, last_modified)) } - Some(metadata) - } else { - None + _ => None, }; debug!("Checking: {}", path.display()); - // We have to special case this here since the python tokenizer doesn't work with toml + // We have to special case this here since the Python tokenizer doesn't work with TOML. if is_project_toml(path) { - let contents = std::fs::read_to_string(path)?; - let source_file = SourceFileBuilder::new(path.to_string_lossy(), contents).finish(); - let messages = lint_pyproject_toml(source_file)?; + let messages = if settings + .lib + .rules + .iter_enabled() + .any(|rule_code| rule_code.lint_source().is_pyproject_toml()) + { + let contents = std::fs::read_to_string(path)?; + let source_file = SourceFileBuilder::new(path.to_string_lossy(), contents).finish(); + lint_pyproject_toml(source_file, &settings.lib)? + } else { + vec![] + }; return Ok(Diagnostics { messages, ..Diagnostics::default() @@ -144,15 +148,17 @@ pub(crate) fn lint_path( } // Read the file from disk - let (contents, jupyter_index) = if is_jupyter_notebook(path) { + let mut source_kind = if is_jupyter_notebook(path) { match load_jupyter_notebook(path) { - Ok((contents, jupyter_index)) => (contents, Some(jupyter_index)), - Err(diagnostics) => return Ok(*diagnostics), + Ok(notebook) => SourceKind::Jupyter(notebook), + Err(diagnostic) => return Ok(*diagnostic), } } else { - (std::fs::read_to_string(path)?, None) + SourceKind::Python(std::fs::read_to_string(path)?) }; + let contents = source_kind.content().to_string(); + // Lint the file. let ( LinterResult { @@ -165,19 +171,34 @@ pub(crate) fn lint_path( result, transformed, fixed, - }) = lint_fix(&contents, path, package, noqa, &settings.lib) - { + }) = lint_fix( + &contents, + path, + package, + noqa, + &settings.lib, + &mut source_kind, + ) { if !fixed.is_empty() { - if matches!(autofix, flags::FixMode::Apply) { - write(path, transformed.as_bytes())?; - } else if matches!(autofix, flags::FixMode::Diff) { - let mut stdout = io::stdout().lock(); - TextDiff::from_lines(contents.as_str(), &transformed) - .unified_diff() - .header(&fs::relativize_path(path), &fs::relativize_path(path)) - .to_writer(&mut stdout)?; - stdout.write_all(b"\n")?; - stdout.flush()?; + match autofix { + flags::FixMode::Apply => match &source_kind { + SourceKind::Python(_) => { + write(path, transformed.as_bytes())?; + } + SourceKind::Jupyter(notebook) => { + notebook.write(path)?; + } + }, + flags::FixMode::Diff => { + let mut stdout = io::stdout().lock(); + TextDiff::from_lines(contents.as_str(), &transformed) + .unified_diff() + .header(&fs::relativize_path(path), &fs::relativize_path(path)) + .to_writer(&mut stdout)?; + stdout.write_all(b"\n")?; + stdout.flush()?; + } + flags::FixMode::Generate => {} } } (result, fixed) @@ -195,45 +216,39 @@ pub(crate) fn lint_path( let imports = imports.unwrap_or_default(); + if let Some((cache, relative_path, file_last_modified)) = caching { + // We don't cache parsing errors. + if parse_error.is_none() { + cache.update( + relative_path.to_owned(), + file_last_modified, + &messages, + &imports, + ); + } + } + if let Some(err) = parse_error { error!( "{}", DisplayParseError::new( err, - SourceCode::new(&contents, &LineIndex::from_source_text(&contents)) + SourceCode::new(&contents, &LineIndex::from_source_text(&contents)), + Some(&source_kind), ) ); - - // Purge the cache. - if let Some(metadata) = metadata { - cache::del(path, package, &metadata, settings); - } - } else { - // Re-populate the cache. - if let Some(metadata) = metadata { - cache::set(path, package, &metadata, settings, &messages, &imports); - } } - let jupyter_index = match jupyter_index { - None => FxHashMap::default(), - Some(jupyter_index) => { - let mut index = FxHashMap::default(); - index.insert( - path.to_str() - .ok_or_else(|| anyhow!("Unable to parse filename: {:?}", path))? - .to_string(), - jupyter_index, - ); - index - } - }; - Ok(Diagnostics { messages, fixed: FxHashMap::from_iter([(fs::relativize_path(path), fixed)]), imports, - jupyter_index, + source_kind: FxHashMap::from_iter([( + path.to_str() + .ok_or_else(|| anyhow!("Unable to parse filename: {:?}", path))? + .to_string(), + source_kind, + )]), }) } @@ -247,6 +262,7 @@ pub(crate) fn lint_stdin( noqa: flags::Noqa, autofix: flags::FixMode, ) -> Result { + let mut source_kind = SourceKind::Python(contents.to_string()); // Lint the inputs. let ( LinterResult { @@ -265,24 +281,30 @@ pub(crate) fn lint_stdin( package, noqa, settings, + &mut source_kind, ) { - if matches!(autofix, flags::FixMode::Apply) { - // Write the contents to stdout, regardless of whether any errors were fixed. - io::stdout().write_all(transformed.as_bytes())?; - } else if matches!(autofix, flags::FixMode::Diff) { - // But only write a diff if it's non-empty. - if !fixed.is_empty() { - let text_diff = TextDiff::from_lines(contents, &transformed); - let mut unified_diff = text_diff.unified_diff(); - if let Some(path) = path { - unified_diff.header(&fs::relativize_path(path), &fs::relativize_path(path)); - } - - let mut stdout = io::stdout().lock(); - unified_diff.to_writer(&mut stdout)?; - stdout.write_all(b"\n")?; - stdout.flush()?; + match autofix { + flags::FixMode::Apply => { + // Write the contents to stdout, regardless of whether any errors were fixed. + io::stdout().write_all(transformed.as_bytes())?; } + flags::FixMode::Diff => { + // But only write a diff if it's non-empty. + if !fixed.is_empty() { + let text_diff = TextDiff::from_lines(contents, &transformed); + let mut unified_diff = text_diff.unified_diff(); + if let Some(path) = path { + unified_diff + .header(&fs::relativize_path(path), &fs::relativize_path(path)); + } + + let mut stdout = io::stdout().lock(); + unified_diff.to_writer(&mut stdout)?; + stdout.write_all(b"\n")?; + stdout.flush()?; + } + } + flags::FixMode::Generate => {} } (result, fixed) @@ -298,7 +320,7 @@ pub(crate) fn lint_stdin( let fixed = FxHashMap::default(); // Write the contents to stdout anyway. - if matches!(autofix, flags::FixMode::Apply) { + if autofix.is_apply() { io::stdout().write_all(contents.as_bytes())?; } @@ -332,12 +354,12 @@ pub(crate) fn lint_stdin( fixed, )]), imports, - jupyter_index: FxHashMap::default(), + source_kind: FxHashMap::default(), }) } #[cfg(test)] -mod test { +mod tests { use std::path::Path; use crate::diagnostics::{load_jupyter_notebook, Diagnostics}; diff --git a/crates/ruff_cli/src/lib.rs b/crates/ruff_cli/src/lib.rs index 7f7a70f2eb..edfb43f5de 100644 --- a/crates/ruff_cli/src/lib.rs +++ b/crates/ruff_cli/src/lib.rs @@ -1,3 +1,4 @@ +use std::fs::File; use std::io::{self, stdout, BufWriter, Write}; use std::path::{Path, PathBuf}; use std::process::ExitCode; @@ -12,7 +13,7 @@ use ruff::logging::{set_up_logging, LogLevel}; use ruff::settings::types::SerializationFormat; use ruff::settings::{flags, CliSettings}; use ruff::{fs, warn_user_once}; -use ruff_python_formatter::format_module; +use ruff_python_formatter::{format_module, PyFormatOptions}; use crate::args::{Args, CheckArgs, Command}; use crate::commands::run_stdin::read_from_stdin; @@ -24,7 +25,7 @@ mod commands; mod diagnostics; mod panic; mod printer; -mod resolve; +pub mod resolve; #[derive(Copy, Clone)] pub enum ExitStatus { @@ -57,7 +58,7 @@ enum ChangeKind { /// Returns `None` if no relevant changes were detected. fn change_detected(paths: &[PathBuf]) -> Option { // If any `.toml` files were modified, return `ChangeKind::Configuration`. Otherwise, return - // `ChangeKind::SourceFile` if any `.py`, `.pyi`, or `.pyw` files were modified. + // `ChangeKind::SourceFile` if any `.py`, `.pyi`, `.pyw`, or `.ipynb` files were modified. let mut source_file = false; for path in paths { if let Some(suffix) = path.extension() { @@ -65,7 +66,7 @@ fn change_detected(paths: &[PathBuf]) -> Option { Some("toml") => { return Some(ChangeKind::Configuration); } - Some("py" | "pyi" | "pyw") => source_file = true, + Some("py" | "pyi" | "pyw" | "ipynb") => source_file = true, _ => {} } } @@ -76,6 +77,27 @@ fn change_detected(paths: &[PathBuf]) -> Option { None } +/// Returns true if the linter should read from standard input. +fn is_stdin(files: &[PathBuf], stdin_filename: Option<&Path>) -> bool { + // If the user provided a `--stdin-filename`, always read from standard input. + if stdin_filename.is_some() { + if let Some(file) = files.iter().find(|file| file.as_path() != Path::new("-")) { + warn_user_once!( + "Ignoring file {} in favor of standard input.", + file.display() + ); + } + return true; + } + + // If the user provided exactly `-`, read from standard input. + if files.len() == 1 && files[0] == Path::new("-") { + return true; + } + + false +} + pub fn run( Args { command, @@ -112,7 +134,14 @@ quoting the executed command, along with the relevant file contents and `pyproje set_up_logging(&log_level)?; match command { - Command::Rule { rule, format } => commands::rule::rule(rule, format)?, + Command::Rule { rule, all, format } => { + if all { + commands::rule::rules(format)?; + } + if let Some(rule) = rule { + commands::rule::rule(rule, format)?; + } + } Command::Config { option } => return Ok(commands::config::config(option.as_deref())), Command::Linter { format } => commands::linter::linter(format)?, Command::Clean => commands::clean::clean(log_level)?, @@ -136,7 +165,7 @@ fn format(files: &[PathBuf]) -> Result { // dummy, to check that the function was actually called let contents = code.replace("# DEL", ""); // real formatting that is currently a passthrough - format_module(&contents) + format_module(&contents, PyFormatOptions::default()) }; match &files { @@ -159,7 +188,7 @@ fn format(files: &[PathBuf]) -> Result { Ok(ExitStatus::Success) } -fn check(args: CheckArgs, log_level: LogLevel) -> Result { +pub fn check(args: CheckArgs, log_level: LogLevel) -> Result { let (cli, overrides) = args.partition(); // Construct the "default" settings. These are used when no `pyproject.toml` @@ -171,12 +200,26 @@ fn check(args: CheckArgs, log_level: LogLevel) -> Result { cli.stdin_filename.as_deref(), )?; + let mut writer: Box = match cli.output_file { + Some(path) if !cli.watch => { + colored::control::set_override(false); + let file = File::create(path)?; + Box::new(BufWriter::new(file)) + } + _ => Box::new(BufWriter::new(io::stdout())), + }; + if cli.show_settings { - commands::show_settings::show_settings(&cli.files, &pyproject_config, &overrides)?; + commands::show_settings::show_settings( + &cli.files, + &pyproject_config, + &overrides, + &mut writer, + )?; return Ok(ExitStatus::Success); } if cli.show_files { - commands::show_files::show_files(&cli.files, &pyproject_config, &overrides)?; + commands::show_files::show_files(&cli.files, &pyproject_config, &overrides, &mut writer)?; return Ok(ExitStatus::Success); } @@ -192,23 +235,17 @@ fn check(args: CheckArgs, log_level: LogLevel) -> Result { } = pyproject_config.settings.cli; // Autofix rules are as follows: + // - By default, generate all fixes, but don't apply them to the filesystem. // - If `--fix` or `--fix-only` is set, always apply fixes to the filesystem (or // print them to stdout, if we're reading from stdin). - // - Otherwise, if `--format json` is set, generate the fixes (so we print them - // out as part of the JSON payload), but don't write them to disk. // - If `--diff` or `--fix-only` are set, don't print any violations (only // fixes). - // TODO(charlie): Consider adding ESLint's `--fix-dry-run`, which would generate - // but not apply fixes. That would allow us to avoid special-casing JSON - // here. let autofix = if cli.diff { flags::FixMode::Diff } else if fix || fix_only { flags::FixMode::Apply - } else if matches!(format, SerializationFormat::Json) { - flags::FixMode::Generate } else { - flags::FixMode::None + flags::FixMode::Generate }; let cache = !cli.no_cache; let noqa = !cli.ignore_noqa; @@ -238,7 +275,7 @@ fn check(args: CheckArgs, log_level: LogLevel) -> Result { } if cli.add_noqa { - if !matches!(autofix, flags::FixMode::None) { + if !autofix.is_generate() { warn_user_once!("--fix is incompatible with --add-noqa."); } let modifications = @@ -282,7 +319,7 @@ fn check(args: CheckArgs, log_level: LogLevel) -> Result { noqa.into(), autofix, )?; - printer.write_continuously(&messages)?; + printer.write_continuously(&mut writer, &messages)?; // In watch mode, we may need to re-resolve the configuration. // TODO(charlie): Re-compute other derivative values, like the `printer`. @@ -314,13 +351,13 @@ fn check(args: CheckArgs, log_level: LogLevel) -> Result { noqa.into(), autofix, )?; - printer.write_continuously(&messages)?; + printer.write_continuously(&mut writer, &messages)?; } Err(err) => return Err(err.into()), } } } else { - let is_stdin = cli.files == vec![PathBuf::from("-")]; + let is_stdin = is_stdin(&cli.files, cli.stdin_filename.as_deref()); // Generate lint violations. let diagnostics = if is_stdin { @@ -347,10 +384,9 @@ fn check(args: CheckArgs, log_level: LogLevel) -> Result { // source code goes to stdout). if !(is_stdin && matches!(autofix, flags::FixMode::Apply | flags::FixMode::Diff)) { if cli.statistics { - printer.write_statistics(&diagnostics)?; + printer.write_statistics(&diagnostics, &mut writer)?; } else { - let mut stdout = BufWriter::new(io::stdout().lock()); - printer.write_once(&diagnostics, &mut stdout)?; + printer.write_once(&diagnostics, &mut writer)?; } } diff --git a/crates/ruff_cli/src/printer.rs b/crates/ruff_cli/src/printer.rs index 81d4c95f4e..0c8070e6e4 100644 --- a/crates/ruff_cli/src/printer.rs +++ b/crates/ruff_cli/src/printer.rs @@ -1,8 +1,7 @@ use std::cmp::Reverse; use std::fmt::Display; use std::hash::Hash; -use std::io; -use std::io::{BufWriter, Write}; +use std::io::Write; use anyhow::Result; use bitflags::bitflags; @@ -16,7 +15,7 @@ use ruff::linter::FixTable; use ruff::logging::LogLevel; use ruff::message::{ AzureEmitter, Emitter, EmitterContext, GithubEmitter, GitlabEmitter, GroupedEmitter, - JsonEmitter, JunitEmitter, PylintEmitter, TextEmitter, + JsonEmitter, JsonLinesEmitter, JunitEmitter, PylintEmitter, TextEmitter, }; use ruff::notify_user; use ruff::registry::{AsRule, Rule}; @@ -98,7 +97,7 @@ impl Printer { } } - fn write_summary_text(&self, stdout: &mut dyn Write, diagnostics: &Diagnostics) -> Result<()> { + fn write_summary_text(&self, writer: &mut dyn Write, diagnostics: &Diagnostics) -> Result<()> { if self.log_level >= LogLevel::Default { if self.flags.contains(Flags::SHOW_VIOLATIONS) { let fixed = diagnostics @@ -111,12 +110,12 @@ impl Printer { if fixed > 0 { let s = if total == 1 { "" } else { "s" }; writeln!( - stdout, + writer, "Found {total} error{s} ({fixed} fixed, {remaining} remaining)." )?; } else if remaining > 0 { let s = if remaining == 1 { "" } else { "s" }; - writeln!(stdout, "Found {remaining} error{s}.")?; + writeln!(writer, "Found {remaining} error{s}.")?; } if show_fix_status(self.autofix_level) { @@ -127,7 +126,7 @@ impl Printer { .count(); if num_fixable > 0 { writeln!( - stdout, + writer, "[{}] {num_fixable} potentially fixable with the --fix option.", "*".cyan(), )?; @@ -141,10 +140,10 @@ impl Printer { .sum::(); if fixed > 0 { let s = if fixed == 1 { "" } else { "s" }; - if matches!(self.autofix_level, flags::FixMode::Apply) { - writeln!(stdout, "Fixed {fixed} error{s}.")?; - } else if matches!(self.autofix_level, flags::FixMode::Diff) { - writeln!(stdout, "Would fix {fixed} error{s}.")?; + if self.autofix_level.is_apply() { + writeln!(writer, "Fixed {fixed} error{s}.")?; + } else { + writeln!(writer, "Would fix {fixed} error{s}.")?; } } } @@ -155,7 +154,7 @@ impl Printer { pub(crate) fn write_once( &self, diagnostics: &Diagnostics, - writer: &mut impl Write, + writer: &mut dyn Write, ) -> Result<()> { if matches!(self.log_level, LogLevel::Silent) { return Ok(()); @@ -178,14 +177,17 @@ impl Printer { return Ok(()); } - let context = EmitterContext::new(&diagnostics.jupyter_index); + let context = EmitterContext::new(&diagnostics.source_kind); match self.format { SerializationFormat::Json => { - JsonEmitter::default().emit(writer, &diagnostics.messages, &context)?; + JsonEmitter.emit(writer, &diagnostics.messages, &context)?; + } + SerializationFormat::JsonLines => { + JsonLinesEmitter.emit(writer, &diagnostics.messages, &context)?; } SerializationFormat::Junit => { - JunitEmitter::default().emit(writer, &diagnostics.messages, &context)?; + JunitEmitter.emit(writer, &diagnostics.messages, &context)?; } SerializationFormat::Text => { TextEmitter::default() @@ -220,16 +222,16 @@ impl Printer { self.write_summary_text(writer, diagnostics)?; } SerializationFormat::Github => { - GithubEmitter::default().emit(writer, &diagnostics.messages, &context)?; + GithubEmitter.emit(writer, &diagnostics.messages, &context)?; } SerializationFormat::Gitlab => { GitlabEmitter::default().emit(writer, &diagnostics.messages, &context)?; } SerializationFormat::Pylint => { - PylintEmitter::default().emit(writer, &diagnostics.messages, &context)?; + PylintEmitter.emit(writer, &diagnostics.messages, &context)?; } SerializationFormat::Azure => { - AzureEmitter::default().emit(writer, &diagnostics.messages, &context)?; + AzureEmitter.emit(writer, &diagnostics.messages, &context)?; } } @@ -238,7 +240,11 @@ impl Printer { Ok(()) } - pub(crate) fn write_statistics(&self, diagnostics: &Diagnostics) -> Result<()> { + pub(crate) fn write_statistics( + &self, + diagnostics: &Diagnostics, + writer: &mut dyn Write, + ) -> Result<()> { let statistics: Vec = diagnostics .messages .iter() @@ -274,7 +280,6 @@ impl Printer { return Ok(()); } - let mut stdout = BufWriter::new(io::stdout().lock()); match self.format { SerializationFormat::Text => { // Compute the maximum number of digits in the count and code, for all messages, @@ -299,7 +304,7 @@ impl Printer { // By default, we mimic Flake8's `--statistics` format. for statistic in statistics { writeln!( - stdout, + writer, "{:>count_width$}\t{: { - writeln!(stdout, "{}", serde_json::to_string_pretty(&statistics)?)?; + writeln!(writer, "{}", serde_json::to_string_pretty(&statistics)?)?; } _ => { anyhow::bail!( @@ -328,12 +333,16 @@ impl Printer { } } - stdout.flush()?; + writer.flush()?; Ok(()) } - pub(crate) fn write_continuously(&self, diagnostics: &Diagnostics) -> Result<()> { + pub(crate) fn write_continuously( + &self, + writer: &mut dyn Write, + diagnostics: &Diagnostics, + ) -> Result<()> { if matches!(self.log_level, LogLevel::Silent) { return Ok(()); } @@ -350,19 +359,18 @@ impl Printer { ); } - let mut stdout = BufWriter::new(io::stdout().lock()); if !diagnostics.messages.is_empty() { if self.log_level >= LogLevel::Default { - writeln!(stdout)?; + writeln!(writer)?; } - let context = EmitterContext::new(&diagnostics.jupyter_index); + let context = EmitterContext::new(&diagnostics.source_kind); TextEmitter::default() .with_show_fix_status(show_fix_status(self.autofix_level)) .with_show_source(self.flags.contains(Flags::SHOW_SOURCE)) - .emit(&mut stdout, &diagnostics.messages, &context)?; + .emit(writer, &diagnostics.messages, &context)?; } - stdout.flush()?; + writer.flush()?; Ok(()) } @@ -388,10 +396,10 @@ const fn show_fix_status(autofix_level: flags::FixMode) -> bool { // this pass! (We're occasionally unable to determine whether a specific // violation is fixable without trying to fix it, so if autofix is not // enabled, we may inadvertently indicate that a rule is fixable.) - !matches!(autofix_level, flags::FixMode::Apply) + !autofix_level.is_apply() } -fn print_fix_summary(stdout: &mut T, fixed: &FxHashMap) -> Result<()> { +fn print_fix_summary(writer: &mut dyn Write, fixed: &FxHashMap) -> Result<()> { let total = fixed .values() .map(|table| table.values().sum::()) @@ -407,14 +415,14 @@ fn print_fix_summary(stdout: &mut T, fixed: &FxHashMap(stdout: &mut T, fixed: &FxHashMapnum_digits$} × {} ({})", rule.noqa_code().to_string().red().bold(), rule.as_ref(), diff --git a/crates/ruff_cli/src/resolve.rs b/crates/ruff_cli/src/resolve.rs index 7952d70158..0bf5db6670 100644 --- a/crates/ruff_cli/src/resolve.rs +++ b/crates/ruff_cli/src/resolve.rs @@ -14,7 +14,7 @@ use crate::args::Overrides; /// Resolve the relevant settings strategy and defaults for the current /// invocation. -pub(crate) fn resolve( +pub fn resolve( isolated: bool, config: Option<&Path>, overrides: &Overrides, diff --git a/crates/ruff_cli/tests/integration_test.rs b/crates/ruff_cli/tests/integration_test.rs index 033839d28f..674b2fd5a2 100644 --- a/crates/ruff_cli/tests/integration_test.rs +++ b/crates/ruff_cli/tests/integration_test.rs @@ -91,34 +91,35 @@ fn stdin_json() -> Result<()> { r#"[ {{ "code": "F401", - "message": "`os` imported but unused", + "end_location": {{ + "column": 10, + "row": 1 + }}, + "filename": "{file_path}", "fix": {{ "applicability": "Automatic", - "message": "Remove unused import: `os`", "edits": [ {{ "content": "", - "location": {{ - "row": 1, - "column": 1 - }}, "end_location": {{ - "row": 2, - "column": 1 + "column": 1, + "row": 2 + }}, + "location": {{ + "column": 1, + "row": 1 }} }} - ] + ], + "message": "Remove unused import: `os`" }}, "location": {{ - "row": 1, - "column": 8 + "column": 8, + "row": 1 }}, - "end_location": {{ - "row": 1, - "column": 10 - }}, - "filename": "{file_path}", - "noqa_row": 1 + "message": "`os` imported but unused", + "noqa_row": 1, + "url": "https://beta.ruff.rs/docs/rules/unused-import" }} ]"# ) diff --git a/crates/ruff_dev/Cargo.toml b/crates/ruff_dev/Cargo.toml index 4fa0405c9a..36c9c42d59 100644 --- a/crates/ruff_dev/Cargo.toml +++ b/crates/ruff_dev/Cargo.toml @@ -2,25 +2,39 @@ name = "ruff_dev" version = "0.0.0" publish = false +authors = { workspace = true } edition = { workspace = true } rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } [dependencies] ruff = { path = "../ruff", features = ["schemars"] } ruff_cli = { path = "../ruff_cli" } ruff_diagnostics = { path = "../ruff_diagnostics" } +ruff_formatter = { path = "../ruff_formatter" } +ruff_python_formatter = { path = "../ruff_python_formatter" } +ruff_python_stdlib = { path = "../ruff_python_stdlib" } ruff_textwrap = { path = "../ruff_textwrap" } anyhow = { workspace = true } clap = { workspace = true } +ignore = { workspace = true } +indicatif = "0.17.5" itertools = { workspace = true } libcst = { workspace = true } +log = { workspace = true } once_cell = { workspace = true } pretty_assertions = { version = "1.3.0" } regex = { workspace = true } +rayon = "1.7.0" rustpython-format = { workspace = true } rustpython-parser = { workspace = true } schemars = { workspace = true } serde_json = { workspace = true } +similar = { workspace = true } strum = { workspace = true } strum_macros = { workspace = true } +tempfile = "3.6.0" diff --git a/crates/ruff_dev/src/format_dev.rs b/crates/ruff_dev/src/format_dev.rs new file mode 100644 index 0000000000..9daf4554c5 --- /dev/null +++ b/crates/ruff_dev/src/format_dev.rs @@ -0,0 +1,664 @@ +use anyhow::{bail, Context}; +use clap::{CommandFactory, FromArgMatches}; +use ignore::DirEntry; +use indicatif::ProgressBar; + +use rayon::iter::{IntoParallelIterator, ParallelIterator}; +use ruff::resolver::python_files_in_path; +use ruff::settings::types::{FilePattern, FilePatternSet}; +use ruff_cli::args::CheckArgs; +use ruff_cli::resolve::resolve; +use ruff_formatter::{FormatError, PrintError}; +use ruff_python_formatter::{format_module, FormatModuleError, PyFormatOptions}; +use similar::{ChangeTag, TextDiff}; +use std::fmt::{Display, Formatter}; +use std::fs::File; +use std::io::{BufWriter, Write}; +use std::ops::{Add, AddAssign}; +use std::panic::catch_unwind; +use std::path::{Path, PathBuf}; +use std::process::ExitCode; +use std::sync::mpsc::channel; +use std::time::{Duration, Instant}; +use std::{fmt, fs, io}; +use tempfile::NamedTempFile; + +/// Find files that ruff would check so we can format them. Adapted from `ruff_cli`. +fn ruff_check_paths(dirs: &[PathBuf]) -> anyhow::Result>> { + let args_matches = CheckArgs::command() + .no_binary_name(true) + .get_matches_from(dirs); + let check_args: CheckArgs = CheckArgs::from_arg_matches(&args_matches)?; + let (cli, overrides) = check_args.partition(); + let mut pyproject_config = resolve( + cli.isolated, + cli.config.as_deref(), + &overrides, + cli.stdin_filename.as_deref(), + )?; + // We don't want to format pyproject.toml + pyproject_config.settings.lib.include = FilePatternSet::try_from_vec(vec![ + FilePattern::Builtin("*.py"), + FilePattern::Builtin("*.pyi"), + ]) + .unwrap(); + let (paths, _resolver) = python_files_in_path(&cli.files, &pyproject_config, &overrides)?; + if paths.is_empty() { + bail!("no python files in {:?}", dirs) + } + Ok(paths) +} + +/// Collects statistics over the formatted files to compute the Jaccard index or the similarity +/// index. +/// +/// If we define `B` as the black formatted input and `R` as the ruff formatted output, then +/// * `B∩R`: Unchanged lines, neutral in the diff +/// * `B\R`: Black only lines, minus in the diff +/// * `R\B`: Ruff only lines, plus in the diff +/// +/// The [Jaccard index](https://en.wikipedia.org/wiki/Jaccard_index) can be defined as +/// ```text +/// J(B, R) = |B∩R| / (|B\R| + |R\B| + |B∩R|) +/// ``` +/// which you can read as number unchanged lines in the diff divided by all lines in the diff. If +/// the input is not black formatted, this only becomes a measure for the changes made to the +/// codebase during the initial formatting. +/// +/// Another measure is the similarity index, the percentage of unchanged lines. We compute it as +/// ```text +/// Sim(B, R) = |B∩R| / (|B\R| + |B∩R|) +/// ``` +/// which you can alternatively read as all lines in the input +#[derive(Default, Debug, Copy, Clone)] +pub(crate) struct Statistics { + /// The size of `A\B`, the number of lines only in the input, which we assume to be black + /// formatted + black_input: u32, + /// The size of `B\A`, the number of lines only in the formatted output + ruff_output: u32, + /// The number of matching identical lines + intersection: u32, +} + +impl Statistics { + pub(crate) fn from_versions(black: &str, ruff: &str) -> Self { + if black == ruff { + let intersection = u32::try_from(black.lines().count()).unwrap(); + Self { + black_input: 0, + ruff_output: 0, + intersection, + } + } else { + let diff = TextDiff::from_lines(black, ruff); + let mut statistics = Self::default(); + for change in diff.iter_all_changes() { + match change.tag() { + ChangeTag::Delete => statistics.black_input += 1, + ChangeTag::Insert => statistics.ruff_output += 1, + ChangeTag::Equal => statistics.intersection += 1, + } + } + statistics + } + } + + /// 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 + } + + #[allow(clippy::cast_precision_loss)] + pub(crate) fn similarity_index(&self) -> f32 { + self.intersection as f32 / (self.black_input + self.intersection) as f32 + } +} + +impl Add for Statistics { + type Output = Statistics; + + fn add(self, rhs: Statistics) -> Self::Output { + Statistics { + black_input: self.black_input + rhs.black_input, + ruff_output: self.ruff_output + rhs.ruff_output, + intersection: self.intersection + rhs.intersection, + } + } +} + +impl AddAssign for Statistics { + fn add_assign(&mut self, rhs: Statistics) { + *self = *self + rhs; + } +} + +/// Control the verbosity of the output +#[derive(Copy, Clone, PartialEq, Eq, clap::ValueEnum, Default)] +pub(crate) enum Format { + /// Filenames only + Minimal, + /// Filenames and reduced diff + #[default] + Default, + /// Full diff and invalid code + Full, +} + +#[allow(clippy::struct_excessive_bools)] +#[derive(clap::Args)] +pub(crate) struct Args { + /// Like `ruff check`'s files. See `--multi-project` if you want to format an ecosystem + /// checkout. + pub(crate) files: Vec, + /// Check stability + /// + /// We want to ensure that once formatted content stays the same when formatted again, which is + /// known as formatter stability or formatter idempotency, and that the formatter prints + /// syntactically valid code. As our test cases cover only a limited amount of code, this allows + /// checking entire repositories. + #[arg(long)] + pub(crate) stability_check: bool, + /// Format the files. Without this flag, the python files are not modified + #[arg(long)] + pub(crate) write: bool, + /// Control the verbosity of the output + #[arg(long, default_value_t, value_enum)] + pub(crate) format: Format, + /// Print only the first error and exit, `-x` is same as pytest + #[arg(long, short = 'x')] + pub(crate) exit_first_error: bool, + /// Checks each project inside a directory, useful e.g. if you want to check all of the + /// ecosystem checkouts. + #[arg(long)] + pub(crate) multi_project: bool, + /// Write all errors to this file in addition to stdout. Only used in multi-project mode. + #[arg(long)] + pub(crate) error_file: Option, +} + +pub(crate) fn main(args: &Args) -> anyhow::Result { + let all_success = if args.multi_project { + format_dev_multi_project(args) + } else { + let result = format_dev_project(&args.files, args.stability_check, args.write, true)?; + let error_count = result.error_count(); + + #[allow(clippy::print_stdout)] + { + print!("{}", result.display(args.format)); + println!( + "Found {} stability errors in {} files (similarity index {:.3}) in {:.2}s", + error_count, + result.file_count, + result.statistics.similarity_index(), + result.duration.as_secs_f32(), + ); + } + + error_count == 0 + }; + if all_success { + Ok(ExitCode::SUCCESS) + } else { + Ok(ExitCode::FAILURE) + } +} + +/// Each `path` is one of the `files` in `Args` +enum Message { + Start { + path: PathBuf, + }, + Failed { + path: PathBuf, + error: anyhow::Error, + }, + Finished { + path: PathBuf, + result: CheckRepoResult, + }, +} + +/// Checks a directory of projects +fn format_dev_multi_project(args: &Args) -> bool { + let mut total_errors = 0; + let mut total_files = 0; + let start = Instant::now(); + + rayon::scope(|scope| { + let (sender, receiver) = channel(); + + // Workers, to check is subdirectory in parallel + for base_dir in &args.files { + for dir in base_dir.read_dir().unwrap() { + let path = dir.unwrap().path().clone(); + + let sender = sender.clone(); + + scope.spawn(move |_| { + sender.send(Message::Start { path: path.clone() }).unwrap(); + + match format_dev_project( + &[path.clone()], + args.stability_check, + args.write, + false, + ) { + Ok(result) => sender.send(Message::Finished { result, path }), + Err(error) => sender.send(Message::Failed { error, path }), + } + .unwrap(); + }); + } + } + + // Main thread, writing to stdout + scope.spawn(|_| { + let mut error_file = args.error_file.as_ref().map(|error_file| { + BufWriter::new(File::create(error_file).expect("Couldn't open error file")) + }); + + let bar = ProgressBar::new(args.files.len() as u64); + for message in receiver { + match message { + Message::Start { path } => { + bar.println(path.display().to_string()); + } + Message::Finished { path, result } => { + total_errors += result.error_count(); + total_files += result.file_count; + + bar.println(format!( + "Finished {} with {} files (similarity index {:.3}) in {:.2}s", + path.display(), + result.file_count, + result.statistics.similarity_index(), + result.duration.as_secs_f32(), + )); + bar.println(result.display(args.format).to_string().trim_end()); + if let Some(error_file) = &mut error_file { + write!(error_file, "{}", result.display(args.format)).unwrap(); + } + bar.inc(1); + } + Message::Failed { path, error } => { + bar.println(format!("Failed {}: {}", path.display(), error)); + bar.inc(1); + } + } + } + bar.finish(); + }); + }); + + let duration = start.elapsed(); + + #[allow(clippy::print_stdout)] + { + println!( + "{total_errors} stability errors in {total_files} files in {}s", + duration.as_secs_f32() + ); + } + + total_errors == 0 +} + +fn format_dev_project( + files: &[PathBuf], + stability_check: bool, + write: bool, + progress_bar: bool, +) -> anyhow::Result { + let start = Instant::now(); + + // Find files to check (or in this case, format twice). Adapted from ruff_cli + // First argument is ignored + let paths = ruff_check_paths(files)?; + + let bar = progress_bar.then(|| ProgressBar::new(paths.len() as u64)); + let result_iter = paths + .into_par_iter() + .map(|dir_entry| { + let dir_entry = match dir_entry.context("Iterating the files in the repository failed") + { + Ok(dir_entry) => dir_entry, + Err(err) => return Err(err), + }; + let file = dir_entry.path().to_path_buf(); + // For some reason it does not filter in the beginning + if dir_entry.file_name() == "pyproject.toml" { + return Ok((Ok(Statistics::default()), file)); + } + + let file = dir_entry.path().to_path_buf(); + // Handle panics (mostly in `debug_assert!`) + let result = match catch_unwind(|| format_dev_file(&file, stability_check, write)) { + Ok(result) => result, + Err(panic) => { + if let Some(message) = panic.downcast_ref::() { + Err(CheckFileError::Panic { + message: message.clone(), + }) + } else if let Some(&message) = panic.downcast_ref::<&str>() { + Err(CheckFileError::Panic { + message: message.to_string(), + }) + } else { + Err(CheckFileError::Panic { + // This should not happen, but it can + message: "(Panic didn't set a string message)".to_string(), + }) + } + } + }; + if let Some(bar) = &bar { + bar.inc(1); + } + Ok((result, file)) + }) + .collect::>>()?; + if let Some(bar) = bar { + bar.finish(); + } + + let mut statistics = Statistics::default(); + let mut formatted_counter = 0; + let mut diagnostics = Vec::new(); + for (result, file) in result_iter { + formatted_counter += 1; + match result { + Ok(statistics_file) => statistics += statistics_file, + Err(error) => diagnostics.push(Diagnostic { file, error }), + } + } + + let duration = start.elapsed(); + + Ok(CheckRepoResult { + duration, + file_count: formatted_counter, + diagnostics, + statistics, + }) +} + +/// A compact diff that only shows a header and changes, but nothing unchanged. This makes viewing +/// multiple errors easier. +fn diff_show_only_changes( + writer: &mut Formatter, + formatted: &str, + reformatted: &str, +) -> fmt::Result { + for changes in TextDiff::from_lines(formatted, reformatted) + .unified_diff() + .iter_hunks() + { + for (idx, change) in changes + .iter_changes() + .filter(|change| change.tag() != ChangeTag::Equal) + .enumerate() + { + if idx == 0 { + writeln!(writer, "{}", changes.header())?; + } + write!(writer, "{}", change.tag())?; + writer.write_str(change.value())?; + } + } + Ok(()) +} + +struct CheckRepoResult { + duration: Duration, + file_count: usize, + diagnostics: Vec, + statistics: Statistics, +} + +impl CheckRepoResult { + fn display(&self, format: Format) -> DisplayCheckRepoResult { + DisplayCheckRepoResult { + result: self, + format, + } + } + + /// Count the actual errors excluding invalid input files and io errors + fn error_count(&self) -> usize { + self.diagnostics + .iter() + .filter(|diagnostics| !diagnostics.error.is_success()) + .count() + } +} + +struct DisplayCheckRepoResult<'a> { + result: &'a CheckRepoResult, + format: Format, +} + +impl Display for DisplayCheckRepoResult<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + for diagnostic in &self.result.diagnostics { + write!(f, "{}", diagnostic.display(self.format))?; + } + Ok(()) + } +} + +#[derive(Debug)] +struct Diagnostic { + file: PathBuf, + error: CheckFileError, +} + +impl Diagnostic { + fn display(&self, format: Format) -> DisplayDiagnostic { + DisplayDiagnostic { + diagnostic: self, + format, + } + } +} + +struct DisplayDiagnostic<'a> { + format: Format, + diagnostic: &'a Diagnostic, +} + +impl Display for DisplayDiagnostic<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let Diagnostic { file, error } = &self.diagnostic; + + match error { + CheckFileError::Unstable { + formatted, + reformatted, + } => { + writeln!(f, "Unstable formatting {}", file.display())?; + match self.format { + Format::Minimal => {} + Format::Default => { + diff_show_only_changes(f, formatted, reformatted)?; + } + Format::Full => { + let diff = TextDiff::from_lines(formatted.as_str(), reformatted.as_str()) + .unified_diff() + .header("Formatted once", "Formatted twice") + .to_string(); + writeln!( + f, + r#"Reformatting the formatted code a second time resulted in formatting changes. +--- +{diff}--- + +Formatted once: +--- +{formatted}--- + +Formatted twice: +--- +{reformatted}---\n"#, + )?; + } + } + } + CheckFileError::Panic { message } => { + writeln!(f, "Panic {}: {}", file.display(), message)?; + } + CheckFileError::SyntaxErrorInInput(error) => { + writeln!(f, "Syntax error in {}: {}", file.display(), error)?; + } + CheckFileError::SyntaxErrorInOutput { formatted, error } => { + writeln!( + f, + "Formatter generated invalid syntax {}: {}", + file.display(), + error + )?; + if self.format == Format::Full { + writeln!(f, "---\n{formatted}\n---\n")?; + } + } + CheckFileError::FormatError(error) => { + writeln!(f, "Formatter error for {}: {}", file.display(), error)?; + } + CheckFileError::PrintError(error) => { + writeln!(f, "Printer error for {}: {}", file.display(), error)?; + } + CheckFileError::IoError(error) => { + writeln!(f, "Error reading {}: {}", file.display(), error)?; + } + #[cfg(not(debug_assertions))] + CheckFileError::Slow(duration) => { + writeln!( + f, + "Slow formatting {}: Formatting the file took {}ms", + file.display(), + duration.as_millis() + )?; + } + } + Ok(()) + } +} + +#[derive(Debug)] +enum CheckFileError { + /// First and second pass of the formatter are different + Unstable { + formatted: String, + reformatted: String, + }, + /// The input file was already invalid (not a bug) + SyntaxErrorInInput(FormatModuleError), + /// The formatter introduced a syntax error + SyntaxErrorInOutput { + formatted: String, + error: FormatModuleError, + }, + /// The formatter failed (bug) + FormatError(FormatError), + /// The printer failed (bug) + PrintError(PrintError), + /// Failed to read the file, this sometimes happens e.g. with strange filenames (not a bug) + IoError(io::Error), + /// From `catch_unwind` + Panic { message: String }, + + /// Formatting a file took too long + #[cfg(not(debug_assertions))] + Slow(Duration), +} + +impl CheckFileError { + /// Returns `false` if this is a formatter bug or `true` is if it is something outside of ruff + fn is_success(&self) -> bool { + match self { + CheckFileError::SyntaxErrorInInput(_) | CheckFileError::IoError(_) => true, + CheckFileError::Unstable { .. } + | CheckFileError::SyntaxErrorInOutput { .. } + | CheckFileError::FormatError(_) + | CheckFileError::PrintError(_) + | CheckFileError::Panic { .. } => false, + #[cfg(not(debug_assertions))] + CheckFileError::Slow(_) => false, + } + } +} + +impl From for CheckFileError { + fn from(value: io::Error) -> Self { + Self::IoError(value) + } +} + +fn format_dev_file( + input_path: &Path, + stability_check: bool, + write: bool, +) -> Result { + let content = fs::read_to_string(input_path)?; + #[cfg(not(debug_assertions))] + let start = Instant::now(); + let printed = match format_module(&content, PyFormatOptions::default()) { + Ok(printed) => printed, + Err(err @ (FormatModuleError::LexError(_) | FormatModuleError::ParseError(_))) => { + return Err(CheckFileError::SyntaxErrorInInput(err)); + } + Err(FormatModuleError::FormatError(err)) => { + return Err(CheckFileError::FormatError(err)); + } + Err(FormatModuleError::PrintError(err)) => { + return Err(CheckFileError::PrintError(err)); + } + }; + let formatted = printed.as_code(); + #[cfg(not(debug_assertions))] + let format_duration = Instant::now() - start; + + if write && content != formatted { + // Simple atomic write. + // The file is in a directory so it must have a parent. Surprisingly `DirEntry` doesn't + // give us access without unwrap + let mut file = NamedTempFile::new_in(input_path.parent().unwrap())?; + file.write_all(formatted.as_bytes())?; + // "If a file exists at the target path, persist will atomically replace it." + file.persist(input_path).map_err(|error| error.error)?; + } + + if stability_check { + let reformatted = match format_module(formatted, PyFormatOptions::default()) { + Ok(reformatted) => reformatted, + Err(err @ (FormatModuleError::LexError(_) | FormatModuleError::ParseError(_))) => { + return Err(CheckFileError::SyntaxErrorInOutput { + formatted: formatted.to_string(), + error: err, + }); + } + Err(FormatModuleError::FormatError(err)) => { + return Err(CheckFileError::FormatError(err)); + } + Err(FormatModuleError::PrintError(err)) => { + return Err(CheckFileError::PrintError(err)); + } + }; + + if reformatted.as_code() != formatted { + return Err(CheckFileError::Unstable { + formatted: formatted.to_string(), + reformatted: reformatted.into_code(), + }); + } + } + + #[cfg(not(debug_assertions))] + if format_duration > Duration::from_millis(50) { + return Err(CheckFileError::Slow(format_duration)); + } + + Ok(Statistics::from_versions(&content, formatted)) +} diff --git a/crates/ruff_dev/src/generate_all.rs b/crates/ruff_dev/src/generate_all.rs index 3eb1b0adfa..6b8212ce64 100644 --- a/crates/ruff_dev/src/generate_all.rs +++ b/crates/ruff_dev/src/generate_all.rs @@ -21,7 +21,7 @@ pub(crate) enum Mode { /// Don't write to the file, check if the file is up-to-date and error if not. Check, - /// Write the generated help to stdout (rather than to `docs/configuration.md`). + /// Write the generated help to stdout. DryRun, } diff --git a/crates/ruff_dev/src/generate_cli_help.rs b/crates/ruff_dev/src/generate_cli_help.rs index 25e2417467..420927495f 100644 --- a/crates/ruff_dev/src/generate_cli_help.rs +++ b/crates/ruff_dev/src/generate_cli_help.rs @@ -119,7 +119,7 @@ fn check_help_text() -> String { } #[cfg(test)] -mod test { +mod tests { use anyhow::Result; use crate::generate_all::Mode; diff --git a/crates/ruff_dev/src/generate_json_schema.rs b/crates/ruff_dev/src/generate_json_schema.rs index 147638dba6..d284783d9f 100644 --- a/crates/ruff_dev/src/generate_json_schema.rs +++ b/crates/ruff_dev/src/generate_json_schema.rs @@ -32,15 +32,20 @@ pub(crate) fn main(args: &Args) -> Result<()> { Mode::Check => { let current = fs::read_to_string(schema_path)?; if current == schema_string { - println!("up-to-date: {filename}"); + println!("Up-to-date: {filename}"); } else { let comparison = StrComparison::new(¤t, &schema_string); bail!("{filename} changed, please run `{REGENERATE_ALL_COMMAND}`:\n{comparison}"); } } Mode::Write => { - let file = schema_path; - fs::write(file, schema_string.as_bytes())?; + let current = fs::read_to_string(&schema_path)?; + if current == schema_string { + println!("Up-to-date: {filename}"); + } else { + println!("Updating: {filename}"); + fs::write(schema_path, schema_string.as_bytes())?; + } } } @@ -48,15 +53,21 @@ pub(crate) fn main(args: &Args) -> Result<()> { } #[cfg(test)] -mod test { +mod tests { use anyhow::Result; + use std::env; use crate::generate_all::Mode; use super::{main, Args}; - #[test] + #[cfg_attr(not(feature = "unreachable-code"), test)] fn test_generate_json_schema() -> Result<()> { - main(&Args { mode: Mode::Check }) + let mode = if env::var("RUFF_UPDATE_SCHEMA").as_deref() == Ok("1") { + Mode::Write + } else { + Mode::Check + }; + main(&Args { mode }) } } diff --git a/crates/ruff_dev/src/generate_options.rs b/crates/ruff_dev/src/generate_options.rs index 4e6bfe280f..c2e58bb836 100644 --- a/crates/ruff_dev/src/generate_options.rs +++ b/crates/ruff_dev/src/generate_options.rs @@ -1,4 +1,6 @@ -//! Generate a Markdown-compatible listing of configuration options. +//! Generate a Markdown-compatible listing of configuration options for `pyproject.toml`. +//! +//! Used for . use itertools::Itertools; use ruff::settings::options::Options; @@ -46,18 +48,24 @@ pub(crate) fn generate() -> String { // Generate all the top-level fields. for (name, entry) in &sorted_options { - let OptionEntry::Field(field) = entry else { continue; }; + let OptionEntry::Field(field) = entry else { + continue; + }; emit_field(&mut output, name, field, None); output.push_str("---\n\n"); } // Generate all the sub-groups. for (group_name, entry) in &sorted_options { - let OptionEntry::Group(fields) = entry else { continue; }; - output.push_str(&format!("### `{group_name}`\n")); + let OptionEntry::Group(fields) = entry else { + continue; + }; + output.push_str(&format!("### {group_name}\n")); output.push('\n'); for (name, entry) in fields.iter().sorted_by_key(|(name, _)| name) { - let OptionEntry::Field(field) = entry else { continue; }; + let OptionEntry::Field(field) = entry else { + continue; + }; emit_field(&mut output, name, field, Some(group_name)); output.push_str("---\n\n"); } diff --git a/crates/ruff_dev/src/generate_rules_table.rs b/crates/ruff_dev/src/generate_rules_table.rs index 4d29bfea2b..5a77acee2d 100644 --- a/crates/ruff_dev/src/generate_rules_table.rs +++ b/crates/ruff_dev/src/generate_rules_table.rs @@ -1,24 +1,36 @@ //! Generate a Markdown-compatible table of supported lint rules. +//! +//! Used for . use itertools::Itertools; use strum::IntoEnumIterator; -use ruff::registry::{Linter, Rule, RuleNamespace, UpstreamCategory}; +use ruff::registry::{Linter, Rule, RuleNamespace}; use ruff::settings::options::Options; +use ruff::upstream_categories::UpstreamCategoryAndPrefix; use ruff_diagnostics::AutofixKind; const FIX_SYMBOL: &str = "🛠"; +const NURSERY_SYMBOL: &str = "🌅"; fn generate_table(table_out: &mut String, rules: impl IntoIterator, linter: &Linter) { - table_out.push_str("| Code | Name | Message | Fix |"); + table_out.push_str("| Code | Name | Message | |"); table_out.push('\n'); - table_out.push_str("| ---- | ---- | ------- | --- |"); + table_out.push_str("| ---- | ---- | ------- | ------: |"); table_out.push('\n'); for rule in rules { let fix_token = match rule.autofixable() { - AutofixKind::None => "", - AutofixKind::Always | AutofixKind::Sometimes => FIX_SYMBOL, + AutofixKind::Always | AutofixKind::Sometimes => { + format!("{FIX_SYMBOL}") + } + AutofixKind::None => format!("{FIX_SYMBOL}"), }; + let nursery_token = if rule.is_nursery() { + format!("{NURSERY_SYMBOL}") + } else { + format!("{NURSERY_SYMBOL}") + }; + let status_token = format!("{fix_token} {nursery_token}"); let rule_name = rule.as_ref(); @@ -32,7 +44,7 @@ fn generate_table(table_out: &mut String, rules: impl IntoIterator, .then_some(format_args!("[{rule_name}](rules/{rule_name}.md)")) .unwrap_or(format_args!("{rule_name}")), rule.message_formats()[0], - fix_token + status_token, )); table_out.push('\n'); } @@ -41,14 +53,26 @@ fn generate_table(table_out: &mut String, rules: impl IntoIterator, pub(crate) fn generate() -> String { // Generate the table string. - let mut table_out = format!("The {FIX_SYMBOL} emoji indicates that a rule is automatically fixable by the `--fix` command-line option.\n\n"); + let mut table_out = String::new(); + + table_out.push_str(&format!( + "The {FIX_SYMBOL} emoji indicates that a rule is automatically fixable by the `--fix` command-line option.")); + table_out.push('\n'); + table_out.push('\n'); + + table_out.push_str(&format!( + "The {NURSERY_SYMBOL} emoji indicates that a rule is part of the [\"nursery\"](../faq/#what-is-the-nursery)." + )); + table_out.push('\n'); + table_out.push('\n'); + for linter in Linter::iter() { let codes_csv: String = match linter.common_prefix() { "" => linter .upstream_categories() .unwrap() .iter() - .map(|UpstreamCategory(prefix, ..)| prefix.short_code()) + .map(|c| c.prefix) .join(", "), prefix => prefix.to_string(), }; @@ -93,19 +117,23 @@ pub(crate) fn generate() -> String { table_out.push('\n'); } - if let Some(categories) = linter.upstream_categories() { - for UpstreamCategory(prefix, name) in categories { - table_out.push_str(&format!( - "#### {name} ({}{})", - linter.common_prefix(), - prefix.short_code() - )); + let rules_by_upstream_category = linter + .all_rules() + .map(|rule| (rule.upstream_category(&linter), rule)) + .into_group_map(); + + if rules_by_upstream_category.len() > 1 { + 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})")); + } table_out.push('\n'); table_out.push('\n'); - generate_table(&mut table_out, prefix, &linter); + generate_table(&mut table_out, rules.clone(), &linter); } } else { - generate_table(&mut table_out, &linter, &linter); + generate_table(&mut table_out, linter.all_rules(), &linter); } } diff --git a/crates/ruff_dev/src/main.rs b/crates/ruff_dev/src/main.rs index c8beed5fb1..c981d8c8a2 100644 --- a/crates/ruff_dev/src/main.rs +++ b/crates/ruff_dev/src/main.rs @@ -4,7 +4,11 @@ use anyhow::Result; use clap::{Parser, Subcommand}; +use ruff::logging::{set_up_logging, LogLevel}; +use ruff_cli::check; +use std::process::ExitCode; +mod format_dev; mod generate_all; mod generate_cli_help; mod generate_docs; @@ -27,6 +31,7 @@ struct Args { } #[derive(Subcommand)] +#[allow(clippy::large_enum_variant)] enum Command { /// Run all code and documentation generation steps. GenerateAll(generate_all::Args), @@ -48,22 +53,56 @@ enum Command { PrintTokens(print_tokens::Args), /// Run round-trip source code generation on a given Python file. RoundTrip(round_trip::Args), + /// Run a ruff command n times for profiling/benchmarking + Repeat { + #[clap(flatten)] + args: ruff_cli::args::CheckArgs, + #[clap(flatten)] + log_level_args: ruff_cli::args::LogLevelArgs, + /// Run this many times + #[clap(long)] + repeat: usize, + }, + /// Several utils related to the formatter which can be run on one or more repositories. The + /// selected set of files in a repository is the same as for `ruff check`. + /// + /// * Check formatter stability: Format a repository twice and ensure that it looks that the + /// first and second formatting look the same. + /// * Format: Format the files in a repository to be able to check them with `git diff` + /// * Statistics: The subcommand the Jaccard index between the (assumed to be black formatted) + /// input and the ruff formatted output + FormatDev(format_dev::Args), } -fn main() -> Result<()> { +fn main() -> Result { let args = Args::parse(); #[allow(clippy::print_stdout)] - match &args.command { - Command::GenerateAll(args) => generate_all::main(args)?, - Command::GenerateJSONSchema(args) => generate_json_schema::main(args)?, + 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()), Command::GenerateOptions => println!("{}", generate_options::generate()), - Command::GenerateCliHelp(args) => generate_cli_help::main(args)?, - Command::GenerateDocs(args) => generate_docs::main(args)?, - Command::PrintAST(args) => print_ast::main(args)?, - Command::PrintCST(args) => print_cst::main(args)?, - Command::PrintTokens(args) => print_tokens::main(args)?, - Command::RoundTrip(args) => round_trip::main(args)?, + Command::GenerateCliHelp(args) => generate_cli_help::main(&args)?, + Command::GenerateDocs(args) => generate_docs::main(&args)?, + Command::PrintAST(args) => print_ast::main(&args)?, + Command::PrintCST(args) => print_cst::main(&args)?, + Command::PrintTokens(args) => print_tokens::main(&args)?, + Command::RoundTrip(args) => round_trip::main(&args)?, + Command::Repeat { + args, + repeat, + log_level_args, + } => { + let log_level = LogLevel::from(&log_level_args); + set_up_logging(&log_level)?; + for _ in 0..repeat { + check(args.clone(), log_level)?; + } + } + Command::FormatDev(args) => { + let exit_code = format_dev::main(&args)?; + return Ok(exit_code); + } } - Ok(()) + Ok(ExitCode::SUCCESS) } diff --git a/crates/ruff_dev/src/round_trip.rs b/crates/ruff_dev/src/round_trip.rs index 4cc4f36c59..394ec4b190 100644 --- a/crates/ruff_dev/src/round_trip.rs +++ b/crates/ruff_dev/src/round_trip.rs @@ -1,4 +1,4 @@ -//! Run round-trip source code generation on a given Python file. +//! Run round-trip source code generation on a given Python or Jupyter notebook file. #![allow(clippy::print_stdout, clippy::print_stderr)] use std::fs; @@ -6,17 +6,24 @@ use std::path::PathBuf; use anyhow::Result; +use ruff::jupyter; use ruff::round_trip; +use ruff_python_stdlib::path::is_jupyter_notebook; #[derive(clap::Args)] pub(crate) struct Args { - /// Python file to round-trip. + /// Python or Jupyter notebook file to round-trip. #[arg(required = true)] file: PathBuf, } pub(crate) fn main(args: &Args) -> Result<()> { - let contents = fs::read_to_string(&args.file)?; - println!("{}", round_trip(&contents, &args.file.to_string_lossy())?); + let path = args.file.as_path(); + if is_jupyter_notebook(path) { + println!("{}", jupyter::round_trip(path)?); + } else { + let contents = fs::read_to_string(&args.file)?; + println!("{}", round_trip(&contents, &args.file.to_string_lossy())?); + } Ok(()) } diff --git a/crates/ruff_diagnostics/Cargo.toml b/crates/ruff_diagnostics/Cargo.toml index 980abbbc51..9921f41124 100644 --- a/crates/ruff_diagnostics/Cargo.toml +++ b/crates/ruff_diagnostics/Cargo.toml @@ -2,8 +2,13 @@ name = "ruff_diagnostics" version = "0.0.0" publish = false +authors = { workspace = true } edition = { workspace = true } rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } [lib] diff --git a/crates/ruff_diagnostics/src/fix.rs b/crates/ruff_diagnostics/src/fix.rs index ae54282d04..c96b428c8d 100644 --- a/crates/ruff_diagnostics/src/fix.rs +++ b/crates/ruff_diagnostics/src/fix.rs @@ -66,7 +66,7 @@ impl Fix { )] pub fn unspecified_edits(edit: Edit, rest: impl IntoIterator) -> Self { Self { - edits: std::iter::once(edit).chain(rest.into_iter()).collect(), + edits: std::iter::once(edit).chain(rest).collect(), applicability: Applicability::Unspecified, isolation_level: IsolationLevel::default(), } @@ -84,7 +84,7 @@ impl Fix { /// Create a new [`Fix`] with [automatic applicability](Applicability::Automatic) from multiple [`Edit`] elements. pub fn automatic_edits(edit: Edit, rest: impl IntoIterator) -> Self { Self { - edits: std::iter::once(edit).chain(rest.into_iter()).collect(), + edits: std::iter::once(edit).chain(rest).collect(), applicability: Applicability::Automatic, isolation_level: IsolationLevel::default(), } @@ -102,7 +102,7 @@ impl Fix { /// Create a new [`Fix`] with [suggested applicability](Applicability::Suggested) from multiple [`Edit`] elements. pub fn suggested_edits(edit: Edit, rest: impl IntoIterator) -> Self { Self { - edits: std::iter::once(edit).chain(rest.into_iter()).collect(), + edits: std::iter::once(edit).chain(rest).collect(), applicability: Applicability::Suggested, isolation_level: IsolationLevel::default(), } @@ -120,7 +120,7 @@ impl Fix { /// Create a new [`Fix`] with [manual applicability](Applicability::Manual) from multiple [`Edit`] elements. pub fn manual_edits(edit: Edit, rest: impl IntoIterator) -> Self { Self { - edits: std::iter::once(edit).chain(rest.into_iter()).collect(), + edits: std::iter::once(edit).chain(rest).collect(), applicability: Applicability::Manual, isolation_level: IsolationLevel::default(), } diff --git a/crates/ruff_formatter/Cargo.toml b/crates/ruff_formatter/Cargo.toml index 593b47e359..17d2a6b549 100644 --- a/crates/ruff_formatter/Cargo.toml +++ b/crates/ruff_formatter/Cargo.toml @@ -2,8 +2,13 @@ name = "ruff_formatter" version = "0.0.0" publish = false +authors = { workspace = true } edition = { workspace = true } rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } [dependencies] ruff_text_size = { workspace = true } diff --git a/crates/ruff_formatter/src/arguments.rs b/crates/ruff_formatter/src/arguments.rs index d2fd95eecd..01ee8f91e8 100644 --- a/crates/ruff_formatter/src/arguments.rs +++ b/crates/ruff_formatter/src/arguments.rs @@ -96,7 +96,7 @@ impl Copy for Arguments<'_, Context> {} impl Clone for Arguments<'_, Context> { fn clone(&self) -> Self { - Self(self.0) + *self } } diff --git a/crates/ruff_formatter/src/buffer.rs b/crates/ruff_formatter/src/buffer.rs index aa90a73a9a..13cb542085 100644 --- a/crates/ruff_formatter/src/buffer.rs +++ b/crates/ruff_formatter/src/buffer.rs @@ -29,7 +29,6 @@ pub trait Buffer { /// /// assert_eq!(buffer.into_vec(), vec![FormatElement::StaticText { text: "test" }]); /// ``` - /// fn write_element(&mut self, element: FormatElement) -> FormatResult<()>; /// Returns a slice containing all elements written into this buffer. diff --git a/crates/ruff_formatter/src/builders.rs b/crates/ruff_formatter/src/builders.rs index 24879eb868..8e5ed0b8a0 100644 --- a/crates/ruff_formatter/src/builders.rs +++ b/crates/ruff_formatter/src/builders.rs @@ -528,7 +528,22 @@ impl Format for LineSuffixBoundary { /// use ruff_formatter::prelude::*; /// use ruff_formatter::{format, write, LineWidth}; /// -/// enum SomeLabelId {} +/// #[derive(Debug, Copy, Clone)] +/// enum MyLabels { +/// Main +/// } +/// +/// impl tag::LabelDefinition for MyLabels { +/// fn value(&self) -> u64 { +/// *self as u64 +/// } +/// +/// fn name(&self) -> &'static str { +/// match self { +/// Self::Main => "Main" +/// } +/// } +/// } /// /// # fn main() -> FormatResult<()> { /// let formatted = format!( @@ -537,24 +552,24 @@ impl Format for LineSuffixBoundary { /// let mut recording = f.start_recording(); /// write!(recording, [ /// labelled( -/// LabelId::of::(), +/// LabelId::of(MyLabels::Main), /// &text("'I have a label'") /// ) /// ])?; /// /// let recorded = recording.stop(); /// -/// let is_labelled = recorded.first().map_or(false, |element| element.has_label(LabelId::of::())); +/// let is_labelled = recorded.first().map_or(false, |element| element.has_label(LabelId::of(MyLabels::Main))); /// /// if is_labelled { -/// write!(f, [text(" has label SomeLabelId")]) +/// write!(f, [text(" has label `Main`")]) /// } else { -/// write!(f, [text(" doesn't have label SomeLabelId")]) +/// write!(f, [text(" doesn't have label `Main`")]) /// } /// })] /// )?; /// -/// assert_eq!("'I have a label' has label SomeLabelId", formatted.print()?.as_code()); +/// assert_eq!("'I have a label' has label `Main`", formatted.print()?.as_code()); /// # Ok(()) /// # } /// ``` @@ -1395,7 +1410,7 @@ impl Format for Group<'_, Context> { impl std::fmt::Debug for Group<'_, Context> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("GroupElements") + f.debug_struct("Group") .field("group_id", &self.group_id) .field("should_expand", &self.should_expand) .field("content", &"{{content}}") @@ -1403,6 +1418,135 @@ impl std::fmt::Debug for Group<'_, Context> { } } +/// Sets the `condition` for the group. The element will behave as a regular group if `condition` is met, +/// and as *ungrouped* content if the condition is not met. +/// +/// ## Examples +/// +/// Only expand before operators if the parentheses are necessary. +/// +/// ``` +/// # use ruff_formatter::prelude::*; +/// # use ruff_formatter::{format, format_args, LineWidth, SimpleFormatOptions}; +/// +/// # fn main() -> FormatResult<()> { +/// use ruff_formatter::Formatted; +/// let content = format_with(|f| { +/// let parentheses_id = f.group_id("parentheses"); +/// group(&format_args![ +/// if_group_breaks(&text("(")), +/// indent_if_group_breaks(&format_args![ +/// soft_line_break(), +/// conditional_group(&format_args![ +/// text("'aaaaaaa'"), +/// soft_line_break_or_space(), +/// text("+"), +/// space(), +/// fits_expanded(&conditional_group(&format_args![ +/// text("["), +/// soft_block_indent(&format_args![ +/// text("'Good morning!',"), +/// soft_line_break_or_space(), +/// text("'How are you?'"), +/// ]), +/// text("]"), +/// ], tag::Condition::if_group_fits_on_line(parentheses_id))), +/// soft_line_break_or_space(), +/// text("+"), +/// space(), +/// conditional_group(&format_args![ +/// text("'bbbb'"), +/// soft_line_break_or_space(), +/// text("and"), +/// space(), +/// text("'c'") +/// ], tag::Condition::if_group_fits_on_line(parentheses_id)) +/// ], tag::Condition::if_breaks()), +/// ], parentheses_id), +/// soft_line_break(), +/// if_group_breaks(&text(")")) +/// ]) +/// .with_group_id(Some(parentheses_id)) +/// .fmt(f) +/// }); +/// +/// let formatted = format!(SimpleFormatContext::default(), [content])?; +/// let document = formatted.into_document(); +/// +/// // All content fits +/// let all_fits = Formatted::new(document.clone(), SimpleFormatContext::new(SimpleFormatOptions { +/// line_width: LineWidth::try_from(65).unwrap(), +/// ..SimpleFormatOptions::default() +/// })); +/// +/// assert_eq!( +/// "'aaaaaaa' + ['Good morning!', 'How are you?'] + 'bbbb' and 'c'", +/// all_fits.print()?.as_code() +/// ); +/// +/// // The parentheses group fits, because it can expand the list, +/// let list_expanded = Formatted::new(document.clone(), SimpleFormatContext::new(SimpleFormatOptions { +/// line_width: LineWidth::try_from(21).unwrap(), +/// ..SimpleFormatOptions::default() +/// })); +/// +/// assert_eq!( +/// "'aaaaaaa' + [\n\t'Good morning!',\n\t'How are you?'\n] + 'bbbb' and 'c'", +/// list_expanded.print()?.as_code() +/// ); +/// +/// // It is necessary to split all groups to fit the content +/// let all_expanded = Formatted::new(document, SimpleFormatContext::new(SimpleFormatOptions { +/// line_width: LineWidth::try_from(11).unwrap(), +/// ..SimpleFormatOptions::default() +/// })); +/// +/// assert_eq!( +/// "(\n\t'aaaaaaa'\n\t+ [\n\t\t'Good morning!',\n\t\t'How are you?'\n\t]\n\t+ 'bbbb'\n\tand 'c'\n)", +/// all_expanded.print()?.as_code() +/// ); +/// # Ok(()) +/// # } +/// ``` +#[inline] +pub fn conditional_group( + content: &Content, + condition: Condition, +) -> ConditionalGroup +where + Content: Format, +{ + ConditionalGroup { + content: Argument::new(content), + condition, + } +} + +#[derive(Clone)] +pub struct ConditionalGroup<'content, Context> { + content: Argument<'content, Context>, + condition: Condition, +} + +impl Format for ConditionalGroup<'_, Context> { + fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { + f.write_element(FormatElement::Tag(StartConditionalGroup( + tag::ConditionalGroup::new(self.condition), + )))?; + f.write_fmt(Arguments::from(&self.content))?; + f.write_element(FormatElement::Tag(EndConditionalGroup)) + } +} + +impl std::fmt::Debug for ConditionalGroup<'_, Context> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ConditionalGroup") + .field("condition", &self.condition) + .field("content", &"{{content}}") + .finish() + } +} + /// IR element that forces the parent group to print in expanded mode. /// /// Has no effect if used outside of a group or element that introduce implicit groups (fill element). @@ -1826,6 +1970,86 @@ impl std::fmt::Debug for IndentIfGroupBreaks<'_, Context> { } } +/// Changes the definition of *fits* for `content`. Instead of measuring it in *flat*, measure it with +/// all line breaks expanded and test if no line exceeds the line width. The [`FitsExpanded`] acts +/// as a expands boundary similar to best fitting, meaning that a [hard_line_break] will not cause the parent group to expand. +/// +/// Useful in conjunction with a group with a condition. +/// +/// ## Examples +/// The outer group with the binary expression remains *flat* regardless of the array expression +/// that spans multiple lines. +/// +/// ``` +/// # use ruff_formatter::{format, format_args, LineWidth, SimpleFormatOptions, write}; +/// # use ruff_formatter::prelude::*; +/// +/// # fn main() -> FormatResult<()> { +/// let content = format_with(|f| { +/// let group_id = f.group_id("header"); +/// +/// write!(f, [ +/// group(&format_args![ +/// text("a"), +/// soft_line_break_or_space(), +/// text("+"), +/// space(), +/// fits_expanded(&group(&format_args![ +/// text("["), +/// soft_block_indent(&format_args![ +/// text("a,"), space(), text("# comment"), expand_parent(), soft_line_break_or_space(), +/// text("b") +/// ]), +/// text("]") +/// ])) +/// ]), +/// ]) +/// }); +/// +/// let formatted = format!(SimpleFormatContext::default(), [content])?; +/// +/// assert_eq!( +/// "a + [\n\ta, # comment\n\tb\n]", +/// formatted.print()?.as_code() +/// ); +/// # Ok(()) +/// # } +/// ``` +pub fn fits_expanded(content: &Content) -> FitsExpanded +where + Content: Format, +{ + FitsExpanded { + content: Argument::new(content), + condition: None, + } +} + +#[derive(Clone)] +pub struct FitsExpanded<'a, Context> { + content: Argument<'a, Context>, + condition: Option, +} + +impl FitsExpanded<'_, Context> { + /// Sets a `condition` to when the content should fit in expanded mode. The content uses the regular fits + /// definition if the `condition` is not met. + pub fn with_condition(mut self, condition: Option) -> Self { + self.condition = condition; + self + } +} + +impl Format for FitsExpanded<'_, Context> { + fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { + f.write_element(FormatElement::Tag(StartFitsExpanded( + tag::FitsExpanded::new().with_condition(self.condition), + )))?; + f.write_fmt(Arguments::from(&self.content))?; + f.write_element(FormatElement::Tag(EndFitsExpanded)) + } +} + /// Utility for formatting some content with an inline lambda function. #[derive(Copy, Clone)] pub struct FormatWith { @@ -2131,11 +2355,11 @@ impl<'a, 'buf, Context> FillBuilder<'a, 'buf, Context> { /// The first variant is the most flat, and the last is the most expanded variant. /// See [`best_fitting!`] macro for a more in-detail documentation #[derive(Copy, Clone)] -pub struct FormatBestFitting<'a, Context> { +pub struct BestFitting<'a, Context> { variants: Arguments<'a, Context>, } -impl<'a, Context> FormatBestFitting<'a, Context> { +impl<'a, Context> BestFitting<'a, Context> { /// Creates a new best fitting IR with the given variants. The method itself isn't unsafe /// but it is to discourage people from using it because the printer will panic if /// the slice doesn't contain at least the least and most expanded variants. @@ -2154,7 +2378,7 @@ impl<'a, Context> FormatBestFitting<'a, Context> { } } -impl Format for FormatBestFitting<'_, Context> { +impl Format for BestFitting<'_, Context> { fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { let mut buffer = VecBuffer::new(f.state_mut()); let variants = self.variants.items(); @@ -2172,9 +2396,11 @@ impl Format for FormatBestFitting<'_, Context> { // SAFETY: The constructor guarantees that there are always at least two variants. It's, therefore, // safe to call into the unsafe `from_vec_unchecked` function let element = unsafe { - FormatElement::BestFitting(format_element::BestFitting::from_vec_unchecked( - formatted_variants, - )) + FormatElement::BestFitting { + variants: format_element::BestFittingVariants::from_vec_unchecked( + formatted_variants, + ), + } }; f.write_element(element) diff --git a/crates/ruff_formatter/src/format_element.rs b/crates/ruff_formatter/src/format_element.rs index a7dd7dedae..7506c9cb75 100644 --- a/crates/ruff_formatter/src/format_element.rs +++ b/crates/ruff_formatter/src/format_element.rs @@ -6,7 +6,7 @@ use std::hash::{Hash, Hasher}; use std::ops::Deref; use std::rc::Rc; -use crate::format_element::tag::{LabelId, Tag}; +use crate::format_element::tag::{GroupMode, LabelId, Tag}; use crate::source_code::SourceCodeSlice; use crate::TagKind; use ruff_text_size::TextSize; @@ -57,7 +57,7 @@ pub enum FormatElement { /// A list of different variants representing the same content. The printer picks the best fitting content. /// Line breaks inside of a best fitting don't propagate to parent groups. - BestFitting(BestFitting), + BestFitting { variants: BestFittingVariants }, /// A [Tag] that marks the start/end of some content to which some special formatting is applied. Tag(Tag), @@ -84,9 +84,10 @@ impl std::fmt::Debug for FormatElement { .field(contains_newlines) .finish(), FormatElement::LineSuffixBoundary => write!(fmt, "LineSuffixBoundary"), - FormatElement::BestFitting(best_fitting) => { - fmt.debug_tuple("BestFitting").field(&best_fitting).finish() - } + FormatElement::BestFitting { variants } => fmt + .debug_struct("BestFitting") + .field("variants", variants) + .finish(), FormatElement::Interned(interned) => { fmt.debug_list().entries(interned.deref()).finish() } @@ -134,6 +135,15 @@ impl PrintMode { } } +impl From for PrintMode { + fn from(value: GroupMode) -> Self { + match value { + GroupMode::Flat => PrintMode::Flat, + GroupMode::Expand | GroupMode::Propagated => PrintMode::Expanded, + } + } +} + #[derive(Clone)] pub struct Interned(Rc<[FormatElement]>); @@ -256,7 +266,10 @@ impl FormatElements for FormatElement { FormatElement::Interned(interned) => interned.will_break(), // Traverse into the most flat version because the content is guaranteed to expand when even // the most flat version contains some content that forces a break. - FormatElement::BestFitting(best_fitting) => best_fitting.most_flat().will_break(), + FormatElement::BestFitting { + variants: best_fitting, + .. + } => best_fitting.most_flat().will_break(), FormatElement::LineSuffixBoundary | FormatElement::Space | FormatElement::Tag(_) @@ -284,19 +297,13 @@ impl FormatElements for FormatElement { } } -/// Provides the printer with different representations for the same element so that the printer -/// can pick the best fitting variant. -/// -/// Best fitting is defined as the variant that takes the most horizontal space but fits on the line. -#[derive(Clone, Eq, PartialEq)] -pub struct BestFitting { - /// The different variants for this element. - /// The first element is the one that takes up the most space horizontally (the most flat), - /// The last element takes up the least space horizontally (but most horizontal space). - variants: Box<[Box<[FormatElement]>]>, -} +/// The different variants for this element. +/// The first element is the one that takes up the most space horizontally (the most flat), +/// The last element takes up the least space horizontally (but most horizontal space). +#[derive(Clone, Eq, PartialEq, Debug)] +pub struct BestFittingVariants(Box<[Box<[FormatElement]>]>); -impl BestFitting { +impl BestFittingVariants { /// Creates a new best fitting IR with the given variants. The method itself isn't unsafe /// but it is to discourage people from using it because the printer will panic if /// the slice doesn't contain at least the least and most expanded variants. @@ -312,33 +319,42 @@ impl BestFitting { "Requires at least the least expanded and most expanded variants" ); - Self { - variants: variants.into_boxed_slice(), - } + Self(variants.into_boxed_slice()) } /// Returns the most expanded variant pub fn most_expanded(&self) -> &[FormatElement] { - self.variants.last().expect( + self.0.last().expect( "Most contain at least two elements, as guaranteed by the best fitting builder.", ) } - pub fn variants(&self) -> &[Box<[FormatElement]>] { - &self.variants + pub fn as_slice(&self) -> &[Box<[FormatElement]>] { + &self.0 } /// Returns the least expanded variant pub fn most_flat(&self) -> &[FormatElement] { - self.variants.first().expect( + self.0.first().expect( "Most contain at least two elements, as guaranteed by the best fitting builder.", ) } } -impl std::fmt::Debug for BestFitting { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_list().entries(&*self.variants).finish() +impl Deref for BestFittingVariants { + type Target = [Box<[FormatElement]>]; + + fn deref(&self) -> &Self::Target { + self.as_slice() + } +} + +impl<'a> IntoIterator for &'a BestFittingVariants { + type Item = &'a Box<[FormatElement]>; + type IntoIter = std::slice::Iter<'a, Box<[FormatElement]>>; + + fn into_iter(self) -> Self::IntoIter { + self.as_slice().iter() } } @@ -397,7 +413,7 @@ mod sizes { assert_eq_size!(ruff_text_size::TextRange, [u8; 8]); assert_eq_size!(crate::prelude::tag::VerbatimKind, [u8; 8]); assert_eq_size!(crate::prelude::Interned, [u8; 16]); - assert_eq_size!(crate::format_element::BestFitting, [u8; 16]); + assert_eq_size!(crate::format_element::BestFittingVariants, [u8; 16]); #[cfg(not(debug_assertions))] assert_eq_size!(crate::SourceCodeSlice, [u8; 8]); diff --git a/crates/ruff_formatter/src/format_element/document.rs b/crates/ruff_formatter/src/format_element/document.rs index 2b83cb9907..7eb1c37156 100644 --- a/crates/ruff_formatter/src/format_element/document.rs +++ b/crates/ruff_formatter/src/format_element/document.rs @@ -1,5 +1,5 @@ use super::tag::Tag; -use crate::format_element::tag::DedentMode; +use crate::format_element::tag::{Condition, DedentMode}; use crate::prelude::tag::GroupMode; use crate::prelude::*; use crate::printer::LineEnding; @@ -33,12 +33,17 @@ impl Document { #[derive(Debug)] enum Enclosing<'a> { Group(&'a tag::Group), + ConditionalGroup(&'a tag::ConditionalGroup), + FitsExpanded(&'a tag::FitsExpanded), BestFitting, } fn expand_parent(enclosing: &[Enclosing]) { - if let Some(Enclosing::Group(group)) = enclosing.last() { - group.propagate_expand(); + match enclosing.last() { + Some(Enclosing::Group(group)) => group.propagate_expand(), + Some(Enclosing::ConditionalGroup(group)) => group.propagate_expand(), + Some(Enclosing::FitsExpanded(fits_expanded)) => fits_expanded.propagate_expand(), + _ => {} } } @@ -58,6 +63,14 @@ impl Document { Some(Enclosing::Group(group)) => !group.mode().is_flat(), _ => false, }, + FormatElement::Tag(Tag::StartConditionalGroup(group)) => { + enclosing.push(Enclosing::ConditionalGroup(group)); + false + } + FormatElement::Tag(Tag::EndConditionalGroup) => match enclosing.pop() { + Some(Enclosing::ConditionalGroup(group)) => !group.mode().is_flat(), + _ => false, + }, FormatElement::Interned(interned) => match checked_interned.get(interned) { Some(interned_expands) => *interned_expands, None => { @@ -67,10 +80,10 @@ impl Document { interned_expands } }, - FormatElement::BestFitting(best_fitting) => { + FormatElement::BestFitting { variants } => { enclosing.push(Enclosing::BestFitting); - for variant in best_fitting.variants() { + for variant in variants { propagate_expands(variant, enclosing, checked_interned); } @@ -79,6 +92,16 @@ impl Document { enclosing.pop(); continue; } + FormatElement::Tag(Tag::StartFitsExpanded(fits_expanded)) => { + enclosing.push(Enclosing::FitsExpanded(fits_expanded)); + false + } + FormatElement::Tag(Tag::EndFitsExpanded) => { + enclosing.pop(); + // Fits expanded acts as a boundary + expands = false; + continue; + } FormatElement::StaticText { text } => text.contains('\n'), FormatElement::DynamicText { text, .. } => text.contains('\n'), FormatElement::SourceCodeSlice { @@ -280,14 +303,14 @@ impl Format> for &[FormatElement] { write!(f, [text("line_suffix_boundary")])?; } - FormatElement::BestFitting(best_fitting) => { + FormatElement::BestFitting { variants } => { write!(f, [text("best_fitting([")])?; f.write_elements([ FormatElement::Tag(StartIndent), FormatElement::Line(LineMode::Hard), ])?; - for variant in best_fitting.variants() { + for variant in variants { write!(f, [variant.deref(), hard_line_break()])?; } @@ -431,6 +454,29 @@ impl Format> for &[FormatElement] { } } + StartConditionalGroup(group) => { + write!( + f, + [ + text("conditional_group(condition:"), + space(), + group.condition(), + text(","), + space() + ] + )?; + + match group.mode() { + GroupMode::Flat => {} + GroupMode::Expand => { + write!(f, [text("expand: true,"), space()])?; + } + GroupMode::Propagated => { + write!(f, [text("expand: propagated,"), space()])?; + } + } + } + StartIndentIfGroupBreaks(id) => { write!( f, @@ -481,6 +527,28 @@ impl Format> for &[FormatElement] { write!(f, [text("fill(")])?; } + StartFitsExpanded(tag::FitsExpanded { + condition, + propagate_expand, + }) => { + write!(f, [text("fits_expanded(propagate_expand:"), space()])?; + + if propagate_expand.get() { + write!(f, [text("true")])?; + } else { + write!(f, [text("false")])?; + } + + write!(f, [text(","), space()])?; + + if let Some(condition) = condition { + write!( + f, + [text("condition:"), space(), condition, text(","), space()] + )?; + } + } + StartEntry => { // handled after the match for all start tags } @@ -493,8 +561,10 @@ impl Format> for &[FormatElement] { | EndAlign | EndIndent | EndGroup + | EndConditionalGroup | EndLineSuffix | EndDedent + | EndFitsExpanded | EndVerbatim => { write!(f, [ContentArrayEnd, text(")")])?; } @@ -648,6 +718,31 @@ impl FormatElements for [FormatElement] { } } +impl Format> for Condition { + fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { + match (self.mode, self.group_id) { + (PrintMode::Flat, None) => write!(f, [text("if_fits_on_line")]), + (PrintMode::Flat, Some(id)) => write!( + f, + [ + text("if_group_fits_on_line("), + dynamic_text(&std::format!("\"{id:?}\""), None), + text(")") + ] + ), + (PrintMode::Expanded, None) => write!(f, [text("if_breaks")]), + (PrintMode::Expanded, Some(id)) => write!( + f, + [ + text("if_group_breaks("), + dynamic_text(&std::format!("\"{id:?}\""), None), + text(")") + ] + ), + } + } +} + #[cfg(test)] mod tests { use crate::prelude::*; diff --git a/crates/ruff_formatter/src/format_element/tag.rs b/crates/ruff_formatter/src/format_element/tag.rs index 38ebbaf1c4..f586cc8b1c 100644 --- a/crates/ruff_formatter/src/format_element/tag.rs +++ b/crates/ruff_formatter/src/format_element/tag.rs @@ -1,8 +1,5 @@ use crate::format_element::PrintMode; use crate::{GroupId, TextSize}; -#[cfg(debug_assertions)] -use std::any::type_name; -use std::any::TypeId; use std::cell::Cell; use std::num::NonZeroU8; @@ -36,6 +33,16 @@ pub enum Tag { StartGroup(Group), EndGroup, + /// Creates a logical group similar to [`Tag::StartGroup`] but only if the condition is met. + /// This is an optimized representation for (assuming the content should only be grouped if another group fits): + /// + /// ```text + /// if_group_breaks(content, other_group_id), + /// if_group_fits_on_line(group(&content), other_group_id) + /// ``` + StartConditionalGroup(ConditionalGroup), + EndConditionalGroup, + /// Allows to specify content that gets printed depending on whatever the enclosing group /// is printed on a single line or multiple lines. See [crate::builders::if_group_breaks] for examples. StartConditionalContent(Condition), @@ -70,6 +77,9 @@ pub enum Tag { /// See [crate::builders::labelled] for documentation. StartLabelled(LabelId), EndLabelled, + + StartFitsExpanded(FitsExpanded), + EndFitsExpanded, } impl Tag { @@ -80,7 +90,8 @@ impl Tag { Tag::StartIndent | Tag::StartAlign(_) | Tag::StartDedent(_) - | Tag::StartGroup { .. } + | Tag::StartGroup(_) + | Tag::StartConditionalGroup(_) | Tag::StartConditionalContent(_) | Tag::StartIndentIfGroupBreaks(_) | Tag::StartFill @@ -88,6 +99,7 @@ impl Tag { | Tag::StartLineSuffix | Tag::StartVerbatim(_) | Tag::StartLabelled(_) + | Tag::StartFitsExpanded(_) ) } @@ -104,6 +116,7 @@ impl Tag { StartAlign(_) | EndAlign => TagKind::Align, StartDedent(_) | EndDedent => TagKind::Dedent, StartGroup(_) | EndGroup => TagKind::Group, + StartConditionalGroup(_) | EndConditionalGroup => TagKind::ConditionalGroup, StartConditionalContent(_) | EndConditionalContent => TagKind::ConditionalContent, StartIndentIfGroupBreaks(_) | EndIndentIfGroupBreaks => TagKind::IndentIfGroupBreaks, StartFill | EndFill => TagKind::Fill, @@ -111,6 +124,7 @@ impl Tag { StartLineSuffix | EndLineSuffix => TagKind::LineSuffix, StartVerbatim(_) | EndVerbatim => TagKind::Verbatim, StartLabelled(_) | EndLabelled => TagKind::Labelled, + StartFitsExpanded { .. } | EndFitsExpanded => TagKind::FitsExpanded, } } } @@ -125,6 +139,7 @@ pub enum TagKind { Align, Dedent, Group, + ConditionalGroup, ConditionalContent, IndentIfGroupBreaks, Fill, @@ -132,6 +147,7 @@ pub enum TagKind { LineSuffix, Verbatim, Labelled, + FitsExpanded, } #[derive(Debug, Copy, Default, Clone, Eq, PartialEq)] @@ -153,6 +169,27 @@ impl GroupMode { } } +#[derive(Debug, Clone, Eq, PartialEq, Default)] +pub struct FitsExpanded { + pub(crate) condition: Option, + pub(crate) propagate_expand: Cell, +} + +impl FitsExpanded { + pub fn new() -> Self { + Self::default() + } + + pub fn with_condition(mut self, condition: Option) -> Self { + self.condition = condition; + self + } + + pub fn propagate_expand(&self) { + self.propagate_expand.set(true) + } +} + #[derive(Debug, Clone, Eq, PartialEq, Default)] pub struct Group { id: Option, @@ -192,6 +229,33 @@ impl Group { } } +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct ConditionalGroup { + mode: Cell, + condition: Condition, +} + +impl ConditionalGroup { + pub fn new(condition: Condition) -> Self { + Self { + mode: Cell::new(GroupMode::Flat), + condition, + } + } + + pub fn condition(&self) -> Condition { + self.condition + } + + pub fn propagate_expand(&self) { + self.mode.set(GroupMode::Propagated) + } + + pub fn mode(&self) -> GroupMode { + self.mode.get() + } +} + #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum DedentMode { /// Reduces the indent by a level (if the current indent is > 0) @@ -201,10 +265,10 @@ pub enum DedentMode { Root, } -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Copy, Clone, Eq, PartialEq)] pub struct Condition { - /// - Flat -> Omitted if the enclosing group is a multiline group, printed for groups fitting on a single line - /// - Multiline -> Omitted if the enclosing group fits on a single line, printed if the group breaks over multiple lines. + /// - `Flat` -> Omitted if the enclosing group is a multiline group, printed for groups fitting on a single line + /// - `Expanded` -> Omitted if the enclosing group fits on a single line, printed if the group breaks over multiple lines. pub(crate) mode: PrintMode, /// The id of the group for which it should check if it breaks or not. The group must appear in the document @@ -213,21 +277,45 @@ pub struct Condition { } impl Condition { - pub fn new(mode: PrintMode) -> Self { + pub(crate) fn new(mode: PrintMode) -> Self { Self { mode, group_id: None, } } + pub fn if_fits_on_line() -> Self { + Self { + mode: PrintMode::Flat, + group_id: None, + } + } + + pub fn if_group_fits_on_line(group_id: GroupId) -> Self { + Self { + mode: PrintMode::Flat, + group_id: Some(group_id), + } + } + + pub fn if_breaks() -> Self { + Self { + mode: PrintMode::Expanded, + group_id: None, + } + } + + pub fn if_group_breaks(group_id: GroupId) -> Self { + Self { + mode: PrintMode::Expanded, + group_id: Some(group_id), + } + } + pub fn with_group_id(mut self, id: Option) -> Self { self.group_id = id; self } - - pub fn mode(&self) -> PrintMode { - self.mode - } } #[derive(Clone, Debug, Eq, PartialEq)] @@ -239,37 +327,48 @@ impl Align { } } -#[derive(Eq, PartialEq, Copy, Clone)] +#[derive(Debug, Eq, Copy, Clone)] pub struct LabelId { - id: TypeId, + value: u64, #[cfg(debug_assertions)] - label: &'static str, + name: &'static str, } -#[cfg(debug_assertions)] -impl std::fmt::Debug for LabelId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(self.label) - } -} +impl PartialEq for LabelId { + fn eq(&self, other: &Self) -> bool { + let is_equal = self.value == other.value; -#[cfg(not(debug_assertions))] -impl std::fmt::Debug for LabelId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - std::write!(f, "#{:?}", self.id) + #[cfg(debug_assertions)] + { + if is_equal { + assert_eq!(self.name, other.name, "Two `LabelId`s with different names have the same `value`. Are you mixing labels of two different `LabelDefinition` or are the values returned by the `LabelDefinition` not unique?"); + } + } + + is_equal } } impl LabelId { - pub fn of() -> Self { + pub fn of(label: T) -> Self { Self { - id: TypeId::of::(), + value: label.value(), #[cfg(debug_assertions)] - label: type_name::(), + name: label.name(), } } } +/// Defines the valid labels of a language. You want to have at most one implementation per formatter +/// project. +pub trait LabelDefinition { + /// Returns the `u64` uniquely identifying this specific label. + fn value(&self) -> u64; + + /// Returns the name of the label that is shown in debug builds. + fn name(&self) -> &'static str; +} + #[derive(Clone, Copy, Eq, PartialEq, Debug)] pub enum VerbatimKind { Bogus, diff --git a/crates/ruff_formatter/src/format_extensions.rs b/crates/ruff_formatter/src/format_extensions.rs index 82b99fda93..cde30df58a 100644 --- a/crates/ruff_formatter/src/format_extensions.rs +++ b/crates/ruff_formatter/src/format_extensions.rs @@ -42,7 +42,7 @@ pub trait MemoizeFormat { /// # fn main() -> FormatResult<()> { /// let normal = MyFormat::new(); /// - /// // Calls `format` for everytime the object gets formatted + /// // Calls `format` every time the object gets formatted /// assert_eq!( /// "Formatted 1 times. Formatted 2 times.", /// format!(SimpleFormatContext::default(), [normal, space(), normal])?.print()?.as_code() @@ -57,7 +57,6 @@ pub trait MemoizeFormat { /// # Ok(()) /// # } /// ``` - /// fn memoized(self) -> Memoized where Self: Sized + Format, @@ -142,7 +141,6 @@ where /// assert_eq!("Counter:\n\tCount: 0\nCount: 0\n", formatted.print()?.as_code()); /// # Ok(()) /// # } - /// /// ``` pub fn inspect(&mut self, f: &mut Formatter) -> FormatResult<&[FormatElement]> { let result = self diff --git a/crates/ruff_formatter/src/lib.rs b/crates/ruff_formatter/src/lib.rs index e1f9e4f21e..5c99ebba87 100644 --- a/crates/ruff_formatter/src/lib.rs +++ b/crates/ruff_formatter/src/lib.rs @@ -48,7 +48,7 @@ pub use buffer::{ Buffer, BufferExtensions, BufferSnapshot, Inspect, PreambleBuffer, RemoveSoftLinesBuffer, VecBuffer, }; -pub use builders::FormatBestFitting; +pub use builders::BestFitting; pub use source_code::{SourceCode, SourceCodeSlice}; pub use crate::diagnostics::{ActualStart, FormatError, InvalidDocumentError, PrintError}; @@ -59,10 +59,8 @@ use std::num::ParseIntError; use std::str::FromStr; #[derive(Debug, Eq, PartialEq, Clone, Copy, Hash)] -#[cfg_attr( - feature = "serde", - derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema) -)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[derive(Default)] pub enum IndentStyle { /// Tab @@ -112,10 +110,8 @@ impl std::fmt::Display for IndentStyle { /// /// The allowed range of values is 1..=320 #[derive(Clone, Copy, Debug, Eq, PartialEq)] -#[cfg_attr( - feature = "serde", - derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema) -)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct LineWidth(u16); impl LineWidth { @@ -278,10 +274,8 @@ impl FormatOptions for SimpleFormatOptions { /// Lightweight sourcemap marker between source and output tokens #[derive(Debug, Copy, Clone, Eq, PartialEq)] -#[cfg_attr( - feature = "serde", - derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema) -)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct SourceMarker { /// Position of the marker in the original source pub source: TextSize, @@ -340,10 +334,8 @@ where pub type PrintResult = Result; #[derive(Debug, Clone, Eq, PartialEq)] -#[cfg_attr( - feature = "serde", - derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema) -)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct Printed { code: String, range: Option, @@ -721,7 +713,6 @@ where /// # Ok(()) /// # } /// ``` -/// #[inline(always)] pub fn write( output: &mut dyn Buffer, diff --git a/crates/ruff_formatter/src/macros.rs b/crates/ruff_formatter/src/macros.rs index fb6c66e6fa..4f5bcdf050 100644 --- a/crates/ruff_formatter/src/macros.rs +++ b/crates/ruff_formatter/src/macros.rs @@ -320,17 +320,17 @@ macro_rules! format { /// the content up to the first non-soft line break without exceeding the configured print width. /// This definition differs from groups as that non-soft line breaks make group expand. /// -/// [crate::FormatBestFitting] acts as a "break" boundary, meaning that it is considered to fit +/// [crate::BestFitting] acts as a "break" boundary, meaning that it is considered to fit /// /// /// [`Flat`]: crate::format_element::PrintMode::Flat /// [`Expanded`]: crate::format_element::PrintMode::Expanded -/// [`MostExpanded`]: crate::format_element::BestFitting::most_expanded +/// [`MostExpanded`]: crate::format_element::BestFittingVariants::most_expanded #[macro_export] macro_rules! best_fitting { ($least_expanded:expr, $($tail:expr),+ $(,)?) => {{ unsafe { - $crate::FormatBestFitting::from_arguments_unchecked($crate::format_args!($least_expanded, $($tail),+)) + $crate::BestFitting::from_arguments_unchecked($crate::format_args!($least_expanded, $($tail),+)) } }} } diff --git a/crates/ruff_formatter/src/printer/call_stack.rs b/crates/ruff_formatter/src/printer/call_stack.rs index 8bedc9f783..a262f210a7 100644 --- a/crates/ruff_formatter/src/printer/call_stack.rs +++ b/crates/ruff_formatter/src/printer/call_stack.rs @@ -1,7 +1,7 @@ use crate::format_element::tag::TagKind; use crate::format_element::PrintMode; use crate::printer::stack::{Stack, StackedStack}; -use crate::printer::Indention; +use crate::printer::{Indention, MeasureMode}; use crate::{IndentStyle, InvalidDocumentError, PrintError, PrintResult}; use std::fmt::Debug; use std::num::NonZeroU8; @@ -28,6 +28,7 @@ pub(super) struct StackFrame { pub(super) struct PrintElementArgs { indent: Indention, mode: PrintMode, + measure_mode: MeasureMode, } impl PrintElementArgs { @@ -42,6 +43,10 @@ impl PrintElementArgs { self.mode } + pub(super) fn measure_mode(&self) -> MeasureMode { + self.measure_mode + } + pub(super) fn indention(&self) -> Indention { self.indent } @@ -70,6 +75,11 @@ impl PrintElementArgs { self.mode = mode; self } + + pub(crate) fn with_measure_mode(mut self, mode: MeasureMode) -> Self { + self.measure_mode = mode; + self + } } impl Default for PrintElementArgs { @@ -77,6 +87,7 @@ impl Default for PrintElementArgs { Self { indent: Indention::Level(0), mode: PrintMode::Expanded, + measure_mode: MeasureMode::FirstLine, } } } diff --git a/crates/ruff_formatter/src/printer/mod.rs b/crates/ruff_formatter/src/printer/mod.rs index 223a42005d..2bcb7fe582 100644 --- a/crates/ruff_formatter/src/printer/mod.rs +++ b/crates/ruff_formatter/src/printer/mod.rs @@ -4,18 +4,11 @@ mod printer_options; mod queue; mod stack; -pub use printer_options::*; - -use crate::format_element::{BestFitting, LineMode, PrintMode}; -use crate::{ - ActualStart, FormatElement, GroupId, IndentStyle, InvalidDocumentError, PrintError, - PrintResult, Printed, SourceMarker, TextRange, -}; - use crate::format_element::document::Document; -use crate::format_element::tag::Condition; +use crate::format_element::tag::{Condition, GroupMode}; +use crate::format_element::{BestFittingVariants, LineMode, PrintMode}; +use crate::prelude::tag; use crate::prelude::tag::{DedentMode, Tag, TagKind, VerbatimKind}; -use crate::prelude::Tag::EndFill; use crate::printer::call_stack::{ CallStack, FitsCallStack, PrintCallStack, PrintElementArgs, StackFrame, }; @@ -24,7 +17,12 @@ use crate::printer::queue::{ AllPredicate, FitsEndPredicate, FitsQueue, PrintQueue, Queue, SingleEntryPredicate, }; use crate::source_code::SourceCode; +use crate::{ + ActualStart, FormatElement, GroupId, IndentStyle, InvalidDocumentError, PrintError, + PrintResult, Printed, SourceMarker, TextRange, +}; use drop_bomb::DebugDropBomb; +pub use printer_options::*; use ruff_text_size::{TextLen, TextSize}; use std::num::NonZeroU8; use unicode_width::UnicodeWidthChar; @@ -137,8 +135,8 @@ impl<'a> Printer<'a> { self.flush_line_suffixes(queue, stack, Some(HARD_BREAK)); } - FormatElement::BestFitting(best_fitting) => { - self.print_best_fitting(best_fitting, queue, stack)?; + FormatElement::BestFitting { variants } => { + self.print_best_fitting(variants, queue, stack)?; } FormatElement::Interned(content) => { @@ -146,39 +144,26 @@ impl<'a> Printer<'a> { } FormatElement::Tag(StartGroup(group)) => { - let group_mode = if !group.mode().is_flat() { - PrintMode::Expanded - } else { - match args.mode() { - PrintMode::Flat if self.state.measured_group_fits => { - // A parent group has already verified that this group fits on a single line - // Thus, just continue in flat mode - PrintMode::Flat - } - // The printer is either in expanded mode or it's necessary to re-measure if the group fits - // because the printer printed a line break - _ => { - self.state.measured_group_fits = true; - - // Measure to see if the group fits up on a single line. If that's the case, - // print the group in "flat" mode, otherwise continue in expanded mode - stack.push(TagKind::Group, args.with_print_mode(PrintMode::Flat)); - let fits = self.fits(queue, stack)?; - stack.pop(TagKind::Group)?; - - if fits { - PrintMode::Flat - } else { - PrintMode::Expanded - } - } - } - }; - - stack.push(TagKind::Group, args.with_print_mode(group_mode)); + let print_mode = + self.print_group(TagKind::Group, group.mode(), args, queue, stack)?; if let Some(id) = group.id() { - self.state.group_modes.insert_print_mode(id, group_mode); + self.state.group_modes.insert_print_mode(id, print_mode); + } + } + + FormatElement::Tag(StartConditionalGroup(group)) => { + let condition = group.condition(); + let expected_mode = match condition.group_id { + None => args.mode(), + Some(id) => self.state.group_modes.unwrap_print_mode(id, element), + }; + + if expected_mode == condition.mode { + self.print_group(TagKind::ConditionalGroup, group.mode(), args, queue, stack)?; + } else { + // Condition isn't met, render as normal content + stack.push(TagKind::ConditionalGroup, args); } } @@ -211,10 +196,10 @@ impl<'a> Printer<'a> { Some(id) => self.state.group_modes.unwrap_print_mode(*id, element), }; - if group_mode != *mode { - queue.skip_content(TagKind::ConditionalContent); - } else { + if *mode == group_mode { stack.push(TagKind::ConditionalContent, args); + } else { + queue.skip_content(TagKind::ConditionalContent); } } @@ -246,18 +231,44 @@ impl<'a> Printer<'a> { stack.push(TagKind::Verbatim, args); } + FormatElement::Tag(StartFitsExpanded(tag::FitsExpanded { condition, .. })) => { + let condition_met = match condition { + Some(condition) => { + let group_mode = match condition.group_id { + Some(group_id) => { + self.state.group_modes.unwrap_print_mode(group_id, element) + } + None => args.mode(), + }; + + condition.mode == group_mode + } + None => true, + }; + + if condition_met { + // We measured the inner groups all in expanded. It now is necessary to measure if the inner groups fit as well. + self.state.measured_group_fits = false; + } + + stack.push(TagKind::FitsExpanded, args); + } + FormatElement::Tag(tag @ (StartLabelled(_) | StartEntry)) => { stack.push(tag.kind(), args); } + FormatElement::Tag( tag @ (EndLabelled | EndEntry | EndGroup + | EndConditionalGroup | EndIndent | EndDedent | EndAlign | EndConditionalContent | EndIndentIfGroupBreaks + | EndFitsExpanded | EndVerbatim | EndLineSuffix | EndFill), @@ -276,6 +287,49 @@ impl<'a> Printer<'a> { result } + fn print_group( + &mut self, + kind: TagKind, + mode: GroupMode, + args: PrintElementArgs, + queue: &mut PrintQueue<'a>, + stack: &mut PrintCallStack, + ) -> PrintResult { + let group_mode = match mode { + GroupMode::Expand | GroupMode::Propagated => PrintMode::Expanded, + GroupMode::Flat => { + match args.mode() { + PrintMode::Flat if self.state.measured_group_fits => { + // A parent group has already verified that this group fits on a single line + // Thus, just continue in flat mode + PrintMode::Flat + } + // The printer is either in expanded mode or it's necessary to re-measure if the group fits + // because the printer printed a line break + _ => { + self.state.measured_group_fits = true; + + // Measure to see if the group fits up on a single line. If that's the case, + // print the group in "flat" mode, otherwise continue in expanded mode + stack.push(kind, args.with_print_mode(PrintMode::Flat)); + let fits = self.fits(queue, stack)?; + stack.pop(kind)?; + + if fits { + PrintMode::Flat + } else { + PrintMode::Expanded + } + } + } + } + }; + + stack.push(kind, args.with_print_mode(group_mode)); + + Ok(group_mode) + } + fn print_text(&mut self, text: &str, source_range: Option) { if !self.state.pending_indent.is_empty() { let (indent_char, repeat_count) = match self.options.indent_style() { @@ -371,19 +425,18 @@ impl<'a> Printer<'a> { fn print_best_fitting( &mut self, - best_fitting: &'a BestFitting, + variants: &'a BestFittingVariants, queue: &mut PrintQueue<'a>, stack: &mut PrintCallStack, ) -> PrintResult<()> { let args = stack.top(); if args.mode().is_flat() && self.state.measured_group_fits { - queue.extend_back(best_fitting.most_flat()); + queue.extend_back(variants.most_flat()); self.print_entry(queue, stack, args) } else { self.state.measured_group_fits = true; - - let normal_variants = &best_fitting.variants()[..best_fitting.variants().len() - 1]; + let normal_variants = &variants[..variants.len() - 1]; for variant in normal_variants.iter() { // Test if this variant fits and if so, use it. Otherwise try the next @@ -394,12 +447,12 @@ impl<'a> Printer<'a> { return invalid_start_tag(TagKind::Entry, variant.first()); } - let entry_args = args.with_print_mode(PrintMode::Flat); - // Skip the first element because we want to override the args for the entry and the // args must be popped from the stack as soon as it sees the matching end entry. let content = &variant[1..]; + let entry_args = args.with_print_mode(PrintMode::Flat); + queue.extend_back(content); stack.push(TagKind::Entry, entry_args); let variant_fits = self.fits(queue, stack)?; @@ -411,12 +464,12 @@ impl<'a> Printer<'a> { if variant_fits { queue.extend_back(variant); - return self.print_entry(queue, stack, entry_args); + return self.print_entry(queue, stack, args.with_print_mode(PrintMode::Flat)); } } // No variant fits, take the last (most expanded) as fallback - let most_expanded = best_fitting.most_expanded(); + let most_expanded = variants.most_expanded(); queue.extend_back(most_expanded); self.print_entry(queue, stack, args.with_print_mode(PrintMode::Expanded)) } @@ -555,7 +608,7 @@ impl<'a> Printer<'a> { } } - if queue.top() == Some(&FormatElement::Tag(EndFill)) { + if queue.top() == Some(&FormatElement::Tag(Tag::EndFill)) { Ok(()) } else { invalid_end_tag(TagKind::Fill, stack.top_kind()) @@ -700,7 +753,7 @@ struct PrinterState<'a> { verbatim_markers: Vec, group_modes: GroupModes, // Re-used queue to measure if a group fits. Optimisation to avoid re-allocating a new - // vec everytime a group gets measured + // vec every time a group gets measured fits_stack: Vec, fits_queue: Vec<&'a [FormatElement]>, } @@ -959,8 +1012,8 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> { FormatElement::Space => return Ok(self.fits_text(" ")), FormatElement::Line(line_mode) => { - if args.mode().is_flat() { - match line_mode { + match args.mode() { + PrintMode::Flat => match line_mode { LineMode::SoftOrSpace => return Ok(self.fits_text(" ")), LineMode::Soft => {} LineMode::Hard | LineMode::Empty => { @@ -970,13 +1023,22 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> { Fits::Yes }); } + }, + PrintMode::Expanded => { + match args.measure_mode() { + MeasureMode::FirstLine => { + // Reachable if the restQueue contains an element with mode expanded because Expanded + // is what the mode's initialized to by default + // This means, the printer is outside of the current element at this point and any + // line break should be printed as regular line break + return Ok(Fits::Yes); + } + MeasureMode::AllLines => { + // Continue measuring on the next line + self.state.line_width = 0; + } + } } - } else { - // Reachable if the restQueue contains an element with mode expanded because Expanded - // is what the mode's initialized to by default - // This means, the printer is outside of the current element at this point and any - // line break should be printed as regular line break -> Fits - return Ok(Fits::Yes); } } @@ -1000,17 +1062,18 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> { FormatElement::SourcePosition(_) => {} - FormatElement::BestFitting(best_fitting) => { + FormatElement::BestFitting { variants } => { let slice = match args.mode() { - PrintMode::Flat => best_fitting.most_flat(), - PrintMode::Expanded => best_fitting.most_expanded(), + PrintMode::Flat => variants.most_flat(), + PrintMode::Expanded => variants.most_expanded(), }; if !matches!(slice.first(), Some(FormatElement::Tag(Tag::StartEntry))) { return invalid_start_tag(TagKind::Entry, slice.first()); } - self.queue.extend_back(slice); + self.stack.push(TagKind::Entry, args); + self.queue.extend_back(&slice[1..]); } FormatElement::Interned(content) => self.queue.extend_back(content), @@ -1036,26 +1099,13 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> { } FormatElement::Tag(StartGroup(group)) => { - if self.must_be_flat && !group.mode().is_flat() { - return Ok(Fits::No); - } - - let group_mode = if !group.mode().is_flat() { - PrintMode::Expanded - } else { - args.mode() - }; - - self.stack - .push(TagKind::Group, args.with_print_mode(group_mode)); - - if let Some(id) = group.id() { - self.group_modes_mut().insert_print_mode(id, group_mode); - } + return self.fits_group(TagKind::Group, group.mode(), group.id(), args); } - FormatElement::Tag(StartConditionalContent(condition)) => { - let group_mode = match condition.group_id { + FormatElement::Tag(StartConditionalGroup(group)) => { + let condition = group.condition(); + + let print_mode = match condition.group_id { None => args.mode(), Some(group_id) => self .group_modes() @@ -1063,20 +1113,36 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> { .unwrap_or_else(|| args.mode()), }; - if group_mode != condition.mode { - self.queue.skip_content(TagKind::ConditionalContent); + if condition.mode == print_mode { + return self.fits_group(TagKind::ConditionalGroup, group.mode(), None, args); } else { + self.stack.push(TagKind::ConditionalGroup, args); + } + } + + FormatElement::Tag(StartConditionalContent(condition)) => { + let print_mode = match condition.group_id { + None => args.mode(), + Some(group_id) => self + .group_modes() + .get_print_mode(group_id) + .unwrap_or_else(|| args.mode()), + }; + + if condition.mode == print_mode { self.stack.push(TagKind::ConditionalContent, args); + } else { + self.queue.skip_content(TagKind::ConditionalContent); } } FormatElement::Tag(StartIndentIfGroupBreaks(id)) => { - let group_mode = self + let print_mode = self .group_modes() .get_print_mode(*id) .unwrap_or_else(|| args.mode()); - match group_mode { + match print_mode { PrintMode::Flat => { self.stack.push(TagKind::IndentIfGroupBreaks, args); } @@ -1098,22 +1164,61 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> { return invalid_end_tag(TagKind::LineSuffix, self.stack.top_kind()); } + FormatElement::Tag(StartFitsExpanded(tag::FitsExpanded { + condition, + propagate_expand, + })) => { + let condition_met = match condition { + Some(condition) => { + let group_mode = match condition.group_id { + Some(group_id) => self + .group_modes() + .get_print_mode(group_id) + .unwrap_or_else(|| args.mode()), + None => args.mode(), + }; + + condition.mode == group_mode + } + None => true, + }; + + if condition_met { + // Measure in fully expanded mode. + self.stack.push( + TagKind::FitsExpanded, + args.with_print_mode(PrintMode::Expanded) + .with_measure_mode(MeasureMode::AllLines), + ) + } else { + if propagate_expand.get() && args.mode().is_flat() { + return Ok(Fits::No); + } + + // As usual + self.stack.push(TagKind::FitsExpanded, args) + } + } + FormatElement::Tag( tag @ (StartFill | StartVerbatim(_) | StartLabelled(_) | StartEntry), ) => { self.stack.push(tag.kind(), args); } + FormatElement::Tag( tag @ (EndFill | EndVerbatim | EndLabelled | EndEntry | EndGroup + | EndConditionalGroup | EndIndentIfGroupBreaks | EndConditionalContent | EndAlign | EndDedent - | EndIndent), + | EndIndent + | EndFitsExpanded), ) => { self.stack.pop(tag.kind())?; } @@ -1122,6 +1227,34 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> { Ok(Fits::Maybe) } + fn fits_group( + &mut self, + kind: TagKind, + mode: GroupMode, + id: Option, + args: PrintElementArgs, + ) -> PrintResult { + if self.must_be_flat && !mode.is_flat() { + return Ok(Fits::No); + } + + // Continue printing groups in expanded mode if measuring a `best_fitting` element where + // a group expands. + let print_mode = if !mode.is_flat() { + PrintMode::Expanded + } else { + args.mode() + }; + + self.stack.push(kind, args.with_print_mode(print_mode)); + + if let Some(id) = id { + self.group_modes_mut().insert_print_mode(id, print_mode); + } + + Ok(Fits::Maybe) + } + fn fits_text(&mut self, text: &str) -> Fits { let indent = std::mem::take(&mut self.state.pending_indent); self.state.line_width += indent.level() as usize * self.options().indent_width() as usize @@ -1234,6 +1367,18 @@ struct FitsState { line_width: usize, } +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +enum MeasureMode { + /// The content fits if a hard line break or soft line break in [`PrintMode::Expanded`] is seen + /// before exceeding the configured print width. + /// Returns + FirstLine, + + /// The content only fits if non of the lines exceed the print width. Lines are terminated by either + /// a hard line break or a soft line break in [`PrintMode::Expanded`]. + AllLines, +} + #[cfg(test)] mod tests { use crate::prelude::*; diff --git a/crates/ruff_index/Cargo.toml b/crates/ruff_index/Cargo.toml index 77e1c3fb56..29e55d60b4 100644 --- a/crates/ruff_index/Cargo.toml +++ b/crates/ruff_index/Cargo.toml @@ -2,8 +2,13 @@ name = "ruff_index" version = "0.0.0" publish = false +authors = { workspace = true } edition = { workspace = true } rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } [lib] diff --git a/crates/ruff_index/src/slice.rs b/crates/ruff_index/src/slice.rs index ddb534ea82..a6d3b033df 100644 --- a/crates/ruff_index/src/slice.rs +++ b/crates/ruff_index/src/slice.rs @@ -40,6 +40,11 @@ impl IndexSlice { } } + #[inline] + pub const fn first(&self) -> Option<&T> { + self.raw.first() + } + #[inline] pub const fn len(&self) -> usize { self.raw.len() @@ -63,6 +68,13 @@ impl IndexSlice { (0..self.len()).map(|n| I::new(n)) } + #[inline] + pub fn iter_enumerated( + &self, + ) -> impl DoubleEndedIterator + ExactSizeIterator + '_ { + self.raw.iter().enumerate().map(|(n, t)| (I::new(n), t)) + } + #[inline] pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, T> { self.raw.iter_mut() @@ -127,8 +139,8 @@ impl IndexMut for IndexSlice { } impl<'a, I: Idx, T> IntoIterator for &'a IndexSlice { - type Item = &'a T; type IntoIter = std::slice::Iter<'a, T>; + type Item = &'a T; #[inline] fn into_iter(self) -> std::slice::Iter<'a, T> { @@ -137,8 +149,8 @@ impl<'a, I: Idx, T> IntoIterator for &'a IndexSlice { } impl<'a, I: Idx, T> IntoIterator for &'a mut IndexSlice { - type Item = &'a mut T; type IntoIter = std::slice::IterMut<'a, T>; + type Item = &'a mut T; #[inline] fn into_iter(self) -> std::slice::IterMut<'a, T> { diff --git a/crates/ruff_index/src/vec.rs b/crates/ruff_index/src/vec.rs index 36fa6388ac..516e53487f 100644 --- a/crates/ruff_index/src/vec.rs +++ b/crates/ruff_index/src/vec.rs @@ -121,8 +121,8 @@ impl FromIterator for IndexVec { } impl IntoIterator for IndexVec { - type Item = T; type IntoIter = std::vec::IntoIter; + type Item = T; #[inline] fn into_iter(self) -> std::vec::IntoIter { @@ -131,8 +131,8 @@ impl IntoIterator for IndexVec { } impl<'a, I: Idx, T> IntoIterator for &'a IndexVec { - type Item = &'a T; type IntoIter = std::slice::Iter<'a, T>; + type Item = &'a T; #[inline] fn into_iter(self) -> std::slice::Iter<'a, T> { @@ -141,8 +141,8 @@ impl<'a, I: Idx, T> IntoIterator for &'a IndexVec { } impl<'a, I: Idx, T> IntoIterator for &'a mut IndexVec { - type Item = &'a mut T; type IntoIter = std::slice::IterMut<'a, T>; + type Item = &'a mut T; #[inline] fn into_iter(self) -> std::slice::IterMut<'a, T> { diff --git a/crates/ruff_macros/Cargo.toml b/crates/ruff_macros/Cargo.toml index f774aee299..6b48c144cd 100644 --- a/crates/ruff_macros/Cargo.toml +++ b/crates/ruff_macros/Cargo.toml @@ -2,8 +2,13 @@ name = "ruff_macros" version = "0.0.0" publish = false +authors = { workspace = true } edition = { workspace = true } rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } [lib] proc-macro = true diff --git a/crates/ruff_macros/src/config.rs b/crates/ruff_macros/src/config.rs index 38dcaef14b..bfbfc8227e 100644 --- a/crates/ruff_macros/src/config.rs +++ b/crates/ruff_macros/src/config.rs @@ -19,7 +19,7 @@ pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result { let mut output = vec![]; - for field in fields.named.iter() { + for field in &fields.named { let docs: Vec<&Attribute> = field .attrs .iter() diff --git a/crates/ruff_macros/src/derive_message_formats.rs b/crates/ruff_macros/src/derive_message_formats.rs index f72113c223..c155ffb00b 100644 --- a/crates/ruff_macros/src/derive_message_formats.rs +++ b/crates/ruff_macros/src/derive_message_formats.rs @@ -19,7 +19,9 @@ pub(crate) fn derive_message_formats(func: &ItemFn) -> TokenStream { } fn parse_block(block: &Block, strings: &mut TokenStream) -> Result<(), TokenStream> { - let Some(Stmt::Expr(last, _)) = block.stmts.last() else {panic!("expected last statement in block to be an expression")}; + let Some(Stmt::Expr(last, _)) = block.stmts.last() else { + panic!("expected last statement in block to be an expression") + }; parse_expr(last, strings)?; Ok(()) } @@ -28,7 +30,9 @@ fn parse_expr(expr: &Expr, strings: &mut TokenStream) -> Result<(), TokenStream> match expr { Expr::Macro(mac) if mac.mac.path.is_ident("format") => { let Some(first_token) = mac.mac.tokens.to_token_stream().into_iter().next() else { - return Err(quote_spanned!(expr.span() => compile_error!("expected format! to have an argument"))) + return Err( + quote_spanned!(expr.span() => compile_error!("expected format! to have an argument")), + ); }; strings.extend(quote! {#first_token,}); Ok(()) diff --git a/crates/ruff_macros/src/map_codes.rs b/crates/ruff_macros/src/map_codes.rs index 327b9a9d3d..eeee0d7ac9 100644 --- a/crates/ruff_macros/src/map_codes.rs +++ b/crates/ruff_macros/src/map_codes.rs @@ -31,14 +31,30 @@ struct Rule { pub(crate) fn map_codes(func: &ItemFn) -> syn::Result { let Some(last_stmt) = func.block.stmts.last() else { - return Err(Error::new(func.block.span(), "expected body to end in an expression")); + return Err(Error::new( + func.block.span(), + "expected body to end in an expression", + )); }; - let Stmt::Expr(Expr::Call(ExprCall { args: some_args, .. }), _) = last_stmt else { - return Err(Error::new(last_stmt.span(), "expected last expression to be `Some(match (..) { .. })`")); + let Stmt::Expr( + Expr::Call(ExprCall { + args: some_args, .. + }), + _, + ) = last_stmt + else { + return Err(Error::new( + last_stmt.span(), + "expected last expression to be `Some(match (..) { .. })`", + )); }; let mut some_args = some_args.into_iter(); - let (Some(Expr::Match(ExprMatch { arms, .. })), None) = (some_args.next(), some_args.next()) else { - return Err(Error::new(last_stmt.span(), "expected last expression to be `Some(match (..) { .. })`")); + let (Some(Expr::Match(ExprMatch { arms, .. })), None) = (some_args.next(), some_args.next()) + else { + return Err(Error::new( + last_stmt.span(), + "expected last expression to be `Some(match (..) { .. })`", + )); }; // Map from: linter (e.g., `Flake8Bugbear`) to rule code (e.g.,`"002"`) to rule data (e.g., @@ -139,30 +155,13 @@ pub(crate) fn map_codes(func: &ItemFn) -> syn::Result { } output.extend(quote! { - impl IntoIterator for &#linter { - type Item = Rule; - type IntoIter = ::std::vec::IntoIter; - - fn into_iter(self) -> Self::IntoIter { + impl #linter { + pub fn rules(self) -> ::std::vec::IntoIter { match self { #prefix_into_iter_match_arms } } } }); } - - output.extend(quote! { - impl IntoIterator for &RuleCodePrefix { - type Item = Rule; - type IntoIter = ::std::vec::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - match self { - #(RuleCodePrefix::#linter_idents(prefix) => prefix.into_iter(),)* - } - } - } - }); - output.extend(quote! { impl RuleCodePrefix { pub fn parse(linter: &Linter, code: &str) -> Result { @@ -172,14 +171,19 @@ pub(crate) fn map_codes(func: &ItemFn) -> syn::Result { #(Linter::#linter_idents => RuleCodePrefix::#linter_idents(#linter_idents::from_str(code).map_err(|_| crate::registry::FromCodeError::Unknown)?),)* }) } + + pub fn rules(self) -> ::std::vec::IntoIter { + match self { + #(RuleCodePrefix::#linter_idents(prefix) => prefix.clone().rules(),)* + } + } } }); let rule_to_code = generate_rule_to_code(&linter_to_rules); output.extend(rule_to_code); - let iter = generate_iter_impl(&linter_to_rules, &all_codes); - output.extend(iter); + output.extend(generate_iter_impl(&linter_to_rules, &linter_idents)); Ok(output) } @@ -193,7 +197,8 @@ fn rules_by_prefix( for (code, rule) in rules { // Nursery rules have to be explicitly selected, so we ignore them when looking at - // prefixes. + // prefix-level selectors (e.g., `--select SIM10`), but add the rule itself under + // its fully-qualified code (e.g., `--select SIM101`). if is_nursery(&rule.group) { rules_by_prefix.insert(code.clone(), vec![(rule.path.clone(), rule.attrs.clone())]); continue; @@ -326,34 +331,51 @@ See also https://github.com/astral-sh/ruff/issues/2186. /// Implement `impl IntoIterator for &Linter` and `RuleCodePrefix::iter()` fn generate_iter_impl( linter_to_rules: &BTreeMap>, - all_codes: &[TokenStream], + linter_idents: &[&Ident], ) -> TokenStream { - let mut linter_into_iter_match_arms = quote!(); + let mut linter_rules_match_arms = quote!(); + let mut linter_all_rules_match_arms = quote!(); for (linter, map) in linter_to_rules { + let rule_paths = map.values().filter(|rule| !is_nursery(&rule.group)).map( + |Rule { attrs, path, .. }| { + let rule_name = path.segments.last().unwrap(); + quote!(#(#attrs)* Rule::#rule_name) + }, + ); + linter_rules_match_arms.extend(quote! { + Linter::#linter => vec![#(#rule_paths,)*].into_iter(), + }); let rule_paths = map.values().map(|Rule { attrs, path, .. }| { let rule_name = path.segments.last().unwrap(); quote!(#(#attrs)* Rule::#rule_name) }); - linter_into_iter_match_arms.extend(quote! { + linter_all_rules_match_arms.extend(quote! { Linter::#linter => vec![#(#rule_paths,)*].into_iter(), }); } quote! { - impl IntoIterator for &Linter { - type Item = Rule; - type IntoIter = ::std::vec::IntoIter; - - fn into_iter(self) -> Self::IntoIter { + impl Linter { + /// Rules not in the nursery. + pub fn rules(self: &Linter) -> ::std::vec::IntoIter { match self { - #linter_into_iter_match_arms + #linter_rules_match_arms + } + } + /// All rules, including those in the nursery. + pub fn all_rules(self: &Linter) -> ::std::vec::IntoIter { + match self { + #linter_all_rules_match_arms } } } impl RuleCodePrefix { - pub fn iter() -> ::std::vec::IntoIter { - vec![ #(#all_codes,)* ].into_iter() + pub fn iter() -> impl Iterator { + use strum::IntoEnumIterator; + + std::iter::empty() + #(.chain(#linter_idents::iter().map(|x| Self::#linter_idents(x))))* } } } diff --git a/crates/ruff_macros/src/newtype_index.rs b/crates/ruff_macros/src/newtype_index.rs index f6524b48a9..2c1f6e14ec 100644 --- a/crates/ruff_macros/src/newtype_index.rs +++ b/crates/ruff_macros/src/newtype_index.rs @@ -36,10 +36,11 @@ pub(super) fn generate_newtype_index(item: ItemStruct) -> syn::Result Self { - assert!(value <= Self::MAX as usize); + assert!(value <= Self::MAX_VALUE as usize); // SAFETY: // * The `value < u32::MAX` guarantees that the add doesn't overflow. @@ -49,7 +50,7 @@ pub(super) fn generate_newtype_index(item: ItemStruct) -> syn::Result Self { - assert!(value <= Self::MAX); + assert!(value <= Self::MAX_VALUE); // SAFETY: // * The `value < u32::MAX` guarantees that the add doesn't overflow. diff --git a/crates/ruff_macros/src/rule_namespace.rs b/crates/ruff_macros/src/rule_namespace.rs index 67dd7c4070..79ae54c19d 100644 --- a/crates/ruff_macros/src/rule_namespace.rs +++ b/crates/ruff_macros/src/rule_namespace.rs @@ -6,10 +6,16 @@ use syn::spanned::Spanned; use syn::{Attribute, Data, DataEnum, DeriveInput, Error, ExprLit, Lit, Meta, MetaNameValue}; pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result { - let DeriveInput { ident, data: Data::Enum(DataEnum { - variants, .. - }), .. } = input else { - return Err(Error::new(input.ident.span(), "only named fields are supported")); + let DeriveInput { + ident, + data: Data::Enum(DataEnum { variants, .. }), + .. + } = input + else { + return Err(Error::new( + input.ident.span(), + "only named fields are supported", + )); }; let mut parsed = Vec::new(); @@ -53,8 +59,12 @@ pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result syn::Result syn::Result<(String, String)> { - let Meta::NameValue(MetaNameValue{value: syn::Expr::Lit(ExprLit { lit: Lit::Str(doc_lit), ..}), ..}) = &doc_attr.meta else { - return Err(Error::new(doc_attr.span(), r#"expected doc attribute to be in the form of #[doc = "..."]"#)) + let Meta::NameValue(MetaNameValue { + value: + syn::Expr::Lit(ExprLit { + lit: Lit::Str(doc_lit), + .. + }), + .. + }) = &doc_attr.meta + else { + return Err(Error::new( + doc_attr.span(), + r#"expected doc attribute to be in the form of #[doc = "..."]"#, + )); }; parse_markdown_link(doc_lit.value().trim()) .map(|(name, url)| (name.to_string(), url.to_string())) diff --git a/crates/ruff_python_ast/Cargo.toml b/crates/ruff_python_ast/Cargo.toml index 14726020cd..855655b8ee 100644 --- a/crates/ruff_python_ast/Cargo.toml +++ b/crates/ruff_python_ast/Cargo.toml @@ -2,8 +2,13 @@ name = "ruff_python_ast" version = "0.0.0" publish = false +authors = { workspace = true } edition = { workspace = true } rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } [lib] diff --git a/crates/ruff_python_ast/src/comparable.rs b/crates/ruff_python_ast/src/comparable.rs index 6477644477..ed246f137e 100644 --- a/crates/ruff_python_ast/src/comparable.rs +++ b/crates/ruff_python_ast/src/comparable.rs @@ -1,13 +1,8 @@ -//! An equivalent object hierarchy to the [`Expr`] hierarchy, but with the +//! An equivalent object hierarchy to the `RustPython` AST hierarchy, but with the //! ability to compare expressions for equality (via [`Eq`] and [`Hash`]). use num_bigint::BigInt; -use rustpython_ast::Decorator; -use rustpython_parser::ast::{ - self, Alias, Arg, Arguments, Boolop, Cmpop, Comprehension, Constant, ConversionFlag, - Excepthandler, Expr, ExprContext, Identifier, Int, Keyword, MatchCase, Operator, Pattern, Stmt, - Unaryop, Withitem, -}; +use rustpython_parser::ast; #[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)] pub enum ComparableExprContext { @@ -16,27 +11,27 @@ pub enum ComparableExprContext { Del, } -impl From<&ExprContext> for ComparableExprContext { - fn from(ctx: &ExprContext) -> Self { +impl From<&ast::ExprContext> for ComparableExprContext { + fn from(ctx: &ast::ExprContext) -> Self { match ctx { - ExprContext::Load => Self::Load, - ExprContext::Store => Self::Store, - ExprContext::Del => Self::Del, + ast::ExprContext::Load => Self::Load, + ast::ExprContext::Store => Self::Store, + ast::ExprContext::Del => Self::Del, } } } #[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)] -pub enum ComparableBoolop { +pub enum ComparableBoolOp { And, Or, } -impl From<&Boolop> for ComparableBoolop { - fn from(op: &Boolop) -> Self { +impl From for ComparableBoolOp { + fn from(op: ast::BoolOp) -> Self { match op { - Boolop::And => Self::And, - Boolop::Or => Self::Or, + ast::BoolOp::And => Self::And, + ast::BoolOp::Or => Self::Or, } } } @@ -58,47 +53,47 @@ pub enum ComparableOperator { FloorDiv, } -impl From<&Operator> for ComparableOperator { - fn from(op: &Operator) -> Self { +impl From for ComparableOperator { + fn from(op: ast::Operator) -> Self { match op { - Operator::Add => Self::Add, - Operator::Sub => Self::Sub, - Operator::Mult => Self::Mult, - Operator::MatMult => Self::MatMult, - Operator::Div => Self::Div, - Operator::Mod => Self::Mod, - Operator::Pow => Self::Pow, - Operator::LShift => Self::LShift, - Operator::RShift => Self::RShift, - Operator::BitOr => Self::BitOr, - Operator::BitXor => Self::BitXor, - Operator::BitAnd => Self::BitAnd, - Operator::FloorDiv => Self::FloorDiv, + ast::Operator::Add => Self::Add, + ast::Operator::Sub => Self::Sub, + ast::Operator::Mult => Self::Mult, + ast::Operator::MatMult => Self::MatMult, + ast::Operator::Div => Self::Div, + ast::Operator::Mod => Self::Mod, + ast::Operator::Pow => Self::Pow, + ast::Operator::LShift => Self::LShift, + ast::Operator::RShift => Self::RShift, + ast::Operator::BitOr => Self::BitOr, + ast::Operator::BitXor => Self::BitXor, + ast::Operator::BitAnd => Self::BitAnd, + ast::Operator::FloorDiv => Self::FloorDiv, } } } #[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)] -pub enum ComparableUnaryop { +pub enum ComparableUnaryOp { Invert, Not, UAdd, USub, } -impl From<&Unaryop> for ComparableUnaryop { - fn from(op: &Unaryop) -> Self { +impl From for ComparableUnaryOp { + fn from(op: ast::UnaryOp) -> Self { match op { - Unaryop::Invert => Self::Invert, - Unaryop::Not => Self::Not, - Unaryop::UAdd => Self::UAdd, - Unaryop::USub => Self::USub, + ast::UnaryOp::Invert => Self::Invert, + ast::UnaryOp::Not => Self::Not, + ast::UnaryOp::UAdd => Self::UAdd, + ast::UnaryOp::USub => Self::USub, } } } #[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)] -pub enum ComparableCmpop { +pub enum ComparableCmpOp { Eq, NotEq, Lt, @@ -111,31 +106,31 @@ pub enum ComparableCmpop { NotIn, } -impl From<&Cmpop> for ComparableCmpop { - fn from(op: &Cmpop) -> Self { +impl From for ComparableCmpOp { + fn from(op: ast::CmpOp) -> Self { match op { - Cmpop::Eq => Self::Eq, - Cmpop::NotEq => Self::NotEq, - Cmpop::Lt => Self::Lt, - Cmpop::LtE => Self::LtE, - Cmpop::Gt => Self::Gt, - Cmpop::GtE => Self::GtE, - Cmpop::Is => Self::Is, - Cmpop::IsNot => Self::IsNot, - Cmpop::In => Self::In, - Cmpop::NotIn => Self::NotIn, + ast::CmpOp::Eq => Self::Eq, + ast::CmpOp::NotEq => Self::NotEq, + ast::CmpOp::Lt => Self::Lt, + ast::CmpOp::LtE => Self::LtE, + ast::CmpOp::Gt => Self::Gt, + ast::CmpOp::GtE => Self::GtE, + ast::CmpOp::Is => Self::Is, + ast::CmpOp::IsNot => Self::IsNot, + ast::CmpOp::In => Self::In, + ast::CmpOp::NotIn => Self::NotIn, } } } #[derive(Debug, PartialEq, Eq, Hash)] pub struct ComparableAlias<'a> { - pub name: &'a str, - pub asname: Option<&'a str>, + name: &'a str, + asname: Option<&'a str>, } -impl<'a> From<&'a Alias> for ComparableAlias<'a> { - fn from(alias: &'a Alias) -> Self { +impl<'a> From<&'a ast::Alias> for ComparableAlias<'a> { + fn from(alias: &'a ast::Alias) -> Self { Self { name: alias.name.as_str(), asname: alias.asname.as_deref(), @@ -144,122 +139,154 @@ impl<'a> From<&'a Alias> for ComparableAlias<'a> { } #[derive(Debug, PartialEq, Eq, Hash)] -pub struct ComparableWithitem<'a> { - pub context_expr: ComparableExpr<'a>, - pub optional_vars: Option>, +pub struct ComparableWithItem<'a> { + context_expr: ComparableExpr<'a>, + optional_vars: Option>, } -impl<'a> From<&'a Withitem> for ComparableWithitem<'a> { - fn from(withitem: &'a Withitem) -> Self { +impl<'a> From<&'a ast::WithItem> for ComparableWithItem<'a> { + fn from(with_item: &'a ast::WithItem) -> Self { Self { - context_expr: (&withitem.context_expr).into(), - optional_vars: withitem.optional_vars.as_ref().map(Into::into), + context_expr: (&with_item.context_expr).into(), + optional_vars: with_item.optional_vars.as_ref().map(Into::into), } } } +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct PatternMatchValue<'a> { + value: ComparableExpr<'a>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct PatternMatchSingleton<'a> { + value: ComparableConstant<'a>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct PatternMatchSequence<'a> { + patterns: Vec>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct PatternMatchMapping<'a> { + keys: Vec>, + patterns: Vec>, + rest: Option<&'a str>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct PatternMatchClass<'a> { + cls: ComparableExpr<'a>, + patterns: Vec>, + kwd_attrs: Vec<&'a str>, + kwd_patterns: Vec>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct PatternMatchStar<'a> { + name: Option<&'a str>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct PatternMatchAs<'a> { + pattern: Option>>, + name: Option<&'a str>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct PatternMatchOr<'a> { + patterns: Vec>, +} + #[allow(clippy::enum_variant_names)] #[derive(Debug, PartialEq, Eq, Hash)] pub enum ComparablePattern<'a> { - MatchValue { - value: ComparableExpr<'a>, - }, - MatchSingleton { - value: ComparableConstant<'a>, - }, - MatchSequence { - patterns: Vec>, - }, - MatchMapping { - keys: Vec>, - patterns: Vec>, - rest: Option<&'a str>, - }, - MatchClass { - cls: ComparableExpr<'a>, - patterns: Vec>, - kwd_attrs: Vec<&'a str>, - kwd_patterns: Vec>, - }, - MatchStar { - name: Option<&'a str>, - }, - MatchAs { - pattern: Option>>, - name: Option<&'a str>, - }, - MatchOr { - patterns: Vec>, - }, + MatchValue(PatternMatchValue<'a>), + MatchSingleton(PatternMatchSingleton<'a>), + MatchSequence(PatternMatchSequence<'a>), + MatchMapping(PatternMatchMapping<'a>), + MatchClass(PatternMatchClass<'a>), + MatchStar(PatternMatchStar<'a>), + MatchAs(PatternMatchAs<'a>), + MatchOr(PatternMatchOr<'a>), } -impl<'a> From<&'a Pattern> for ComparablePattern<'a> { - fn from(pattern: &'a Pattern) -> Self { +impl<'a> From<&'a ast::Pattern> for ComparablePattern<'a> { + fn from(pattern: &'a ast::Pattern) -> Self { match pattern { - Pattern::MatchValue(ast::PatternMatchValue { value, .. }) => Self::MatchValue { - value: value.into(), - }, - Pattern::MatchSingleton(ast::PatternMatchSingleton { value, .. }) => { - Self::MatchSingleton { + ast::Pattern::MatchValue(ast::PatternMatchValue { value, .. }) => { + Self::MatchValue(PatternMatchValue { value: value.into(), - } + }) } - Pattern::MatchSequence(ast::PatternMatchSequence { patterns, .. }) => { - Self::MatchSequence { + ast::Pattern::MatchSingleton(ast::PatternMatchSingleton { value, .. }) => { + Self::MatchSingleton(PatternMatchSingleton { + value: value.into(), + }) + } + ast::Pattern::MatchSequence(ast::PatternMatchSequence { patterns, .. }) => { + Self::MatchSequence(PatternMatchSequence { patterns: patterns.iter().map(Into::into).collect(), - } + }) } - Pattern::MatchMapping(ast::PatternMatchMapping { + ast::Pattern::MatchMapping(ast::PatternMatchMapping { keys, patterns, rest, .. - }) => Self::MatchMapping { + }) => Self::MatchMapping(PatternMatchMapping { keys: keys.iter().map(Into::into).collect(), patterns: patterns.iter().map(Into::into).collect(), rest: rest.as_deref(), - }, - Pattern::MatchClass(ast::PatternMatchClass { + }), + ast::Pattern::MatchClass(ast::PatternMatchClass { cls, patterns, kwd_attrs, kwd_patterns, .. - }) => Self::MatchClass { + }) => Self::MatchClass(PatternMatchClass { cls: cls.into(), patterns: patterns.iter().map(Into::into).collect(), - kwd_attrs: kwd_attrs.iter().map(Identifier::as_str).collect(), + kwd_attrs: kwd_attrs.iter().map(ast::Identifier::as_str).collect(), kwd_patterns: kwd_patterns.iter().map(Into::into).collect(), - }, - Pattern::MatchStar(ast::PatternMatchStar { name, .. }) => Self::MatchStar { - name: name.as_deref(), - }, - Pattern::MatchAs(ast::PatternMatchAs { pattern, name, .. }) => Self::MatchAs { - pattern: pattern.as_ref().map(Into::into), - name: name.as_deref(), - }, - Pattern::MatchOr(ast::PatternMatchOr { patterns, .. }) => Self::MatchOr { - patterns: patterns.iter().map(Into::into).collect(), - }, + }), + ast::Pattern::MatchStar(ast::PatternMatchStar { name, .. }) => { + Self::MatchStar(PatternMatchStar { + name: name.as_deref(), + }) + } + ast::Pattern::MatchAs(ast::PatternMatchAs { pattern, name, .. }) => { + Self::MatchAs(PatternMatchAs { + pattern: pattern.as_ref().map(Into::into), + name: name.as_deref(), + }) + } + ast::Pattern::MatchOr(ast::PatternMatchOr { patterns, .. }) => { + Self::MatchOr(PatternMatchOr { + patterns: patterns.iter().map(Into::into).collect(), + }) + } } } } -impl<'a> From<&'a Box> for Box> { - fn from(pattern: &'a Box) -> Self { +impl<'a> From<&'a Box> for Box> { + fn from(pattern: &'a Box) -> Self { Box::new((&**pattern).into()) } } #[derive(Debug, PartialEq, Eq, Hash)] pub struct ComparableMatchCase<'a> { - pub pattern: ComparablePattern<'a>, - pub guard: Option>, - pub body: Vec>, + pattern: ComparablePattern<'a>, + guard: Option>, + body: Vec>, } -impl<'a> From<&'a MatchCase> for ComparableMatchCase<'a> { - fn from(match_case: &'a MatchCase) -> Self { +impl<'a> From<&'a ast::MatchCase> for ComparableMatchCase<'a> { + fn from(match_case: &'a ast::MatchCase) -> Self { Self { pattern: (&match_case.pattern).into(), guard: match_case.guard.as_ref().map(Into::into), @@ -270,11 +297,11 @@ impl<'a> From<&'a MatchCase> for ComparableMatchCase<'a> { #[derive(Debug, PartialEq, Eq, Hash)] pub struct ComparableDecorator<'a> { - pub expression: ComparableExpr<'a>, + expression: ComparableExpr<'a>, } -impl<'a> From<&'a Decorator> for ComparableDecorator<'a> { - fn from(decorator: &'a Decorator) -> Self { +impl<'a> From<&'a ast::Decorator> for ComparableDecorator<'a> { + fn from(decorator: &'a ast::Decorator) -> Self { Self { expression: (&decorator.expression).into(), } @@ -294,71 +321,67 @@ pub enum ComparableConstant<'a> { Ellipsis, } -impl<'a> From<&'a Constant> for ComparableConstant<'a> { - fn from(constant: &'a Constant) -> Self { +impl<'a> From<&'a ast::Constant> for ComparableConstant<'a> { + fn from(constant: &'a ast::Constant) -> Self { match constant { - Constant::None => Self::None, - Constant::Bool(value) => Self::Bool(value), - Constant::Str(value) => Self::Str(value), - Constant::Bytes(value) => Self::Bytes(value), - Constant::Int(value) => Self::Int(value), - Constant::Tuple(value) => Self::Tuple(value.iter().map(Into::into).collect()), - Constant::Float(value) => Self::Float(value.to_bits()), - Constant::Complex { real, imag } => Self::Complex { + ast::Constant::None => Self::None, + ast::Constant::Bool(value) => Self::Bool(value), + ast::Constant::Str(value) => Self::Str(value), + ast::Constant::Bytes(value) => Self::Bytes(value), + ast::Constant::Int(value) => Self::Int(value), + ast::Constant::Tuple(value) => Self::Tuple(value.iter().map(Into::into).collect()), + ast::Constant::Float(value) => Self::Float(value.to_bits()), + ast::Constant::Complex { real, imag } => Self::Complex { real: real.to_bits(), imag: imag.to_bits(), }, - Constant::Ellipsis => Self::Ellipsis, + ast::Constant::Ellipsis => Self::Ellipsis, } } } #[derive(Debug, PartialEq, Eq, Hash)] pub struct ComparableArguments<'a> { - pub posonlyargs: Vec>, - pub args: Vec>, - pub vararg: Option>, - pub kwonlyargs: Vec>, - pub kw_defaults: Vec>, - pub kwarg: Option>, - pub defaults: Vec>, + posonlyargs: Vec>, + args: Vec>, + vararg: Option>, + kwonlyargs: Vec>, + kwarg: Option>, } -impl<'a> From<&'a Arguments> for ComparableArguments<'a> { - fn from(arguments: &'a Arguments) -> Self { +impl<'a> From<&'a ast::Arguments> for ComparableArguments<'a> { + fn from(arguments: &'a ast::Arguments) -> Self { Self { posonlyargs: arguments.posonlyargs.iter().map(Into::into).collect(), args: arguments.args.iter().map(Into::into).collect(), vararg: arguments.vararg.as_ref().map(Into::into), kwonlyargs: arguments.kwonlyargs.iter().map(Into::into).collect(), - kw_defaults: arguments.kw_defaults.iter().map(Into::into).collect(), - kwarg: arguments.vararg.as_ref().map(Into::into), - defaults: arguments.defaults.iter().map(Into::into).collect(), + kwarg: arguments.kwarg.as_ref().map(Into::into), } } } -impl<'a> From<&'a Box> for ComparableArguments<'a> { - fn from(arguments: &'a Box) -> Self { +impl<'a> From<&'a Box> for ComparableArguments<'a> { + fn from(arguments: &'a Box) -> Self { (&**arguments).into() } } -impl<'a> From<&'a Box> for ComparableArg<'a> { - fn from(arg: &'a Box) -> Self { +impl<'a> From<&'a Box> for ComparableArg<'a> { + fn from(arg: &'a Box) -> Self { (&**arg).into() } } #[derive(Debug, PartialEq, Eq, Hash)] pub struct ComparableArg<'a> { - pub arg: &'a str, - pub annotation: Option>>, - pub type_comment: Option<&'a str>, + arg: &'a str, + annotation: Option>>, + type_comment: Option<&'a str>, } -impl<'a> From<&'a Arg> for ComparableArg<'a> { - fn from(arg: &'a Arg) -> Self { +impl<'a> From<&'a ast::Arg> for ComparableArg<'a> { + fn from(arg: &'a ast::Arg) -> Self { Self { arg: arg.arg.as_str(), annotation: arg.annotation.as_ref().map(Into::into), @@ -368,15 +391,30 @@ impl<'a> From<&'a Arg> for ComparableArg<'a> { } #[derive(Debug, PartialEq, Eq, Hash)] -pub struct ComparableKeyword<'a> { - pub arg: Option<&'a str>, - pub value: ComparableExpr<'a>, +pub struct ComparableArgWithDefault<'a> { + def: ComparableArg<'a>, + default: Option>, } -impl<'a> From<&'a Keyword> for ComparableKeyword<'a> { - fn from(keyword: &'a Keyword) -> Self { +impl<'a> From<&'a ast::ArgWithDefault> for ComparableArgWithDefault<'a> { + fn from(arg: &'a ast::ArgWithDefault) -> Self { Self { - arg: keyword.arg.as_ref().map(Identifier::as_str), + def: (&arg.def).into(), + default: arg.default.as_ref().map(Into::into), + } + } +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ComparableKeyword<'a> { + arg: Option<&'a str>, + value: ComparableExpr<'a>, +} + +impl<'a> From<&'a ast::Keyword> for ComparableKeyword<'a> { + fn from(keyword: &'a ast::Keyword) -> Self { + Self { + arg: keyword.arg.as_ref().map(ast::Identifier::as_str), value: (&keyword.value).into(), } } @@ -384,14 +422,14 @@ impl<'a> From<&'a Keyword> for ComparableKeyword<'a> { #[derive(Debug, PartialEq, Eq, Hash)] pub struct ComparableComprehension<'a> { - pub target: ComparableExpr<'a>, - pub iter: ComparableExpr<'a>, - pub ifs: Vec>, - pub is_async: bool, + target: ComparableExpr<'a>, + iter: ComparableExpr<'a>, + ifs: Vec>, + is_async: bool, } -impl<'a> From<&'a Comprehension> for ComparableComprehension<'a> { - fn from(comprehension: &'a Comprehension) -> Self { +impl<'a> From<&'a ast::Comprehension> for ComparableComprehension<'a> { + fn from(comprehension: &'a ast::Comprehension) -> Self { Self { target: (&comprehension.target).into(), iter: (&comprehension.iter).into(), @@ -402,520 +440,679 @@ impl<'a> From<&'a Comprehension> for ComparableComprehension<'a> { } #[derive(Debug, PartialEq, Eq, Hash)] -pub enum ComparableExcepthandler<'a> { - ExceptHandler { - type_: Option>, - name: Option<&'a str>, - body: Vec>, - }, +pub struct ExceptHandlerExceptHandler<'a> { + type_: Option>>, + name: Option<&'a str>, + body: Vec>, } -impl<'a> From<&'a Excepthandler> for ComparableExcepthandler<'a> { - fn from(excepthandler: &'a Excepthandler) -> Self { - let Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { - type_, name, body, .. - }) = excepthandler; - Self::ExceptHandler { +#[derive(Debug, PartialEq, Eq, Hash)] +pub enum ComparableExceptHandler<'a> { + ExceptHandler(ExceptHandlerExceptHandler<'a>), +} + +impl<'a> From<&'a ast::ExceptHandler> for ComparableExceptHandler<'a> { + fn from(except_handler: &'a ast::ExceptHandler) -> Self { + let ast::ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { + type_, + name, + body, + .. + }) = except_handler; + Self::ExceptHandler(ExceptHandlerExceptHandler { type_: type_.as_ref().map(Into::into), name: name.as_deref(), body: body.iter().map(Into::into).collect(), - } + }) } } #[derive(Debug, PartialEq, Eq, Hash)] -pub enum ComparableExpr<'a> { - BoolOp { - op: ComparableBoolop, - values: Vec>, - }, - NamedExpr { - target: Box>, - value: Box>, - }, - BinOp { - left: Box>, - op: ComparableOperator, - right: Box>, - }, - UnaryOp { - op: ComparableUnaryop, - operand: Box>, - }, - Lambda { - args: ComparableArguments<'a>, - body: Box>, - }, - IfExp { - test: Box>, - body: Box>, - orelse: Box>, - }, - Dict { - keys: Vec>>, - values: Vec>, - }, - Set { - elts: Vec>, - }, - ListComp { - elt: Box>, - generators: Vec>, - }, - SetComp { - elt: Box>, - generators: Vec>, - }, - DictComp { - key: Box>, - value: Box>, - generators: Vec>, - }, - GeneratorExp { - elt: Box>, - generators: Vec>, - }, - Await { - value: Box>, - }, - Yield { - value: Option>>, - }, - YieldFrom { - value: Box>, - }, - Compare { - left: Box>, - ops: Vec, - comparators: Vec>, - }, - Call { - func: Box>, - args: Vec>, - keywords: Vec>, - }, - FormattedValue { - value: Box>, - conversion: ConversionFlag, - format_spec: Option>>, - }, - JoinedStr { - values: Vec>, - }, - Constant { - value: ComparableConstant<'a>, - kind: Option<&'a str>, - }, - Attribute { - value: Box>, - attr: &'a str, - ctx: ComparableExprContext, - }, - Subscript { - value: Box>, - slice: Box>, - ctx: ComparableExprContext, - }, - Starred { - value: Box>, - ctx: ComparableExprContext, - }, - Name { - id: &'a str, - ctx: ComparableExprContext, - }, - List { - elts: Vec>, - ctx: ComparableExprContext, - }, - Tuple { - elts: Vec>, - ctx: ComparableExprContext, - }, - Slice { - lower: Option>>, - upper: Option>>, - step: Option>>, - }, +pub struct ExprBoolOp<'a> { + op: ComparableBoolOp, + values: Vec>, } -impl<'a> From<&'a Box> for Box> { - fn from(expr: &'a Box) -> Self { +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprNamedExpr<'a> { + target: Box>, + value: Box>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprBinOp<'a> { + left: Box>, + op: ComparableOperator, + right: Box>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprUnaryOp<'a> { + op: ComparableUnaryOp, + operand: Box>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprLambda<'a> { + args: ComparableArguments<'a>, + body: Box>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprIfExp<'a> { + test: Box>, + body: Box>, + orelse: Box>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprDict<'a> { + keys: Vec>>, + values: Vec>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprSet<'a> { + elts: Vec>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprListComp<'a> { + elt: Box>, + generators: Vec>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprSetComp<'a> { + elt: Box>, + generators: Vec>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprDictComp<'a> { + key: Box>, + value: Box>, + generators: Vec>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprGeneratorExp<'a> { + elt: Box>, + generators: Vec>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprAwait<'a> { + value: Box>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprYield<'a> { + value: Option>>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprYieldFrom<'a> { + value: Box>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprCompare<'a> { + left: Box>, + ops: Vec, + comparators: Vec>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprCall<'a> { + func: Box>, + args: Vec>, + keywords: Vec>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprFormattedValue<'a> { + value: Box>, + conversion: ast::ConversionFlag, + format_spec: Option>>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprJoinedStr<'a> { + values: Vec>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprConstant<'a> { + value: ComparableConstant<'a>, + kind: Option<&'a str>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprAttribute<'a> { + value: Box>, + attr: &'a str, + ctx: ComparableExprContext, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprSubscript<'a> { + value: Box>, + slice: Box>, + ctx: ComparableExprContext, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprStarred<'a> { + value: Box>, + ctx: ComparableExprContext, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprName<'a> { + id: &'a str, + ctx: ComparableExprContext, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprList<'a> { + elts: Vec>, + ctx: ComparableExprContext, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprTuple<'a> { + elts: Vec>, + ctx: ComparableExprContext, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprSlice<'a> { + lower: Option>>, + upper: Option>>, + step: Option>>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub enum ComparableExpr<'a> { + BoolOp(ExprBoolOp<'a>), + NamedExpr(ExprNamedExpr<'a>), + BinOp(ExprBinOp<'a>), + UnaryOp(ExprUnaryOp<'a>), + Lambda(ExprLambda<'a>), + IfExp(ExprIfExp<'a>), + Dict(ExprDict<'a>), + Set(ExprSet<'a>), + ListComp(ExprListComp<'a>), + SetComp(ExprSetComp<'a>), + DictComp(ExprDictComp<'a>), + GeneratorExp(ExprGeneratorExp<'a>), + Await(ExprAwait<'a>), + Yield(ExprYield<'a>), + YieldFrom(ExprYieldFrom<'a>), + Compare(ExprCompare<'a>), + Call(ExprCall<'a>), + FormattedValue(ExprFormattedValue<'a>), + JoinedStr(ExprJoinedStr<'a>), + Constant(ExprConstant<'a>), + Attribute(ExprAttribute<'a>), + Subscript(ExprSubscript<'a>), + Starred(ExprStarred<'a>), + Name(ExprName<'a>), + List(ExprList<'a>), + Tuple(ExprTuple<'a>), + Slice(ExprSlice<'a>), +} + +impl<'a> From<&'a Box> for Box> { + fn from(expr: &'a Box) -> Self { Box::new((&**expr).into()) } } -impl<'a> From<&'a Box> for ComparableExpr<'a> { - fn from(expr: &'a Box) -> Self { +impl<'a> From<&'a Box> for ComparableExpr<'a> { + fn from(expr: &'a Box) -> Self { (&**expr).into() } } -impl<'a> From<&'a Expr> for ComparableExpr<'a> { - fn from(expr: &'a Expr) -> Self { +impl<'a> From<&'a ast::Expr> for ComparableExpr<'a> { + fn from(expr: &'a ast::Expr) -> Self { match expr { - Expr::BoolOp(ast::ExprBoolOp { + ast::Expr::BoolOp(ast::ExprBoolOp { op, values, range: _range, - }) => Self::BoolOp { - op: op.into(), + }) => Self::BoolOp(ExprBoolOp { + op: (*op).into(), values: values.iter().map(Into::into).collect(), - }, - Expr::NamedExpr(ast::ExprNamedExpr { + }), + ast::Expr::NamedExpr(ast::ExprNamedExpr { target, value, range: _range, - }) => Self::NamedExpr { + }) => Self::NamedExpr(ExprNamedExpr { target: target.into(), value: value.into(), - }, - Expr::BinOp(ast::ExprBinOp { + }), + ast::Expr::BinOp(ast::ExprBinOp { left, op, right, range: _range, - }) => Self::BinOp { + }) => Self::BinOp(ExprBinOp { left: left.into(), - op: op.into(), + op: (*op).into(), right: right.into(), - }, - Expr::UnaryOp(ast::ExprUnaryOp { + }), + ast::Expr::UnaryOp(ast::ExprUnaryOp { op, operand, range: _range, - }) => Self::UnaryOp { - op: op.into(), + }) => Self::UnaryOp(ExprUnaryOp { + op: (*op).into(), operand: operand.into(), - }, - Expr::Lambda(ast::ExprLambda { + }), + ast::Expr::Lambda(ast::ExprLambda { args, body, range: _range, - }) => Self::Lambda { + }) => Self::Lambda(ExprLambda { args: (&**args).into(), body: body.into(), - }, - Expr::IfExp(ast::ExprIfExp { + }), + ast::Expr::IfExp(ast::ExprIfExp { test, body, orelse, range: _range, - }) => Self::IfExp { + }) => Self::IfExp(ExprIfExp { test: test.into(), body: body.into(), orelse: orelse.into(), - }, - Expr::Dict(ast::ExprDict { + }), + ast::Expr::Dict(ast::ExprDict { keys, values, range: _range, - }) => Self::Dict { + }) => Self::Dict(ExprDict { keys: keys .iter() .map(|expr| expr.as_ref().map(Into::into)) .collect(), values: values.iter().map(Into::into).collect(), - }, - Expr::Set(ast::ExprSet { + }), + ast::Expr::Set(ast::ExprSet { elts, range: _range, - }) => Self::Set { + }) => Self::Set(ExprSet { elts: elts.iter().map(Into::into).collect(), - }, - Expr::ListComp(ast::ExprListComp { + }), + ast::Expr::ListComp(ast::ExprListComp { elt, generators, range: _range, - }) => Self::ListComp { + }) => Self::ListComp(ExprListComp { elt: elt.into(), generators: generators.iter().map(Into::into).collect(), - }, - Expr::SetComp(ast::ExprSetComp { + }), + ast::Expr::SetComp(ast::ExprSetComp { elt, generators, range: _range, - }) => Self::SetComp { + }) => Self::SetComp(ExprSetComp { elt: elt.into(), generators: generators.iter().map(Into::into).collect(), - }, - Expr::DictComp(ast::ExprDictComp { + }), + ast::Expr::DictComp(ast::ExprDictComp { key, value, generators, range: _range, - }) => Self::DictComp { + }) => Self::DictComp(ExprDictComp { key: key.into(), value: value.into(), generators: generators.iter().map(Into::into).collect(), - }, - Expr::GeneratorExp(ast::ExprGeneratorExp { + }), + ast::Expr::GeneratorExp(ast::ExprGeneratorExp { elt, generators, range: _range, - }) => Self::GeneratorExp { + }) => Self::GeneratorExp(ExprGeneratorExp { elt: elt.into(), generators: generators.iter().map(Into::into).collect(), - }, - Expr::Await(ast::ExprAwait { + }), + ast::Expr::Await(ast::ExprAwait { value, range: _range, - }) => Self::Await { + }) => Self::Await(ExprAwait { value: value.into(), - }, - Expr::Yield(ast::ExprYield { + }), + ast::Expr::Yield(ast::ExprYield { value, range: _range, - }) => Self::Yield { + }) => Self::Yield(ExprYield { value: value.as_ref().map(Into::into), - }, - Expr::YieldFrom(ast::ExprYieldFrom { + }), + ast::Expr::YieldFrom(ast::ExprYieldFrom { value, range: _range, - }) => Self::YieldFrom { + }) => Self::YieldFrom(ExprYieldFrom { value: value.into(), - }, - Expr::Compare(ast::ExprCompare { + }), + ast::Expr::Compare(ast::ExprCompare { left, ops, comparators, range: _range, - }) => Self::Compare { + }) => Self::Compare(ExprCompare { left: left.into(), - ops: ops.iter().map(Into::into).collect(), + ops: ops.iter().copied().map(Into::into).collect(), comparators: comparators.iter().map(Into::into).collect(), - }, - Expr::Call(ast::ExprCall { + }), + ast::Expr::Call(ast::ExprCall { func, args, keywords, range: _range, - }) => Self::Call { + }) => Self::Call(ExprCall { func: func.into(), args: args.iter().map(Into::into).collect(), keywords: keywords.iter().map(Into::into).collect(), - }, - Expr::FormattedValue(ast::ExprFormattedValue { + }), + ast::Expr::FormattedValue(ast::ExprFormattedValue { value, conversion, format_spec, range: _range, - }) => Self::FormattedValue { + }) => Self::FormattedValue(ExprFormattedValue { value: value.into(), conversion: *conversion, format_spec: format_spec.as_ref().map(Into::into), - }, - Expr::JoinedStr(ast::ExprJoinedStr { + }), + ast::Expr::JoinedStr(ast::ExprJoinedStr { values, range: _range, - }) => Self::JoinedStr { + }) => Self::JoinedStr(ExprJoinedStr { values: values.iter().map(Into::into).collect(), - }, - Expr::Constant(ast::ExprConstant { + }), + ast::Expr::Constant(ast::ExprConstant { value, kind, range: _range, - }) => Self::Constant { + }) => Self::Constant(ExprConstant { value: value.into(), kind: kind.as_ref().map(String::as_str), - }, - Expr::Attribute(ast::ExprAttribute { + }), + ast::Expr::Attribute(ast::ExprAttribute { value, attr, ctx, range: _range, - }) => Self::Attribute { + }) => Self::Attribute(ExprAttribute { value: value.into(), attr: attr.as_str(), ctx: ctx.into(), - }, - Expr::Subscript(ast::ExprSubscript { + }), + ast::Expr::Subscript(ast::ExprSubscript { value, slice, ctx, range: _range, - }) => Self::Subscript { + }) => Self::Subscript(ExprSubscript { value: value.into(), slice: slice.into(), ctx: ctx.into(), - }, - Expr::Starred(ast::ExprStarred { + }), + ast::Expr::Starred(ast::ExprStarred { value, ctx, range: _range, - }) => Self::Starred { + }) => Self::Starred(ExprStarred { value: value.into(), ctx: ctx.into(), - }, - Expr::Name(ast::ExprName { + }), + ast::Expr::Name(ast::ExprName { id, ctx, range: _range, - }) => Self::Name { + }) => Self::Name(ExprName { id: id.as_str(), ctx: ctx.into(), - }, - Expr::List(ast::ExprList { + }), + ast::Expr::List(ast::ExprList { elts, ctx, range: _range, - }) => Self::List { + }) => Self::List(ExprList { elts: elts.iter().map(Into::into).collect(), ctx: ctx.into(), - }, - Expr::Tuple(ast::ExprTuple { + }), + ast::Expr::Tuple(ast::ExprTuple { elts, ctx, range: _range, - }) => Self::Tuple { + }) => Self::Tuple(ExprTuple { elts: elts.iter().map(Into::into).collect(), ctx: ctx.into(), - }, - Expr::Slice(ast::ExprSlice { + }), + ast::Expr::Slice(ast::ExprSlice { lower, upper, step, range: _range, - }) => Self::Slice { + }) => Self::Slice(ExprSlice { lower: lower.as_ref().map(Into::into), upper: upper.as_ref().map(Into::into), step: step.as_ref().map(Into::into), - }, + }), } } } +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtFunctionDef<'a> { + name: &'a str, + args: ComparableArguments<'a>, + body: Vec>, + decorator_list: Vec>, + returns: Option>, + type_comment: Option<&'a str>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtAsyncFunctionDef<'a> { + name: &'a str, + args: ComparableArguments<'a>, + body: Vec>, + decorator_list: Vec>, + returns: Option>, + type_comment: Option<&'a str>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtClassDef<'a> { + name: &'a str, + bases: Vec>, + keywords: Vec>, + body: Vec>, + decorator_list: Vec>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtReturn<'a> { + value: Option>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtDelete<'a> { + targets: Vec>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtAssign<'a> { + targets: Vec>, + value: ComparableExpr<'a>, + type_comment: Option<&'a str>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtAugAssign<'a> { + target: ComparableExpr<'a>, + op: ComparableOperator, + value: ComparableExpr<'a>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtAnnAssign<'a> { + target: ComparableExpr<'a>, + annotation: ComparableExpr<'a>, + value: Option>, + simple: bool, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtFor<'a> { + target: ComparableExpr<'a>, + iter: ComparableExpr<'a>, + body: Vec>, + orelse: Vec>, + type_comment: Option<&'a str>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtAsyncFor<'a> { + target: ComparableExpr<'a>, + iter: ComparableExpr<'a>, + body: Vec>, + orelse: Vec>, + type_comment: Option<&'a str>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtWhile<'a> { + test: ComparableExpr<'a>, + body: Vec>, + orelse: Vec>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtIf<'a> { + test: ComparableExpr<'a>, + body: Vec>, + orelse: Vec>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtWith<'a> { + items: Vec>, + body: Vec>, + type_comment: Option<&'a str>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtAsyncWith<'a> { + items: Vec>, + body: Vec>, + type_comment: Option<&'a str>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtMatch<'a> { + subject: ComparableExpr<'a>, + cases: Vec>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtRaise<'a> { + exc: Option>, + cause: Option>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtTry<'a> { + body: Vec>, + handlers: Vec>, + orelse: Vec>, + finalbody: Vec>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtTryStar<'a> { + body: Vec>, + handlers: Vec>, + orelse: Vec>, + finalbody: Vec>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtAssert<'a> { + test: ComparableExpr<'a>, + msg: Option>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtImport<'a> { + names: Vec>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtImportFrom<'a> { + module: Option<&'a str>, + names: Vec>, + level: Option, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtGlobal<'a> { + names: Vec<&'a str>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtNonlocal<'a> { + names: Vec<&'a str>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct StmtExpr<'a> { + value: ComparableExpr<'a>, +} + #[derive(Debug, PartialEq, Eq, Hash)] pub enum ComparableStmt<'a> { - FunctionDef { - name: &'a str, - args: ComparableArguments<'a>, - body: Vec>, - decorator_list: Vec>, - returns: Option>, - type_comment: Option<&'a str>, - }, - AsyncFunctionDef { - name: &'a str, - args: ComparableArguments<'a>, - body: Vec>, - decorator_list: Vec>, - returns: Option>, - type_comment: Option<&'a str>, - }, - ClassDef { - name: &'a str, - bases: Vec>, - keywords: Vec>, - body: Vec>, - decorator_list: Vec>, - }, - Return { - value: Option>, - }, - Delete { - targets: Vec>, - }, - Assign { - targets: Vec>, - value: ComparableExpr<'a>, - type_comment: Option<&'a str>, - }, - AugAssign { - target: ComparableExpr<'a>, - op: ComparableOperator, - value: ComparableExpr<'a>, - }, - AnnAssign { - target: ComparableExpr<'a>, - annotation: ComparableExpr<'a>, - value: Option>, - simple: bool, - }, - For { - target: ComparableExpr<'a>, - iter: ComparableExpr<'a>, - body: Vec>, - orelse: Vec>, - type_comment: Option<&'a str>, - }, - AsyncFor { - target: ComparableExpr<'a>, - iter: ComparableExpr<'a>, - body: Vec>, - orelse: Vec>, - type_comment: Option<&'a str>, - }, - While { - test: ComparableExpr<'a>, - body: Vec>, - orelse: Vec>, - }, - If { - test: ComparableExpr<'a>, - body: Vec>, - orelse: Vec>, - }, - With { - items: Vec>, - body: Vec>, - type_comment: Option<&'a str>, - }, - AsyncWith { - items: Vec>, - body: Vec>, - type_comment: Option<&'a str>, - }, - Match { - subject: ComparableExpr<'a>, - cases: Vec>, - }, - Raise { - exc: Option>, - cause: Option>, - }, - Try { - body: Vec>, - handlers: Vec>, - orelse: Vec>, - finalbody: Vec>, - }, - TryStar { - body: Vec>, - handlers: Vec>, - orelse: Vec>, - finalbody: Vec>, - }, - Assert { - test: ComparableExpr<'a>, - msg: Option>, - }, - Import { - names: Vec>, - }, - ImportFrom { - module: Option<&'a str>, - names: Vec>, - level: Option, - }, - Global { - names: Vec<&'a str>, - }, - Nonlocal { - names: Vec<&'a str>, - }, - Expr { - value: ComparableExpr<'a>, - }, + FunctionDef(StmtFunctionDef<'a>), + AsyncFunctionDef(StmtAsyncFunctionDef<'a>), + ClassDef(StmtClassDef<'a>), + Return(StmtReturn<'a>), + Delete(StmtDelete<'a>), + Assign(StmtAssign<'a>), + AugAssign(StmtAugAssign<'a>), + AnnAssign(StmtAnnAssign<'a>), + For(StmtFor<'a>), + AsyncFor(StmtAsyncFor<'a>), + While(StmtWhile<'a>), + If(StmtIf<'a>), + With(StmtWith<'a>), + AsyncWith(StmtAsyncWith<'a>), + Match(StmtMatch<'a>), + Raise(StmtRaise<'a>), + Try(StmtTry<'a>), + TryStar(StmtTryStar<'a>), + Assert(StmtAssert<'a>), + Import(StmtImport<'a>), + ImportFrom(StmtImportFrom<'a>), + Global(StmtGlobal<'a>), + Nonlocal(StmtNonlocal<'a>), + Expr(StmtExpr<'a>), Pass, Break, Continue, } -impl<'a> From<&'a Stmt> for ComparableStmt<'a> { - fn from(stmt: &'a Stmt) -> Self { +impl<'a> From<&'a ast::Stmt> for ComparableStmt<'a> { + fn from(stmt: &'a ast::Stmt) -> Self { match stmt { - Stmt::FunctionDef(ast::StmtFunctionDef { + ast::Stmt::FunctionDef(ast::StmtFunctionDef { name, args, body, @@ -923,15 +1120,15 @@ impl<'a> From<&'a Stmt> for ComparableStmt<'a> { returns, type_comment, range: _range, - }) => Self::FunctionDef { + }) => Self::FunctionDef(StmtFunctionDef { name: name.as_str(), args: args.into(), body: body.iter().map(Into::into).collect(), decorator_list: decorator_list.iter().map(Into::into).collect(), returns: returns.as_ref().map(Into::into), type_comment: type_comment.as_ref().map(String::as_str), - }, - Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { + }), + ast::Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { name, args, body, @@ -939,225 +1136,225 @@ impl<'a> From<&'a Stmt> for ComparableStmt<'a> { returns, type_comment, range: _range, - }) => Self::AsyncFunctionDef { + }) => Self::AsyncFunctionDef(StmtAsyncFunctionDef { name: name.as_str(), args: args.into(), body: body.iter().map(Into::into).collect(), decorator_list: decorator_list.iter().map(Into::into).collect(), returns: returns.as_ref().map(Into::into), type_comment: type_comment.as_ref().map(String::as_str), - }, - Stmt::ClassDef(ast::StmtClassDef { + }), + ast::Stmt::ClassDef(ast::StmtClassDef { name, bases, keywords, body, decorator_list, range: _range, - }) => Self::ClassDef { + }) => Self::ClassDef(StmtClassDef { name: name.as_str(), bases: bases.iter().map(Into::into).collect(), keywords: keywords.iter().map(Into::into).collect(), body: body.iter().map(Into::into).collect(), decorator_list: decorator_list.iter().map(Into::into).collect(), - }, - Stmt::Return(ast::StmtReturn { + }), + ast::Stmt::Return(ast::StmtReturn { value, range: _range, - }) => Self::Return { + }) => Self::Return(StmtReturn { value: value.as_ref().map(Into::into), - }, - Stmt::Delete(ast::StmtDelete { + }), + ast::Stmt::Delete(ast::StmtDelete { targets, range: _range, - }) => Self::Delete { + }) => Self::Delete(StmtDelete { targets: targets.iter().map(Into::into).collect(), - }, - Stmt::Assign(ast::StmtAssign { + }), + ast::Stmt::Assign(ast::StmtAssign { targets, value, type_comment, range: _range, - }) => Self::Assign { + }) => Self::Assign(StmtAssign { targets: targets.iter().map(Into::into).collect(), value: value.into(), type_comment: type_comment.as_ref().map(String::as_str), - }, - Stmt::AugAssign(ast::StmtAugAssign { + }), + ast::Stmt::AugAssign(ast::StmtAugAssign { target, op, value, range: _range, - }) => Self::AugAssign { + }) => Self::AugAssign(StmtAugAssign { target: target.into(), - op: op.into(), + op: (*op).into(), value: value.into(), - }, - Stmt::AnnAssign(ast::StmtAnnAssign { + }), + ast::Stmt::AnnAssign(ast::StmtAnnAssign { target, annotation, value, simple, range: _range, - }) => Self::AnnAssign { + }) => Self::AnnAssign(StmtAnnAssign { target: target.into(), annotation: annotation.into(), value: value.as_ref().map(Into::into), simple: *simple, - }, - Stmt::For(ast::StmtFor { + }), + ast::Stmt::For(ast::StmtFor { target, iter, body, orelse, type_comment, range: _range, - }) => Self::For { + }) => Self::For(StmtFor { target: target.into(), iter: iter.into(), body: body.iter().map(Into::into).collect(), orelse: orelse.iter().map(Into::into).collect(), type_comment: type_comment.as_ref().map(String::as_str), - }, - Stmt::AsyncFor(ast::StmtAsyncFor { + }), + ast::Stmt::AsyncFor(ast::StmtAsyncFor { target, iter, body, orelse, type_comment, range: _range, - }) => Self::AsyncFor { + }) => Self::AsyncFor(StmtAsyncFor { target: target.into(), iter: iter.into(), body: body.iter().map(Into::into).collect(), orelse: orelse.iter().map(Into::into).collect(), type_comment: type_comment.as_ref().map(String::as_str), - }, - Stmt::While(ast::StmtWhile { + }), + ast::Stmt::While(ast::StmtWhile { test, body, orelse, range: _range, - }) => Self::While { + }) => Self::While(StmtWhile { test: test.into(), body: body.iter().map(Into::into).collect(), orelse: orelse.iter().map(Into::into).collect(), - }, - Stmt::If(ast::StmtIf { + }), + ast::Stmt::If(ast::StmtIf { test, body, orelse, range: _range, - }) => Self::If { + }) => Self::If(StmtIf { test: test.into(), body: body.iter().map(Into::into).collect(), orelse: orelse.iter().map(Into::into).collect(), - }, - Stmt::With(ast::StmtWith { + }), + ast::Stmt::With(ast::StmtWith { items, body, type_comment, range: _range, - }) => Self::With { + }) => Self::With(StmtWith { items: items.iter().map(Into::into).collect(), body: body.iter().map(Into::into).collect(), type_comment: type_comment.as_ref().map(String::as_str), - }, - Stmt::AsyncWith(ast::StmtAsyncWith { + }), + ast::Stmt::AsyncWith(ast::StmtAsyncWith { items, body, type_comment, range: _range, - }) => Self::AsyncWith { + }) => Self::AsyncWith(StmtAsyncWith { items: items.iter().map(Into::into).collect(), body: body.iter().map(Into::into).collect(), type_comment: type_comment.as_ref().map(String::as_str), - }, - Stmt::Match(ast::StmtMatch { + }), + ast::Stmt::Match(ast::StmtMatch { subject, cases, range: _range, - }) => Self::Match { + }) => Self::Match(StmtMatch { subject: subject.into(), cases: cases.iter().map(Into::into).collect(), - }, - Stmt::Raise(ast::StmtRaise { + }), + ast::Stmt::Raise(ast::StmtRaise { exc, cause, range: _range, - }) => Self::Raise { + }) => Self::Raise(StmtRaise { exc: exc.as_ref().map(Into::into), cause: cause.as_ref().map(Into::into), - }, - Stmt::Try(ast::StmtTry { + }), + ast::Stmt::Try(ast::StmtTry { body, handlers, orelse, finalbody, range: _range, - }) => Self::Try { + }) => Self::Try(StmtTry { body: body.iter().map(Into::into).collect(), handlers: handlers.iter().map(Into::into).collect(), orelse: orelse.iter().map(Into::into).collect(), finalbody: finalbody.iter().map(Into::into).collect(), - }, - Stmt::TryStar(ast::StmtTryStar { + }), + ast::Stmt::TryStar(ast::StmtTryStar { body, handlers, orelse, finalbody, range: _range, - }) => Self::TryStar { + }) => Self::TryStar(StmtTryStar { body: body.iter().map(Into::into).collect(), handlers: handlers.iter().map(Into::into).collect(), orelse: orelse.iter().map(Into::into).collect(), finalbody: finalbody.iter().map(Into::into).collect(), - }, - Stmt::Assert(ast::StmtAssert { + }), + ast::Stmt::Assert(ast::StmtAssert { test, msg, range: _range, - }) => Self::Assert { + }) => Self::Assert(StmtAssert { test: test.into(), msg: msg.as_ref().map(Into::into), - }, - Stmt::Import(ast::StmtImport { + }), + ast::Stmt::Import(ast::StmtImport { names, range: _range, - }) => Self::Import { + }) => Self::Import(StmtImport { names: names.iter().map(Into::into).collect(), - }, - Stmt::ImportFrom(ast::StmtImportFrom { + }), + ast::Stmt::ImportFrom(ast::StmtImportFrom { module, names, level, range: _range, - }) => Self::ImportFrom { + }) => Self::ImportFrom(StmtImportFrom { module: module.as_deref(), names: names.iter().map(Into::into).collect(), level: *level, - }, - Stmt::Global(ast::StmtGlobal { + }), + ast::Stmt::Global(ast::StmtGlobal { names, range: _range, - }) => Self::Global { - names: names.iter().map(Identifier::as_str).collect(), - }, - Stmt::Nonlocal(ast::StmtNonlocal { + }) => Self::Global(StmtGlobal { + names: names.iter().map(ast::Identifier::as_str).collect(), + }), + ast::Stmt::Nonlocal(ast::StmtNonlocal { names, range: _range, - }) => Self::Nonlocal { - names: names.iter().map(Identifier::as_str).collect(), - }, - Stmt::Expr(ast::StmtExpr { + }) => Self::Nonlocal(StmtNonlocal { + names: names.iter().map(ast::Identifier::as_str).collect(), + }), + ast::Stmt::Expr(ast::StmtExpr { value, range: _range, - }) => Self::Expr { + }) => Self::Expr(StmtExpr { value: value.into(), - }, - Stmt::Pass(_) => Self::Pass, - Stmt::Break(_) => Self::Break, - Stmt::Continue(_) => Self::Continue, + }), + ast::Stmt::Pass(_) => Self::Pass, + ast::Stmt::Break(_) => Self::Break, + ast::Stmt::Continue(_) => Self::Continue, } } } diff --git a/crates/ruff_python_ast/src/helpers.rs b/crates/ruff_python_ast/src/helpers.rs index 0f9ee007db..596783db47 100644 --- a/crates/ruff_python_ast/src/helpers.rs +++ b/crates/ruff_python_ast/src/helpers.rs @@ -2,19 +2,17 @@ use std::borrow::Cow; use std::ops::Sub; use std::path::Path; -use itertools::Itertools; -use log::error; use num_traits::Zero; use ruff_text_size::{TextRange, TextSize}; -use rustc_hash::{FxHashMap, FxHashSet}; +use rustc_hash::FxHashMap; +use rustpython_ast::CmpOp; use rustpython_parser::ast::{ - self, Arguments, Cmpop, Constant, Excepthandler, Expr, Keyword, MatchCase, Pattern, Ranged, - Stmt, + self, Arguments, Constant, ExceptHandler, Expr, Keyword, MatchCase, Pattern, Ranged, Stmt, }; use rustpython_parser::{lexer, Mode, Tok}; use smallvec::SmallVec; -use ruff_python_whitespace::{PythonWhitespace, UniversalNewlineIterator}; +use ruff_python_whitespace::{is_python_whitespace, PythonWhitespace, UniversalNewlineIterator}; use crate::call_path::CallPath; use crate::source_code::{Indexer, Locator}; @@ -44,6 +42,7 @@ where range: _range, }) = expr { + // Ex) `list()` if args.is_empty() && keywords.is_empty() { if let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() { if !is_iterable_initializer(id.as_str(), |id| is_builtin(id)) { @@ -334,25 +333,19 @@ where returns, .. }) => { - args.defaults.iter().any(|expr| any_over_expr(expr, func)) - || args - .kw_defaults - .iter() - .any(|expr| any_over_expr(expr, func)) - || args.args.iter().any(|arg| { - arg.annotation - .as_ref() - .map_or(false, |expr| any_over_expr(expr, func)) - }) - || args.kwonlyargs.iter().any(|arg| { - arg.annotation - .as_ref() - .map_or(false, |expr| any_over_expr(expr, func)) - }) - || args.posonlyargs.iter().any(|arg| { - arg.annotation + args.posonlyargs + .iter() + .chain(args.args.iter().chain(args.kwonlyargs.iter())) + .any(|arg_with_default| { + arg_with_default + .default .as_ref() .map_or(false, |expr| any_over_expr(expr, func)) + || arg_with_default + .def + .annotation + .as_ref() + .map_or(false, |expr| any_over_expr(expr, func)) }) || args.vararg.as_ref().map_or(false, |arg| { arg.annotation @@ -449,9 +442,9 @@ where }) => any_over_expr(test, func) || any_over_body(body, func) || any_over_body(orelse, func), Stmt::With(ast::StmtWith { items, body, .. }) | Stmt::AsyncWith(ast::StmtAsyncWith { items, body, .. }) => { - items.iter().any(|withitem| { - any_over_expr(&withitem.context_expr, func) - || withitem + items.iter().any(|with_item| { + any_over_expr(&with_item.context_expr, func) + || with_item .optional_vars .as_ref() .map_or(false, |expr| any_over_expr(expr, func)) @@ -484,7 +477,7 @@ where }) => { any_over_body(body, func) || handlers.iter().any(|handler| { - let Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { + let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { type_, body, .. @@ -546,7 +539,7 @@ where body.iter().any(|stmt| any_over_stmt(stmt, func)) } -fn is_dunder(id: &str) -> bool { +pub fn is_dunder(id: &str) -> bool { id.starts_with("__") && id.ends_with("__") } @@ -635,6 +628,18 @@ pub const fn is_const_true(expr: &Expr) -> bool { ) } +/// Return `true` if an [`Expr`] is `False`. +pub const fn is_const_false(expr: &Expr) -> bool { + matches!( + expr, + Expr::Constant(ast::ExprConstant { + value: Constant::Bool(false), + kind: None, + .. + }), + ) +} + /// Return `true` if a keyword argument is present with a non-`None` value. pub fn has_non_none_keyword(keywords: &[Keyword], keyword: &str) -> bool { find_keyword(keywords, keyword).map_or(false, |keyword| { @@ -644,11 +649,11 @@ pub fn has_non_none_keyword(keywords: &[Keyword], keyword: &str) -> bool { } /// Extract the names of all handled exceptions. -pub fn extract_handled_exceptions(handlers: &[Excepthandler]) -> Vec<&Expr> { +pub fn extract_handled_exceptions(handlers: &[ExceptHandler]) -> Vec<&Expr> { let mut handled_exceptions = Vec::new(); for handler in handlers { match handler { - Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { type_, .. }) => { + ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { type_, .. }) => { if let Some(type_) = type_ { if let Expr::Tuple(ast::ExprTuple { elts, .. }) = &type_.as_ref() { for type_ in elts { @@ -664,25 +669,28 @@ pub fn extract_handled_exceptions(handlers: &[Excepthandler]) -> Vec<&Expr> { handled_exceptions } -/// Return the set of all bound argument names. -pub fn collect_arg_names<'a>(arguments: &'a Arguments) -> FxHashSet<&'a str> { - let mut arg_names: FxHashSet<&'a str> = FxHashSet::default(); - for arg in &arguments.posonlyargs { - arg_names.insert(arg.arg.as_str()); - } - for arg in &arguments.args { - arg_names.insert(arg.arg.as_str()); +/// Returns `true` if the given name is included in the given [`Arguments`]. +pub fn includes_arg_name(name: &str, arguments: &Arguments) -> bool { + if arguments + .posonlyargs + .iter() + .chain(&arguments.args) + .chain(&arguments.kwonlyargs) + .any(|arg| arg.def.arg.as_str() == name) + { + return true; } if let Some(arg) = &arguments.vararg { - arg_names.insert(arg.arg.as_str()); - } - for arg in &arguments.kwonlyargs { - arg_names.insert(arg.arg.as_str()); + if arg.arg.as_str() == name { + return true; + } } if let Some(arg) = &arguments.kwarg { - arg_names.insert(arg.arg.as_str()); + if arg.arg.as_str() == name { + return true; + } } - arg_names + false } /// Given an [`Expr`] that can be callable or not (like a decorator, which could @@ -712,19 +720,19 @@ pub fn map_subscript(expr: &Expr) -> &Expr { } /// Returns `true` if a statement or expression includes at least one comment. -pub fn has_comments(located: &T, locator: &Locator) -> bool +pub fn has_comments(node: &T, locator: &Locator) -> bool where T: Ranged, { - let start = if has_leading_content(located, locator) { - located.start() + let start = if has_leading_content(node.start(), locator) { + node.start() } else { - locator.line_start(located.start()) + locator.line_start(node.start()) }; - let end = if has_trailing_content(located, locator) { - located.end() + let end = if has_trailing_content(node.end(), locator) { + node.end() } else { - locator.line_end(located.end()) + locator.line_end(node.end()) }; has_comments_in(TextRange::new(start, end), locator) @@ -922,7 +930,7 @@ where { fn visit_stmt(&mut self, stmt: &'b Stmt) { match stmt { - Stmt::FunctionDef(_) | Stmt::AsyncFunctionDef(_) => { + Stmt::FunctionDef(_) | Stmt::AsyncFunctionDef(_) | Stmt::ClassDef(_) => { // Don't recurse. } Stmt::Return(stmt) => self.returns.push(stmt), @@ -977,29 +985,23 @@ where } } -/// Return `true` if a [`Ranged`] has leading content. -pub fn has_leading_content(located: &T, locator: &Locator) -> bool -where - T: Ranged, -{ - let line_start = locator.line_start(located.start()); - let leading = &locator.contents()[TextRange::new(line_start, located.start())]; - leading.chars().any(|char| !char.is_whitespace()) +/// Return `true` if the node starting the given [`TextSize`] has leading content. +pub fn has_leading_content(offset: TextSize, locator: &Locator) -> bool { + let line_start = locator.line_start(offset); + let leading = &locator.contents()[TextRange::new(line_start, offset)]; + leading.chars().any(|char| !is_python_whitespace(char)) } -/// Return `true` if a [`Ranged`] has trailing content. -pub fn has_trailing_content(located: &T, locator: &Locator) -> bool -where - T: Ranged, -{ - let line_end = locator.line_end(located.end()); - let trailing = &locator.contents()[TextRange::new(located.end(), line_end)]; +/// Return `true` if the node ending at the given [`TextSize`] has trailing content. +pub fn has_trailing_content(offset: TextSize, locator: &Locator) -> bool { + let line_end = locator.line_end(offset); + let trailing = &locator.contents()[TextRange::new(offset, line_end)]; for char in trailing.chars() { if char == '#' { return false; } - if !char.is_whitespace() { + if !is_python_whitespace(char) { return true; } } @@ -1015,11 +1017,11 @@ where let trailing = &locator.contents()[TextRange::new(located.end(), line_end)]; - for (i, char) in trailing.chars().enumerate() { + for (index, char) in trailing.char_indices() { if char == '#' { - return TextSize::try_from(i).ok(); + return TextSize::try_from(index).ok(); } - if !char.is_whitespace() { + if !is_python_whitespace(char) { return None; } } @@ -1035,7 +1037,7 @@ pub fn trailing_lines_end(stmt: &Stmt, locator: &Locator) -> TextSize { UniversalNewlineIterator::with_offset(rest, line_end) .take_while(|line| line.trim_whitespace().is_empty()) .last() - .map_or(line_end, |l| l.full_end()) + .map_or(line_end, |line| line.full_end()) } /// Return the range of the first parenthesis pair after a given [`TextSize`]. @@ -1071,134 +1073,12 @@ pub fn match_parens(start: TextSize, locator: &Locator) -> Option { } } -/// Return the appropriate visual `Range` for any message that spans a `Stmt`. -/// Specifically, this method returns the range of a function or class name, -/// rather than that of the entire function or class body. -pub fn identifier_range(stmt: &Stmt, locator: &Locator) -> TextRange { - match stmt { - Stmt::ClassDef(ast::StmtClassDef { - decorator_list, - range, - .. - }) - | Stmt::FunctionDef(ast::StmtFunctionDef { - decorator_list, - range, - .. - }) - | Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { - decorator_list, - range, - .. - }) => { - let header_range = decorator_list.last().map_or(*range, |last_decorator| { - TextRange::new(last_decorator.end(), range.end()) - }); - - let contents = locator.slice(header_range); - - let mut tokens = - lexer::lex_starts_at(contents, Mode::Module, header_range.start()).flatten(); - tokens - .find_map(|(t, range)| t.is_name().then_some(range)) - .unwrap_or_else(|| { - error!("Failed to find identifier for {:?}", stmt); - - header_range - }) - } - _ => stmt.range(), - } -} - -/// Return the ranges of [`Tok::Name`] tokens within a specified node. -pub fn find_names<'a, T>( - located: &'a T, - locator: &'a Locator, -) -> impl Iterator + 'a -where - T: Ranged, -{ - let contents = locator.slice(located.range()); - - lexer::lex_starts_at(contents, Mode::Module, located.start()) - .flatten() - .filter(|(tok, _)| matches!(tok, Tok::Name { .. })) - .map(|(_, range)| range) -} - -/// Return the `Range` of `name` in `Excepthandler`. -pub fn excepthandler_name_range(handler: &Excepthandler, locator: &Locator) -> Option { - let Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { - name, - type_, - body, - range: _range, - }) = handler; - - match (name, type_) { - (Some(_), Some(type_)) => { - let contents = &locator.contents()[TextRange::new(type_.end(), body[0].start())]; - - lexer::lex_starts_at(contents, Mode::Module, type_.end()) - .flatten() - .tuple_windows() - .find(|(tok, next_tok)| { - matches!(tok.0, Tok::As) && matches!(next_tok.0, Tok::Name { .. }) - }) - .map(|((..), (_, range))| range) - } - _ => None, - } -} - -/// Return the `Range` of `except` in `Excepthandler`. -pub fn except_range(handler: &Excepthandler, locator: &Locator) -> TextRange { - let Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { body, type_, .. }) = handler; - let end = if let Some(type_) = type_ { - type_.end() - } else { - body.first().expect("Expected body to be non-empty").start() - }; - let contents = &locator.contents()[TextRange::new(handler.start(), end)]; - - lexer::lex_starts_at(contents, Mode::Module, handler.start()) - .flatten() - .find(|(kind, _)| matches!(kind, Tok::Except { .. })) - .map(|(_, range)| range) - .expect("Failed to find `except` range") -} - -/// Return the `Range` of `else` in `For`, `AsyncFor`, and `While` statements. -pub fn else_range(stmt: &Stmt, locator: &Locator) -> Option { - match stmt { - Stmt::For(ast::StmtFor { body, orelse, .. }) - | Stmt::AsyncFor(ast::StmtAsyncFor { body, orelse, .. }) - | Stmt::While(ast::StmtWhile { body, orelse, .. }) - if !orelse.is_empty() => - { - let body_end = body.last().expect("Expected body to be non-empty").end(); - let or_else_start = orelse - .first() - .expect("Expected orelse to be non-empty") - .start(); - let contents = &locator.contents()[TextRange::new(body_end, or_else_start)]; - - lexer::lex_starts_at(contents, Mode::Module, body_end) - .flatten() - .find(|(kind, _)| matches!(kind, Tok::Else)) - .map(|(_, range)| range) - } - _ => None, - } -} - /// Return the `Range` of the first `Tok::Colon` token in a `Range`. pub fn first_colon_range(range: TextRange, locator: &Locator) -> Option { let contents = &locator.contents()[range]; let range = lexer::lex_starts_at(contents, Mode::Module, range.start()) .flatten() - .find(|(kind, _)| matches!(kind, Tok::Colon)) + .find(|(tok, _)| tok.is_colon()) .map(|(_, range)| range); range } @@ -1222,13 +1102,12 @@ pub fn elif_else_range(stmt: &ast::StmtIf, locator: &Locator) -> Option bool { - let previous_line_end = locator.line_start(stmt.start()); - let newline_pos = usize::from(previous_line_end).saturating_sub(1); +/// Given an offset at the end of a line (including newlines), return the offset of the +/// continuation at the end of that line. +fn find_continuation(offset: TextSize, locator: &Locator, indexer: &Indexer) -> Option { + let newline_pos = usize::from(offset).saturating_sub(1); - // Compute start of preceding line + // Skip the newline. let newline_len = match locator.contents().as_bytes()[newline_pos] { b'\n' => { if locator @@ -1243,24 +1122,77 @@ pub fn preceded_by_continuation(stmt: &Stmt, indexer: &Indexer, locator: &Locato } } b'\r' => 1, - // No preceding line - _ => return false, + // No preceding line. + _ => return None, }; - // See if the position is in the continuation line starts - indexer.is_continuation(previous_line_end - TextSize::from(newline_len), locator) + indexer + .is_continuation(offset - TextSize::from(newline_len), locator) + .then(|| offset - TextSize::from(newline_len) - TextSize::from(1)) +} + +/// If the node starting at the given [`TextSize`] is preceded by at least one continuation line +/// (i.e., a line ending in a backslash), return the starting offset of the first such continuation +/// character. +/// +/// For example, given: +/// ```python +/// x = 1; \ +/// y = 2 +/// ``` +/// +/// When passed the offset of `y`, this function will return the offset of the backslash at the end +/// of the first line. +/// +/// Similarly, given: +/// ```python +/// x = 1; \ +/// \ +/// y = 2; +/// ``` +/// +/// When passed the offset of `y`, this function will again return the offset of the backslash at +/// the end of the first line. +pub fn preceded_by_continuations( + offset: TextSize, + locator: &Locator, + indexer: &Indexer, +) -> Option { + // Find the first preceding continuation. + let mut continuation = find_continuation(locator.line_start(offset), locator, indexer)?; + + // Continue searching for continuations, in the unlikely event that we have multiple + // continuations in a row. + loop { + let previous_line_end = locator.line_start(continuation); + if locator + .slice(TextRange::new(previous_line_end, continuation)) + .chars() + .all(is_python_whitespace) + { + if let Some(next_continuation) = find_continuation(previous_line_end, locator, indexer) + { + continuation = next_continuation; + continue; + } + } + break; + } + + Some(continuation) } /// Return `true` if a `Stmt` appears to be part of a multi-statement line, with /// other statements preceding it. pub fn preceded_by_multi_statement_line(stmt: &Stmt, locator: &Locator, indexer: &Indexer) -> bool { - has_leading_content(stmt, locator) || preceded_by_continuation(stmt, indexer, locator) + has_leading_content(stmt.start(), locator) + || preceded_by_continuations(stmt.start(), locator, indexer).is_some() } /// Return `true` if a `Stmt` appears to be part of a multi-statement line, with /// other statements following it. pub fn followed_by_multi_statement_line(stmt: &Stmt, locator: &Locator) -> bool { - has_trailing_content(stmt, locator) + has_trailing_content(stmt.end(), locator) } /// Return `true` if a `Stmt` is a docstring. @@ -1282,11 +1214,11 @@ pub fn is_docstring_stmt(stmt: &Stmt) -> bool { } } -#[derive(Default)] /// A simple representation of a call's positional and keyword arguments. +#[derive(Default)] pub struct SimpleCallArgs<'a> { - pub args: Vec<&'a Expr>, - pub kwargs: FxHashMap<&'a str, &'a Expr>, + args: Vec<&'a Expr>, + kwargs: FxHashMap<&'a str, &'a Expr>, } impl<'a> SimpleCallArgs<'a> { @@ -1296,7 +1228,7 @@ impl<'a> SimpleCallArgs<'a> { ) -> Self { let args = args .into_iter() - .take_while(|arg| !matches!(arg, Expr::Starred(_))) + .take_while(|arg| !arg.is_starred_expr()) .collect(); let kwargs = keywords @@ -1330,13 +1262,23 @@ impl<'a> SimpleCallArgs<'a> { self.args.len() + self.kwargs.len() } + /// Return the number of positional arguments. + pub fn num_args(&self) -> usize { + self.args.len() + } + + /// Return the number of keyword arguments. + pub fn num_kwargs(&self) -> usize { + self.kwargs.len() + } + /// Return `true` if there are no positional or keyword arguments. pub fn is_empty(&self) -> bool { self.len() == 0 } } -/// Check if a node is parent of a conditional branch. +/// Check if a node is part of a conditional branch. pub fn on_conditional_branch<'a>(parents: &mut impl Iterator) -> bool { parents.any(|parent| { if matches!(parent, Stmt::If(_) | Stmt::While(_) | Stmt::Match(_)) { @@ -1347,7 +1289,7 @@ pub fn on_conditional_branch<'a>(parents: &mut impl Iterator) - range: _range, }) = parent { - if matches!(value.as_ref(), Expr::IfExp(_)) { + if value.is_if_exp_expr() { return true; } } @@ -1370,7 +1312,7 @@ pub fn is_unpacking_assignment(parent: &Stmt, child: &Expr) -> bool { match parent { Stmt::With(ast::StmtWith { items, .. }) => items.iter().any(|item| { if let Some(optional_vars) = &item.optional_vars { - if matches!(optional_vars.as_ref(), Expr::Tuple(_)) { + if optional_vars.is_tuple_expr() { if any_over_expr(optional_vars, &|expr| expr == child) { return true; } @@ -1425,105 +1367,6 @@ pub fn is_unpacking_assignment(parent: &Stmt, child: &Expr) -> bool { } } -#[derive(Clone, PartialEq, Debug)] -pub struct LocatedCmpop { - pub range: TextRange, - pub op: Cmpop, -} - -impl LocatedCmpop { - fn new>(range: T, op: Cmpop) -> Self { - Self { - range: range.into(), - op, - } - } -} - -/// Extract all [`Cmpop`] operators from an expression snippet, with appropriate -/// ranges. -/// -/// `RustPython` doesn't include line and column information on [`Cmpop`] nodes. -/// `CPython` doesn't either. This method iterates over the token stream and -/// re-identifies [`Cmpop`] nodes, annotating them with valid ranges. -pub fn locate_cmpops(expr: &Expr, locator: &Locator) -> Vec { - // If `Expr` is a multi-line expression, we need to parenthesize it to - // ensure that it's lexed correctly. - let contents = locator.slice(expr.range()); - let parenthesized_contents = format!("({contents})"); - let mut tok_iter = lexer::lex(&parenthesized_contents, Mode::Expression) - .flatten() - .skip(1) - .map(|(tok, range)| (tok, range.sub(TextSize::from(1)))) - .filter(|(tok, _)| !matches!(tok, Tok::NonLogicalNewline | Tok::Comment(_))) - .peekable(); - - let mut ops: Vec = vec![]; - let mut count = 0u32; - loop { - let Some((tok, range)) = tok_iter.next() else { - break; - }; - if matches!(tok, Tok::Lpar) { - count = count.saturating_add(1); - continue; - } else if matches!(tok, Tok::Rpar) { - count = count.saturating_sub(1); - continue; - } - if count == 0 { - match tok { - Tok::Not => { - if let Some((_, next_range)) = - tok_iter.next_if(|(tok, _)| matches!(tok, Tok::In)) - { - ops.push(LocatedCmpop::new( - TextRange::new(range.start(), next_range.end()), - Cmpop::NotIn, - )); - } - } - Tok::In => { - ops.push(LocatedCmpop::new(range, Cmpop::In)); - } - Tok::Is => { - let op = if let Some((_, next_range)) = - tok_iter.next_if(|(tok, _)| matches!(tok, Tok::Not)) - { - LocatedCmpop::new( - TextRange::new(range.start(), next_range.end()), - Cmpop::IsNot, - ) - } else { - LocatedCmpop::new(range, Cmpop::Is) - }; - ops.push(op); - } - Tok::NotEqual => { - ops.push(LocatedCmpop::new(range, Cmpop::NotEq)); - } - Tok::EqEqual => { - ops.push(LocatedCmpop::new(range, Cmpop::Eq)); - } - Tok::GreaterEqual => { - ops.push(LocatedCmpop::new(range, Cmpop::GtE)); - } - Tok::Greater => { - ops.push(LocatedCmpop::new(range, Cmpop::Gt)); - } - Tok::LessEqual => { - ops.push(LocatedCmpop::new(range, Cmpop::LtE)); - } - Tok::Less => { - ops.push(LocatedCmpop::new(range, Cmpop::Lt)); - } - _ => {} - } - } - } - ops -} - #[derive(Copy, Clone, Debug, PartialEq, is_macro::Is)] pub enum Truthiness { // An expression evaluates to `False`. @@ -1571,11 +1414,18 @@ impl Truthiness { Constant::Ellipsis => Some(true), Constant::Tuple(elts) => Some(!elts.is_empty()), }, - Expr::JoinedStr(ast::ExprJoinedStr { values, range: _range }) => { + Expr::JoinedStr(ast::ExprJoinedStr { + values, + range: _range, + }) => { if values.is_empty() { Some(false) } else if values.iter().any(|value| { - let Expr::Constant(ast::ExprConstant { value: Constant::Str(string), .. } )= &value else { + let Expr::Constant(ast::ExprConstant { + value: Constant::Str(string), + .. + }) = &value + else { return false; }; !string.is_empty() @@ -1585,20 +1435,38 @@ impl Truthiness { None } } - Expr::List(ast::ExprList { elts, range: _range, .. }) - | Expr::Set(ast::ExprSet { elts, range: _range }) - | Expr::Tuple(ast::ExprTuple { elts, range: _range,.. }) => Some(!elts.is_empty()), - Expr::Dict(ast::ExprDict { keys, range: _range, .. }) => Some(!keys.is_empty()), + Expr::List(ast::ExprList { + elts, + range: _range, + .. + }) + | Expr::Set(ast::ExprSet { + elts, + range: _range, + }) + | Expr::Tuple(ast::ExprTuple { + elts, + range: _range, + .. + }) => Some(!elts.is_empty()), + Expr::Dict(ast::ExprDict { + keys, + range: _range, + .. + }) => Some(!keys.is_empty()), Expr::Call(ast::ExprCall { func, args, - keywords, range: _range, + keywords, + range: _range, }) => { if let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() { if is_iterable_initializer(id.as_str(), |id| is_builtin(id)) { if args.is_empty() && keywords.is_empty() { + // Ex) `list()` Some(false) } else if args.len() == 1 && keywords.is_empty() { + // Ex) `list([1, 2, 3])` Self::from_expr(&args[0], is_builtin).into() } else { None @@ -1616,19 +1484,118 @@ impl Truthiness { } } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LocatedCmpOp { + pub range: TextRange, + pub op: CmpOp, +} + +impl LocatedCmpOp { + fn new>(range: T, op: CmpOp) -> Self { + Self { + range: range.into(), + op, + } + } +} + +/// Extract all [`CmpOp`] operators from an expression snippet, with appropriate +/// ranges. +/// +/// `RustPython` doesn't include line and column information on [`CmpOp`] nodes. +/// `CPython` doesn't either. This method iterates over the token stream and +/// re-identifies [`CmpOp`] nodes, annotating them with valid ranges. +pub fn locate_cmp_ops(expr: &Expr, locator: &Locator) -> Vec { + // If `Expr` is a multi-line expression, we need to parenthesize it to + // ensure that it's lexed correctly. + let contents = locator.slice(expr.range()); + let parenthesized_contents = format!("({contents})"); + let mut tok_iter = lexer::lex(&parenthesized_contents, Mode::Expression) + .flatten() + .skip(1) + .map(|(tok, range)| (tok, range.sub(TextSize::from(1)))) + .filter(|(tok, _)| !matches!(tok, Tok::NonLogicalNewline | Tok::Comment(_))) + .peekable(); + + let mut ops: Vec = vec![]; + let mut count = 0u32; + loop { + let Some((tok, range)) = tok_iter.next() else { + break; + }; + if matches!(tok, Tok::Lpar) { + count = count.saturating_add(1); + continue; + } else if matches!(tok, Tok::Rpar) { + count = count.saturating_sub(1); + continue; + } + if count == 0 { + match tok { + Tok::Not => { + if let Some((_, next_range)) = + tok_iter.next_if(|(tok, _)| matches!(tok, Tok::In)) + { + ops.push(LocatedCmpOp::new( + TextRange::new(range.start(), next_range.end()), + CmpOp::NotIn, + )); + } + } + Tok::In => { + ops.push(LocatedCmpOp::new(range, CmpOp::In)); + } + Tok::Is => { + let op = if let Some((_, next_range)) = + tok_iter.next_if(|(tok, _)| matches!(tok, Tok::Not)) + { + LocatedCmpOp::new( + TextRange::new(range.start(), next_range.end()), + CmpOp::IsNot, + ) + } else { + LocatedCmpOp::new(range, CmpOp::Is) + }; + ops.push(op); + } + Tok::NotEqual => { + ops.push(LocatedCmpOp::new(range, CmpOp::NotEq)); + } + Tok::EqEqual => { + ops.push(LocatedCmpOp::new(range, CmpOp::Eq)); + } + Tok::GreaterEqual => { + ops.push(LocatedCmpOp::new(range, CmpOp::GtE)); + } + Tok::Greater => { + ops.push(LocatedCmpOp::new(range, CmpOp::Gt)); + } + Tok::LessEqual => { + ops.push(LocatedCmpOp::new(range, CmpOp::LtE)); + } + Tok::Less => { + ops.push(LocatedCmpOp::new(range, CmpOp::Lt)); + } + _ => {} + } + } + } + ops +} + #[cfg(test)] mod tests { use std::borrow::Cow; use anyhow::Result; use ruff_text_size::{TextLen, TextRange, TextSize}; - use rustpython_ast::{Expr, Stmt, Suite}; - use rustpython_parser::ast::Cmpop; + use rustpython_ast::{CmpOp, Expr, Ranged, Stmt}; + use rustpython_parser::ast::Suite; use rustpython_parser::Parse; use crate::helpers::{ - elif_else_range, else_range, first_colon_range, has_trailing_content, identifier_range, - locate_cmpops, resolve_imported_module_path, LocatedCmpop, + elif_else_range, first_colon_range, has_trailing_content, locate_cmp_ops, + resolve_imported_module_path, LocatedCmpOp, }; use crate::source_code::Locator; @@ -1638,25 +1605,25 @@ mod tests { let program = Suite::parse(contents, "")?; let stmt = program.first().unwrap(); let locator = Locator::new(contents); - assert!(!has_trailing_content(stmt, &locator)); + assert!(!has_trailing_content(stmt.end(), &locator)); let contents = "x = 1; y = 2"; let program = Suite::parse(contents, "")?; let stmt = program.first().unwrap(); let locator = Locator::new(contents); - assert!(has_trailing_content(stmt, &locator)); + assert!(has_trailing_content(stmt.end(), &locator)); let contents = "x = 1 "; let program = Suite::parse(contents, "")?; let stmt = program.first().unwrap(); let locator = Locator::new(contents); - assert!(!has_trailing_content(stmt, &locator)); + assert!(!has_trailing_content(stmt.end(), &locator)); let contents = "x = 1 # Comment"; let program = Suite::parse(contents, "")?; let stmt = program.first().unwrap(); let locator = Locator::new(contents); - assert!(!has_trailing_content(stmt, &locator)); + assert!(!has_trailing_content(stmt.end(), &locator)); let contents = r#" x = 1 @@ -1666,70 +1633,7 @@ y = 2 let program = Suite::parse(contents, "")?; let stmt = program.first().unwrap(); let locator = Locator::new(contents); - assert!(!has_trailing_content(stmt, &locator)); - - Ok(()) - } - - #[test] - fn extract_identifier_range() -> Result<()> { - let contents = "def f(): pass".trim(); - let stmt = Stmt::parse(contents, "")?; - let locator = Locator::new(contents); - assert_eq!( - identifier_range(&stmt, &locator), - TextRange::new(TextSize::from(4), TextSize::from(5)) - ); - - let contents = r#" -def \ - f(): - pass -"# - .trim(); - let stmt = Stmt::parse(contents, "")?; - let locator = Locator::new(contents); - assert_eq!( - identifier_range(&stmt, &locator), - TextRange::new(TextSize::from(8), TextSize::from(9)) - ); - - let contents = "class Class(): pass".trim(); - let stmt = Stmt::parse(contents, "")?; - let locator = Locator::new(contents); - assert_eq!( - identifier_range(&stmt, &locator), - TextRange::new(TextSize::from(6), TextSize::from(11)) - ); - - let contents = "class Class: pass".trim(); - let stmt = Stmt::parse(contents, "")?; - let locator = Locator::new(contents); - assert_eq!( - identifier_range(&stmt, &locator), - TextRange::new(TextSize::from(6), TextSize::from(11)) - ); - - let contents = r#" -@decorator() -class Class(): - pass -"# - .trim(); - let stmt = Stmt::parse(contents, "")?; - let locator = Locator::new(contents); - assert_eq!( - identifier_range(&stmt, &locator), - TextRange::new(TextSize::from(19), TextSize::from(24)) - ); - - let contents = r#"x = y + 1"#.trim(); - let stmt = Stmt::parse(contents, "")?; - let locator = Locator::new(contents); - assert_eq!( - identifier_range(&stmt, &locator), - TextRange::new(TextSize::from(0), TextSize::from(9)) - ); + assert!(!has_trailing_content(stmt.end(), &locator)); Ok(()) } @@ -1771,26 +1675,6 @@ class Class(): ); } - #[test] - fn extract_else_range() -> Result<()> { - let contents = r#" -for x in y: - pass -else: - pass -"# - .trim(); - let stmt = Stmt::parse(contents, "")?; - let locator = Locator::new(contents); - let range = else_range(&stmt, &locator).unwrap(); - assert_eq!(&contents[range], "else"); - assert_eq!( - range, - TextRange::new(TextSize::from(21), TextSize::from(25)) - ); - Ok(()) - } - #[test] fn extract_first_colon_range() { let contents = "with a: pass"; @@ -1834,15 +1718,15 @@ else: } #[test] - fn extract_cmpop_location() -> Result<()> { + fn extract_cmp_op_location() -> Result<()> { let contents = "x == 1"; let expr = Expr::parse(contents, "")?; let locator = Locator::new(contents); assert_eq!( - locate_cmpops(&expr, &locator), - vec![LocatedCmpop::new( + locate_cmp_ops(&expr, &locator), + vec![LocatedCmpOp::new( TextSize::from(2)..TextSize::from(4), - Cmpop::Eq + CmpOp::Eq )] ); @@ -1850,10 +1734,10 @@ else: let expr = Expr::parse(contents, "")?; let locator = Locator::new(contents); assert_eq!( - locate_cmpops(&expr, &locator), - vec![LocatedCmpop::new( + locate_cmp_ops(&expr, &locator), + vec![LocatedCmpOp::new( TextSize::from(2)..TextSize::from(4), - Cmpop::NotEq + CmpOp::NotEq )] ); @@ -1861,10 +1745,10 @@ else: let expr = Expr::parse(contents, "")?; let locator = Locator::new(contents); assert_eq!( - locate_cmpops(&expr, &locator), - vec![LocatedCmpop::new( + locate_cmp_ops(&expr, &locator), + vec![LocatedCmpOp::new( TextSize::from(2)..TextSize::from(4), - Cmpop::Is + CmpOp::Is )] ); @@ -1872,10 +1756,10 @@ else: let expr = Expr::parse(contents, "")?; let locator = Locator::new(contents); assert_eq!( - locate_cmpops(&expr, &locator), - vec![LocatedCmpop::new( + locate_cmp_ops(&expr, &locator), + vec![LocatedCmpOp::new( TextSize::from(2)..TextSize::from(8), - Cmpop::IsNot + CmpOp::IsNot )] ); @@ -1883,10 +1767,10 @@ else: let expr = Expr::parse(contents, "")?; let locator = Locator::new(contents); assert_eq!( - locate_cmpops(&expr, &locator), - vec![LocatedCmpop::new( + locate_cmp_ops(&expr, &locator), + vec![LocatedCmpOp::new( TextSize::from(2)..TextSize::from(4), - Cmpop::In + CmpOp::In )] ); @@ -1894,10 +1778,10 @@ else: let expr = Expr::parse(contents, "")?; let locator = Locator::new(contents); assert_eq!( - locate_cmpops(&expr, &locator), - vec![LocatedCmpop::new( + locate_cmp_ops(&expr, &locator), + vec![LocatedCmpOp::new( TextSize::from(2)..TextSize::from(8), - Cmpop::NotIn + CmpOp::NotIn )] ); @@ -1905,10 +1789,10 @@ else: let expr = Expr::parse(contents, "")?; let locator = Locator::new(contents); assert_eq!( - locate_cmpops(&expr, &locator), - vec![LocatedCmpop::new( + locate_cmp_ops(&expr, &locator), + vec![LocatedCmpOp::new( TextSize::from(2)..TextSize::from(4), - Cmpop::NotEq + CmpOp::NotEq )] ); diff --git a/crates/ruff_python_ast/src/identifier.rs b/crates/ruff_python_ast/src/identifier.rs new file mode 100644 index 0000000000..fb801f098e --- /dev/null +++ b/crates/ruff_python_ast/src/identifier.rs @@ -0,0 +1,336 @@ +//! Extract [`TextRange`] information from AST nodes. +//! +//! For example, given: +//! ```python +//! try: +//! ... +//! except Exception as e: +//! ... +//! ``` +//! +//! This module can be used to identify the [`TextRange`] of the `except` token. + +use ruff_text_size::{TextLen, TextRange, TextSize}; +use rustpython_ast::{Alias, Arg, ArgWithDefault, Pattern}; +use rustpython_parser::ast::{self, ExceptHandler, Ranged, Stmt}; + +use ruff_python_whitespace::{is_python_whitespace, Cursor}; + +use crate::source_code::Locator; + +pub trait Identifier { + /// Return the [`TextRange`] of the identifier in the given AST node. + fn identifier(&self) -> TextRange; +} + +pub trait TryIdentifier { + /// Return the [`TextRange`] of the identifier in the given AST node, or `None` if + /// the node does not have an identifier. + fn try_identifier(&self) -> Option; +} + +impl Identifier for Stmt { + /// Return the [`TextRange`] of the identifier in the given statement. + /// + /// For example, return the range of `f` in: + /// ```python + /// def f(): + /// ... + /// ``` + fn identifier(&self) -> TextRange { + match self { + Stmt::ClassDef(ast::StmtClassDef { name, .. }) + | Stmt::FunctionDef(ast::StmtFunctionDef { name, .. }) + | Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { name, .. }) => name.range(), + _ => self.range(), + } + } +} + +impl Identifier for Arg { + /// Return the [`TextRange`] for the identifier defining an [`Arg`]. + /// + /// For example, return the range of `x` in: + /// ```python + /// def f(x: int): + /// ... + /// ``` + fn identifier(&self) -> TextRange { + self.arg.range() + } +} + +impl Identifier for ArgWithDefault { + /// Return the [`TextRange`] for the identifier defining an [`ArgWithDefault`]. + /// + /// For example, return the range of `x` in: + /// ```python + /// def f(x: int = 0): + /// ... + /// ``` + fn identifier(&self) -> TextRange { + self.def.identifier() + } +} + +impl Identifier for Alias { + /// Return the [`TextRange`] for the identifier defining an [`Alias`]. + /// + /// For example, return the range of `x` in: + /// ```python + /// from foo import bar as x + /// ``` + fn identifier(&self) -> TextRange { + self.asname + .as_ref() + .map_or_else(|| self.name.range(), Ranged::range) + } +} + +impl TryIdentifier for Pattern { + /// Return the [`TextRange`] of the identifier in the given pattern. + /// + /// For example, return the range of `z` in: + /// ```python + /// match x: + /// # Pattern::MatchAs + /// case z: + /// ... + /// ``` + /// + /// Or: + /// ```python + /// match x: + /// # Pattern::MatchAs + /// case y as z: + /// ... + /// ``` + /// + /// Or : + /// ```python + /// match x: + /// # Pattern::MatchMapping + /// case {"a": 1, **z} + /// ... + /// ``` + /// + /// Or : + /// ```python + /// match x: + /// # Pattern::MatchStar + /// case *z: + /// ... + /// ``` + fn try_identifier(&self) -> Option { + let name = match self { + Pattern::MatchAs(ast::PatternMatchAs { + name: Some(name), .. + }) => Some(name), + Pattern::MatchMapping(ast::PatternMatchMapping { + rest: Some(rest), .. + }) => Some(rest), + Pattern::MatchStar(ast::PatternMatchStar { + name: Some(name), .. + }) => Some(name), + _ => None, + }; + name.map(Ranged::range) + } +} + +impl TryIdentifier for ExceptHandler { + /// Return the [`TextRange`] of a named exception in an [`ExceptHandler`]. + /// + /// For example, return the range of `e` in: + /// ```python + /// try: + /// ... + /// except ValueError as e: + /// ... + /// ``` + fn try_identifier(&self) -> Option { + let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { name, .. }) = self; + name.as_ref().map(Ranged::range) + } +} + +/// Return the [`TextRange`] of the `except` token in an [`ExceptHandler`]. +pub fn except(handler: &ExceptHandler, locator: &Locator) -> TextRange { + IdentifierTokenizer::new(locator.contents(), handler.range()) + .next() + .expect("Failed to find `except` token in `ExceptHandler`") +} + +/// Return the [`TextRange`] of the `else` token in a `For`, `AsyncFor`, or `While` statement. +pub fn else_(stmt: &Stmt, locator: &Locator) -> Option { + let (Stmt::For(ast::StmtFor { body, orelse, .. }) + | Stmt::AsyncFor(ast::StmtAsyncFor { body, orelse, .. }) + | Stmt::While(ast::StmtWhile { body, orelse, .. })) = stmt + else { + return None; + }; + + if orelse.is_empty() { + return None; + } + + IdentifierTokenizer::starts_at( + body.last().expect("Expected body to be non-empty").end(), + locator.contents(), + ) + .next() +} + +/// Return `true` if the given character starts a valid Python identifier. +/// +/// Python identifiers must start with an alphabetic character or an underscore. +fn is_python_identifier_start(c: char) -> bool { + c.is_alphabetic() || c == '_' +} + +/// Return `true` if the given character is a valid Python identifier continuation character. +/// +/// Python identifiers can contain alphanumeric characters and underscores, but cannot start with a +/// number. +fn is_python_identifier_continue(c: char) -> bool { + c.is_alphanumeric() || c == '_' +} + +/// Simple zero allocation tokenizer for Python identifiers. +/// +/// The tokenizer must operate over a range that can only contain identifiers, keywords, and +/// comments (along with whitespace and continuation characters). It does not support other tokens, +/// like operators, literals, or delimiters. It also does not differentiate between keywords and +/// identifiers, treating every valid token as an "identifier". +/// +/// This is useful for cases like, e.g., identifying the alias name in an aliased import (`bar` in +/// `import foo as bar`), where we're guaranteed to only have identifiers and keywords in the +/// relevant range. +pub(crate) struct IdentifierTokenizer<'a> { + cursor: Cursor<'a>, + offset: TextSize, +} + +impl<'a> IdentifierTokenizer<'a> { + pub(crate) fn new(source: &'a str, range: TextRange) -> Self { + Self { + cursor: Cursor::new(&source[range]), + offset: range.start(), + } + } + + pub(crate) fn starts_at(offset: TextSize, source: &'a str) -> Self { + let range = TextRange::new(offset, source.text_len()); + Self::new(source, range) + } + + fn next_token(&mut self) -> Option { + while let Some(c) = { + self.offset += self.cursor.token_len(); + self.cursor.start_token(); + self.cursor.bump() + } { + match c { + c if is_python_identifier_start(c) => { + self.cursor.eat_while(is_python_identifier_continue); + return Some(TextRange::at(self.offset, self.cursor.token_len())); + } + + c if is_python_whitespace(c) => { + self.cursor.eat_while(is_python_whitespace); + } + + '#' => { + self.cursor.eat_while(|c| !matches!(c, '\n' | '\r')); + } + + '\r' => { + self.cursor.eat_char('\n'); + } + + '\n' => { + // Nothing to do. + } + + '\\' => { + // Nothing to do. + } + + _ => { + // Nothing to do. + } + }; + } + + None + } +} + +impl Iterator for IdentifierTokenizer<'_> { + type Item = TextRange; + + fn next(&mut self) -> Option { + self.next_token() + } +} + +#[cfg(test)] +mod tests { + use anyhow::Result; + use ruff_text_size::{TextRange, TextSize}; + use rustpython_ast::{Ranged, Stmt}; + use rustpython_parser::Parse; + + use crate::identifier; + use crate::identifier::IdentifierTokenizer; + use crate::source_code::Locator; + + #[test] + fn extract_else_range() -> Result<()> { + let contents = r#" +for x in y: + pass +else: + pass +"# + .trim(); + let stmt = Stmt::parse(contents, "")?; + let locator = Locator::new(contents); + let range = identifier::else_(&stmt, &locator).unwrap(); + assert_eq!(&contents[range], "else"); + assert_eq!( + range, + TextRange::new(TextSize::from(21), TextSize::from(25)) + ); + Ok(()) + } + + #[test] + fn extract_global_names() -> Result<()> { + let contents = r#"global X,Y, Z"#.trim(); + let stmt = Stmt::parse(contents, "")?; + let locator = Locator::new(contents); + + let mut names = IdentifierTokenizer::new(locator.contents(), stmt.range()); + + let range = names.next_token().unwrap(); + assert_eq!(&contents[range], "global"); + assert_eq!(range, TextRange::new(TextSize::from(0), TextSize::from(6))); + + let range = names.next_token().unwrap(); + assert_eq!(&contents[range], "X"); + assert_eq!(range, TextRange::new(TextSize::from(7), TextSize::from(8))); + + let range = names.next_token().unwrap(); + assert_eq!(&contents[range], "Y"); + assert_eq!(range, TextRange::new(TextSize::from(9), TextSize::from(10))); + + let range = names.next_token().unwrap(); + assert_eq!(&contents[range], "Z"); + assert_eq!( + range, + TextRange::new(TextSize::from(12), TextSize::from(13)) + ); + Ok(()) + } +} diff --git a/crates/ruff_python_ast/src/imports.rs b/crates/ruff_python_ast/src/imports.rs index 098f75c0d6..6adfb9fce9 100644 --- a/crates/ruff_python_ast/src/imports.rs +++ b/crates/ruff_python_ast/src/imports.rs @@ -160,8 +160,8 @@ impl ImportMap { } impl<'a> IntoIterator for &'a ImportMap { - type Item = (&'a String, &'a Vec); type IntoIter = std::collections::hash_map::Iter<'a, String, Vec>; + type Item = (&'a String, &'a Vec); fn into_iter(self) -> Self::IntoIter { self.module_to_imports.iter() diff --git a/crates/ruff_python_ast/src/lib.rs b/crates/ruff_python_ast/src/lib.rs index be82bf8233..72928ec9e9 100644 --- a/crates/ruff_python_ast/src/lib.rs +++ b/crates/ruff_python_ast/src/lib.rs @@ -6,9 +6,9 @@ pub mod docstrings; pub mod function; pub mod hashable; pub mod helpers; +pub mod identifier; pub mod imports; pub mod node; -pub mod prelude; pub mod relocate; pub mod source_code; pub mod statement_visitor; diff --git a/crates/ruff_python_ast/src/node.rs b/crates/ruff_python_ast/src/node.rs index 1c8d66ee51..ccbc44c723 100644 --- a/crates/ruff_python_ast/src/node.rs +++ b/crates/ruff_python_ast/src/node.rs @@ -1,5 +1,9 @@ -use crate::prelude::*; use ruff_text_size::TextRange; +use rustpython_ast::{ + Alias, Arg, ArgWithDefault, Arguments, Comprehension, Decorator, ExceptHandler, Keyword, + MatchCase, Mod, Pattern, Stmt, TypeIgnore, WithItem, +}; +use rustpython_parser::ast::{self, Expr, Ranged}; use std::ptr::NonNull; pub trait AstNode: Ranged { @@ -17,82 +21,83 @@ pub trait AstNode: Ranged { #[derive(Clone, Debug, is_macro::Is, PartialEq)] pub enum AnyNode { - ModModule(ModModule), - ModInteractive(ModInteractive), - ModExpression(ModExpression), - ModFunctionType(ModFunctionType), - StmtFunctionDef(StmtFunctionDef), - StmtAsyncFunctionDef(StmtAsyncFunctionDef), - StmtClassDef(StmtClassDef), - StmtReturn(StmtReturn), - StmtDelete(StmtDelete), - StmtAssign(StmtAssign), - StmtAugAssign(StmtAugAssign), - StmtAnnAssign(StmtAnnAssign), - StmtFor(StmtFor), - StmtAsyncFor(StmtAsyncFor), - StmtWhile(StmtWhile), - StmtIf(StmtIf), - StmtWith(StmtWith), - StmtAsyncWith(StmtAsyncWith), - StmtMatch(StmtMatch), - StmtRaise(StmtRaise), - StmtTry(StmtTry), - StmtTryStar(StmtTryStar), - StmtAssert(StmtAssert), - StmtImport(StmtImport), - StmtImportFrom(StmtImportFrom), - StmtGlobal(StmtGlobal), - StmtNonlocal(StmtNonlocal), - StmtExpr(StmtExpr), - StmtPass(StmtPass), - StmtBreak(StmtBreak), - StmtContinue(StmtContinue), - ExprBoolOp(ExprBoolOp), - ExprNamedExpr(ExprNamedExpr), - ExprBinOp(ExprBinOp), - ExprUnaryOp(ExprUnaryOp), - ExprLambda(ExprLambda), - ExprIfExp(ExprIfExp), - ExprDict(ExprDict), - ExprSet(ExprSet), - ExprListComp(ExprListComp), - ExprSetComp(ExprSetComp), - ExprDictComp(ExprDictComp), - ExprGeneratorExp(ExprGeneratorExp), - ExprAwait(ExprAwait), - ExprYield(ExprYield), - ExprYieldFrom(ExprYieldFrom), - ExprCompare(ExprCompare), - ExprCall(ExprCall), - ExprFormattedValue(ExprFormattedValue), - ExprJoinedStr(ExprJoinedStr), - ExprConstant(ExprConstant), - ExprAttribute(ExprAttribute), - ExprSubscript(ExprSubscript), - ExprStarred(ExprStarred), - ExprName(ExprName), - ExprList(ExprList), - ExprTuple(ExprTuple), - ExprSlice(ExprSlice), - ExcepthandlerExceptHandler(ExcepthandlerExceptHandler), - PatternMatchValue(PatternMatchValue), - PatternMatchSingleton(PatternMatchSingleton), - PatternMatchSequence(PatternMatchSequence), - PatternMatchMapping(PatternMatchMapping), - PatternMatchClass(PatternMatchClass), - PatternMatchStar(PatternMatchStar), - PatternMatchAs(PatternMatchAs), - PatternMatchOr(PatternMatchOr), - TypeIgnoreTypeIgnore(TypeIgnoreTypeIgnore), - Comprehension(Comprehension), - Arguments(Arguments), - Arg(Arg), - Keyword(Keyword), - Alias(Alias), - Withitem(Withitem), - MatchCase(MatchCase), - Decorator(Decorator), + ModModule(ast::ModModule), + ModInteractive(ast::ModInteractive), + ModExpression(ast::ModExpression), + ModFunctionType(ast::ModFunctionType), + StmtFunctionDef(ast::StmtFunctionDef), + StmtAsyncFunctionDef(ast::StmtAsyncFunctionDef), + StmtClassDef(ast::StmtClassDef), + StmtReturn(ast::StmtReturn), + StmtDelete(ast::StmtDelete), + StmtAssign(ast::StmtAssign), + StmtAugAssign(ast::StmtAugAssign), + StmtAnnAssign(ast::StmtAnnAssign), + StmtFor(ast::StmtFor), + StmtAsyncFor(ast::StmtAsyncFor), + StmtWhile(ast::StmtWhile), + StmtIf(ast::StmtIf), + StmtWith(ast::StmtWith), + StmtAsyncWith(ast::StmtAsyncWith), + StmtMatch(ast::StmtMatch), + StmtRaise(ast::StmtRaise), + StmtTry(ast::StmtTry), + StmtTryStar(ast::StmtTryStar), + StmtAssert(ast::StmtAssert), + StmtImport(ast::StmtImport), + StmtImportFrom(ast::StmtImportFrom), + StmtGlobal(ast::StmtGlobal), + StmtNonlocal(ast::StmtNonlocal), + StmtExpr(ast::StmtExpr), + StmtPass(ast::StmtPass), + StmtBreak(ast::StmtBreak), + StmtContinue(ast::StmtContinue), + ExprBoolOp(ast::ExprBoolOp), + ExprNamedExpr(ast::ExprNamedExpr), + ExprBinOp(ast::ExprBinOp), + ExprUnaryOp(ast::ExprUnaryOp), + ExprLambda(ast::ExprLambda), + ExprIfExp(ast::ExprIfExp), + ExprDict(ast::ExprDict), + ExprSet(ast::ExprSet), + ExprListComp(ast::ExprListComp), + ExprSetComp(ast::ExprSetComp), + ExprDictComp(ast::ExprDictComp), + ExprGeneratorExp(ast::ExprGeneratorExp), + ExprAwait(ast::ExprAwait), + ExprYield(ast::ExprYield), + ExprYieldFrom(ast::ExprYieldFrom), + ExprCompare(ast::ExprCompare), + ExprCall(ast::ExprCall), + ExprFormattedValue(ast::ExprFormattedValue), + ExprJoinedStr(ast::ExprJoinedStr), + ExprConstant(ast::ExprConstant), + ExprAttribute(ast::ExprAttribute), + ExprSubscript(ast::ExprSubscript), + ExprStarred(ast::ExprStarred), + ExprName(ast::ExprName), + ExprList(ast::ExprList), + ExprTuple(ast::ExprTuple), + ExprSlice(ast::ExprSlice), + ExceptHandlerExceptHandler(ast::ExceptHandlerExceptHandler), + PatternMatchValue(ast::PatternMatchValue), + PatternMatchSingleton(ast::PatternMatchSingleton), + PatternMatchSequence(ast::PatternMatchSequence), + PatternMatchMapping(ast::PatternMatchMapping), + PatternMatchClass(ast::PatternMatchClass), + PatternMatchStar(ast::PatternMatchStar), + PatternMatchAs(ast::PatternMatchAs), + PatternMatchOr(ast::PatternMatchOr), + TypeIgnoreTypeIgnore(ast::TypeIgnoreTypeIgnore), + Comprehension(Comprehension), + Arguments(Arguments), + Arg(Arg), + ArgWithDefault(ArgWithDefault), + Keyword(Keyword), + Alias(Alias), + WithItem(WithItem), + MatchCase(MatchCase), + Decorator(Decorator), } impl AnyNode { @@ -157,7 +162,7 @@ impl AnyNode { | AnyNode::ExprList(_) | AnyNode::ExprTuple(_) | AnyNode::ExprSlice(_) - | AnyNode::ExcepthandlerExceptHandler(_) + | AnyNode::ExceptHandlerExceptHandler(_) | AnyNode::PatternMatchValue(_) | AnyNode::PatternMatchSingleton(_) | AnyNode::PatternMatchSequence(_) @@ -170,9 +175,10 @@ impl AnyNode { | AnyNode::Comprehension(_) | AnyNode::Arguments(_) | AnyNode::Arg(_) + | AnyNode::ArgWithDefault(_) | AnyNode::Keyword(_) | AnyNode::Alias(_) - | AnyNode::Withitem(_) + | AnyNode::WithItem(_) | AnyNode::MatchCase(_) | AnyNode::Decorator(_) => None, } @@ -239,7 +245,7 @@ impl AnyNode { | AnyNode::StmtPass(_) | AnyNode::StmtBreak(_) | AnyNode::StmtContinue(_) - | AnyNode::ExcepthandlerExceptHandler(_) + | AnyNode::ExceptHandlerExceptHandler(_) | AnyNode::PatternMatchValue(_) | AnyNode::PatternMatchSingleton(_) | AnyNode::PatternMatchSequence(_) @@ -252,9 +258,10 @@ impl AnyNode { | AnyNode::Comprehension(_) | AnyNode::Arguments(_) | AnyNode::Arg(_) + | AnyNode::ArgWithDefault(_) | AnyNode::Keyword(_) | AnyNode::Alias(_) - | AnyNode::Withitem(_) + | AnyNode::WithItem(_) | AnyNode::MatchCase(_) | AnyNode::Decorator(_) => None, } @@ -321,7 +328,7 @@ impl AnyNode { | AnyNode::ExprList(_) | AnyNode::ExprTuple(_) | AnyNode::ExprSlice(_) - | AnyNode::ExcepthandlerExceptHandler(_) + | AnyNode::ExceptHandlerExceptHandler(_) | AnyNode::PatternMatchValue(_) | AnyNode::PatternMatchSingleton(_) | AnyNode::PatternMatchSequence(_) @@ -334,9 +341,10 @@ impl AnyNode { | AnyNode::Comprehension(_) | AnyNode::Arguments(_) | AnyNode::Arg(_) + | AnyNode::ArgWithDefault(_) | AnyNode::Keyword(_) | AnyNode::Alias(_) - | AnyNode::Withitem(_) + | AnyNode::WithItem(_) | AnyNode::MatchCase(_) | AnyNode::Decorator(_) => None, } @@ -411,22 +419,23 @@ impl AnyNode { | AnyNode::ExprList(_) | AnyNode::ExprTuple(_) | AnyNode::ExprSlice(_) - | AnyNode::ExcepthandlerExceptHandler(_) + | AnyNode::ExceptHandlerExceptHandler(_) | AnyNode::TypeIgnoreTypeIgnore(_) | AnyNode::Comprehension(_) | AnyNode::Arguments(_) | AnyNode::Arg(_) + | AnyNode::ArgWithDefault(_) | AnyNode::Keyword(_) | AnyNode::Alias(_) - | AnyNode::Withitem(_) + | AnyNode::WithItem(_) | AnyNode::MatchCase(_) | AnyNode::Decorator(_) => None, } } - pub fn except_handler(self) -> Option { + pub fn except_handler(self) -> Option { match self { - AnyNode::ExcepthandlerExceptHandler(node) => Some(Excepthandler::ExceptHandler(node)), + AnyNode::ExceptHandlerExceptHandler(node) => Some(ExceptHandler::ExceptHandler(node)), AnyNode::ModModule(_) | AnyNode::ModInteractive(_) @@ -498,9 +507,10 @@ impl AnyNode { | AnyNode::Comprehension(_) | AnyNode::Arguments(_) | AnyNode::Arg(_) + | AnyNode::ArgWithDefault(_) | AnyNode::Keyword(_) | AnyNode::Alias(_) - | AnyNode::Withitem(_) + | AnyNode::WithItem(_) | AnyNode::MatchCase(_) | AnyNode::Decorator(_) => None, } @@ -576,13 +586,14 @@ impl AnyNode { | AnyNode::PatternMatchStar(_) | AnyNode::PatternMatchAs(_) | AnyNode::PatternMatchOr(_) - | AnyNode::ExcepthandlerExceptHandler(_) + | AnyNode::ExceptHandlerExceptHandler(_) | AnyNode::Comprehension(_) | AnyNode::Arguments(_) | AnyNode::Arg(_) + | AnyNode::ArgWithDefault(_) | AnyNode::Keyword(_) | AnyNode::Alias(_) - | AnyNode::Withitem(_) + | AnyNode::WithItem(_) | AnyNode::MatchCase(_) | AnyNode::Decorator(_) => None, } @@ -672,7 +683,7 @@ impl AnyNode { Self::ExprList(node) => AnyNodeRef::ExprList(node), Self::ExprTuple(node) => AnyNodeRef::ExprTuple(node), Self::ExprSlice(node) => AnyNodeRef::ExprSlice(node), - Self::ExcepthandlerExceptHandler(node) => AnyNodeRef::ExcepthandlerExceptHandler(node), + Self::ExceptHandlerExceptHandler(node) => AnyNodeRef::ExceptHandlerExceptHandler(node), Self::PatternMatchValue(node) => AnyNodeRef::PatternMatchValue(node), Self::PatternMatchSingleton(node) => AnyNodeRef::PatternMatchSingleton(node), Self::PatternMatchSequence(node) => AnyNodeRef::PatternMatchSequence(node), @@ -685,9 +696,10 @@ impl AnyNode { Self::Comprehension(node) => AnyNodeRef::Comprehension(node), Self::Arguments(node) => AnyNodeRef::Arguments(node), Self::Arg(node) => AnyNodeRef::Arg(node), + Self::ArgWithDefault(node) => AnyNodeRef::ArgWithDefault(node), Self::Keyword(node) => AnyNodeRef::Keyword(node), Self::Alias(node) => AnyNodeRef::Alias(node), - Self::Withitem(node) => AnyNodeRef::Withitem(node), + Self::WithItem(node) => AnyNodeRef::WithItem(node), Self::MatchCase(node) => AnyNodeRef::MatchCase(node), Self::Decorator(node) => AnyNodeRef::Decorator(node), } @@ -699,7 +711,7 @@ impl AnyNode { } } -impl AstNode for ModModule { +impl AstNode for ast::ModModule { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -727,7 +739,7 @@ impl AstNode for ModModule { AnyNode::from(self) } } -impl AstNode for ModInteractive { +impl AstNode for ast::ModInteractive { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -755,7 +767,7 @@ impl AstNode for ModInteractive { AnyNode::from(self) } } -impl AstNode for ModExpression { +impl AstNode for ast::ModExpression { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -783,7 +795,7 @@ impl AstNode for ModExpression { AnyNode::from(self) } } -impl AstNode for ModFunctionType { +impl AstNode for ast::ModFunctionType { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -811,7 +823,7 @@ impl AstNode for ModFunctionType { AnyNode::from(self) } } -impl AstNode for StmtFunctionDef { +impl AstNode for ast::StmtFunctionDef { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -839,7 +851,7 @@ impl AstNode for StmtFunctionDef { AnyNode::from(self) } } -impl AstNode for StmtAsyncFunctionDef { +impl AstNode for ast::StmtAsyncFunctionDef { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -867,7 +879,7 @@ impl AstNode for StmtAsyncFunctionDef { AnyNode::from(self) } } -impl AstNode for StmtClassDef { +impl AstNode for ast::StmtClassDef { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -895,7 +907,7 @@ impl AstNode for StmtClassDef { AnyNode::from(self) } } -impl AstNode for StmtReturn { +impl AstNode for ast::StmtReturn { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -923,7 +935,7 @@ impl AstNode for StmtReturn { AnyNode::from(self) } } -impl AstNode for StmtDelete { +impl AstNode for ast::StmtDelete { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -951,7 +963,7 @@ impl AstNode for StmtDelete { AnyNode::from(self) } } -impl AstNode for StmtAssign { +impl AstNode for ast::StmtAssign { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -979,7 +991,7 @@ impl AstNode for StmtAssign { AnyNode::from(self) } } -impl AstNode for StmtAugAssign { +impl AstNode for ast::StmtAugAssign { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1007,7 +1019,7 @@ impl AstNode for StmtAugAssign { AnyNode::from(self) } } -impl AstNode for StmtAnnAssign { +impl AstNode for ast::StmtAnnAssign { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1035,7 +1047,7 @@ impl AstNode for StmtAnnAssign { AnyNode::from(self) } } -impl AstNode for StmtFor { +impl AstNode for ast::StmtFor { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1063,7 +1075,7 @@ impl AstNode for StmtFor { AnyNode::from(self) } } -impl AstNode for StmtAsyncFor { +impl AstNode for ast::StmtAsyncFor { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1091,7 +1103,7 @@ impl AstNode for StmtAsyncFor { AnyNode::from(self) } } -impl AstNode for StmtWhile { +impl AstNode for ast::StmtWhile { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1119,7 +1131,7 @@ impl AstNode for StmtWhile { AnyNode::from(self) } } -impl AstNode for StmtIf { +impl AstNode for ast::StmtIf { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1147,7 +1159,7 @@ impl AstNode for StmtIf { AnyNode::from(self) } } -impl AstNode for StmtWith { +impl AstNode for ast::StmtWith { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1175,7 +1187,7 @@ impl AstNode for StmtWith { AnyNode::from(self) } } -impl AstNode for StmtAsyncWith { +impl AstNode for ast::StmtAsyncWith { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1203,7 +1215,7 @@ impl AstNode for StmtAsyncWith { AnyNode::from(self) } } -impl AstNode for StmtMatch { +impl AstNode for ast::StmtMatch { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1231,7 +1243,7 @@ impl AstNode for StmtMatch { AnyNode::from(self) } } -impl AstNode for StmtRaise { +impl AstNode for ast::StmtRaise { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1259,7 +1271,7 @@ impl AstNode for StmtRaise { AnyNode::from(self) } } -impl AstNode for StmtTry { +impl AstNode for ast::StmtTry { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1287,7 +1299,7 @@ impl AstNode for StmtTry { AnyNode::from(self) } } -impl AstNode for StmtTryStar { +impl AstNode for ast::StmtTryStar { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1315,7 +1327,7 @@ impl AstNode for StmtTryStar { AnyNode::from(self) } } -impl AstNode for StmtAssert { +impl AstNode for ast::StmtAssert { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1343,7 +1355,7 @@ impl AstNode for StmtAssert { AnyNode::from(self) } } -impl AstNode for StmtImport { +impl AstNode for ast::StmtImport { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1371,7 +1383,7 @@ impl AstNode for StmtImport { AnyNode::from(self) } } -impl AstNode for StmtImportFrom { +impl AstNode for ast::StmtImportFrom { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1399,7 +1411,7 @@ impl AstNode for StmtImportFrom { AnyNode::from(self) } } -impl AstNode for StmtGlobal { +impl AstNode for ast::StmtGlobal { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1427,7 +1439,7 @@ impl AstNode for StmtGlobal { AnyNode::from(self) } } -impl AstNode for StmtNonlocal { +impl AstNode for ast::StmtNonlocal { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1455,7 +1467,7 @@ impl AstNode for StmtNonlocal { AnyNode::from(self) } } -impl AstNode for StmtExpr { +impl AstNode for ast::StmtExpr { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1483,7 +1495,7 @@ impl AstNode for StmtExpr { AnyNode::from(self) } } -impl AstNode for StmtPass { +impl AstNode for ast::StmtPass { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1511,7 +1523,7 @@ impl AstNode for StmtPass { AnyNode::from(self) } } -impl AstNode for StmtBreak { +impl AstNode for ast::StmtBreak { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1539,7 +1551,7 @@ impl AstNode for StmtBreak { AnyNode::from(self) } } -impl AstNode for StmtContinue { +impl AstNode for ast::StmtContinue { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1567,7 +1579,7 @@ impl AstNode for StmtContinue { AnyNode::from(self) } } -impl AstNode for ExprBoolOp { +impl AstNode for ast::ExprBoolOp { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1595,7 +1607,7 @@ impl AstNode for ExprBoolOp { AnyNode::from(self) } } -impl AstNode for ExprNamedExpr { +impl AstNode for ast::ExprNamedExpr { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1623,7 +1635,7 @@ impl AstNode for ExprNamedExpr { AnyNode::from(self) } } -impl AstNode for ExprBinOp { +impl AstNode for ast::ExprBinOp { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1651,7 +1663,7 @@ impl AstNode for ExprBinOp { AnyNode::from(self) } } -impl AstNode for ExprUnaryOp { +impl AstNode for ast::ExprUnaryOp { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1679,7 +1691,7 @@ impl AstNode for ExprUnaryOp { AnyNode::from(self) } } -impl AstNode for ExprLambda { +impl AstNode for ast::ExprLambda { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1707,7 +1719,7 @@ impl AstNode for ExprLambda { AnyNode::from(self) } } -impl AstNode for ExprIfExp { +impl AstNode for ast::ExprIfExp { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1735,7 +1747,7 @@ impl AstNode for ExprIfExp { AnyNode::from(self) } } -impl AstNode for ExprDict { +impl AstNode for ast::ExprDict { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1763,7 +1775,7 @@ impl AstNode for ExprDict { AnyNode::from(self) } } -impl AstNode for ExprSet { +impl AstNode for ast::ExprSet { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1791,7 +1803,7 @@ impl AstNode for ExprSet { AnyNode::from(self) } } -impl AstNode for ExprListComp { +impl AstNode for ast::ExprListComp { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1819,7 +1831,7 @@ impl AstNode for ExprListComp { AnyNode::from(self) } } -impl AstNode for ExprSetComp { +impl AstNode for ast::ExprSetComp { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1847,7 +1859,7 @@ impl AstNode for ExprSetComp { AnyNode::from(self) } } -impl AstNode for ExprDictComp { +impl AstNode for ast::ExprDictComp { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1875,7 +1887,7 @@ impl AstNode for ExprDictComp { AnyNode::from(self) } } -impl AstNode for ExprGeneratorExp { +impl AstNode for ast::ExprGeneratorExp { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1903,7 +1915,7 @@ impl AstNode for ExprGeneratorExp { AnyNode::from(self) } } -impl AstNode for ExprAwait { +impl AstNode for ast::ExprAwait { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1931,7 +1943,7 @@ impl AstNode for ExprAwait { AnyNode::from(self) } } -impl AstNode for ExprYield { +impl AstNode for ast::ExprYield { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1959,7 +1971,7 @@ impl AstNode for ExprYield { AnyNode::from(self) } } -impl AstNode for ExprYieldFrom { +impl AstNode for ast::ExprYieldFrom { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -1987,7 +1999,7 @@ impl AstNode for ExprYieldFrom { AnyNode::from(self) } } -impl AstNode for ExprCompare { +impl AstNode for ast::ExprCompare { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2015,7 +2027,7 @@ impl AstNode for ExprCompare { AnyNode::from(self) } } -impl AstNode for ExprCall { +impl AstNode for ast::ExprCall { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2043,7 +2055,7 @@ impl AstNode for ExprCall { AnyNode::from(self) } } -impl AstNode for ExprFormattedValue { +impl AstNode for ast::ExprFormattedValue { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2071,7 +2083,7 @@ impl AstNode for ExprFormattedValue { AnyNode::from(self) } } -impl AstNode for ExprJoinedStr { +impl AstNode for ast::ExprJoinedStr { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2099,7 +2111,7 @@ impl AstNode for ExprJoinedStr { AnyNode::from(self) } } -impl AstNode for ExprConstant { +impl AstNode for ast::ExprConstant { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2127,7 +2139,7 @@ impl AstNode for ExprConstant { AnyNode::from(self) } } -impl AstNode for ExprAttribute { +impl AstNode for ast::ExprAttribute { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2155,7 +2167,7 @@ impl AstNode for ExprAttribute { AnyNode::from(self) } } -impl AstNode for ExprSubscript { +impl AstNode for ast::ExprSubscript { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2183,7 +2195,7 @@ impl AstNode for ExprSubscript { AnyNode::from(self) } } -impl AstNode for ExprStarred { +impl AstNode for ast::ExprStarred { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2211,7 +2223,7 @@ impl AstNode for ExprStarred { AnyNode::from(self) } } -impl AstNode for ExprName { +impl AstNode for ast::ExprName { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2239,7 +2251,7 @@ impl AstNode for ExprName { AnyNode::from(self) } } -impl AstNode for ExprList { +impl AstNode for ast::ExprList { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2267,7 +2279,7 @@ impl AstNode for ExprList { AnyNode::from(self) } } -impl AstNode for ExprTuple { +impl AstNode for ast::ExprTuple { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2295,7 +2307,7 @@ impl AstNode for ExprTuple { AnyNode::from(self) } } -impl AstNode for ExprSlice { +impl AstNode for ast::ExprSlice { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2323,12 +2335,12 @@ impl AstNode for ExprSlice { AnyNode::from(self) } } -impl AstNode for ExcepthandlerExceptHandler { +impl AstNode for ast::ExceptHandlerExceptHandler { fn cast(kind: AnyNode) -> Option where Self: Sized, { - if let AnyNode::ExcepthandlerExceptHandler(node) = kind { + if let AnyNode::ExceptHandlerExceptHandler(node) = kind { Some(node) } else { None @@ -2336,7 +2348,7 @@ impl AstNode for ExcepthandlerExceptHandler { } fn cast_ref(kind: AnyNodeRef) -> Option<&Self> { - if let AnyNodeRef::ExcepthandlerExceptHandler(node) = kind { + if let AnyNodeRef::ExceptHandlerExceptHandler(node) = kind { Some(node) } else { None @@ -2351,7 +2363,7 @@ impl AstNode for ExcepthandlerExceptHandler { AnyNode::from(self) } } -impl AstNode for PatternMatchValue { +impl AstNode for ast::PatternMatchValue { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2379,7 +2391,7 @@ impl AstNode for PatternMatchValue { AnyNode::from(self) } } -impl AstNode for PatternMatchSingleton { +impl AstNode for ast::PatternMatchSingleton { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2407,7 +2419,7 @@ impl AstNode for PatternMatchSingleton { AnyNode::from(self) } } -impl AstNode for PatternMatchSequence { +impl AstNode for ast::PatternMatchSequence { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2435,7 +2447,7 @@ impl AstNode for PatternMatchSequence { AnyNode::from(self) } } -impl AstNode for PatternMatchMapping { +impl AstNode for ast::PatternMatchMapping { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2463,7 +2475,7 @@ impl AstNode for PatternMatchMapping { AnyNode::from(self) } } -impl AstNode for PatternMatchClass { +impl AstNode for ast::PatternMatchClass { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2491,7 +2503,7 @@ impl AstNode for PatternMatchClass { AnyNode::from(self) } } -impl AstNode for PatternMatchStar { +impl AstNode for ast::PatternMatchStar { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2519,7 +2531,7 @@ impl AstNode for PatternMatchStar { AnyNode::from(self) } } -impl AstNode for PatternMatchAs { +impl AstNode for ast::PatternMatchAs { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2547,7 +2559,7 @@ impl AstNode for PatternMatchAs { AnyNode::from(self) } } -impl AstNode for PatternMatchOr { +impl AstNode for ast::PatternMatchOr { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2575,7 +2587,7 @@ impl AstNode for PatternMatchOr { AnyNode::from(self) } } -impl AstNode for TypeIgnoreTypeIgnore { +impl AstNode for ast::TypeIgnoreTypeIgnore { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2604,7 +2616,7 @@ impl AstNode for TypeIgnoreTypeIgnore { } } -impl AstNode for Comprehension { +impl AstNode for Comprehension { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2632,7 +2644,7 @@ impl AstNode for Comprehension { AnyNode::from(self) } } -impl AstNode for Arguments { +impl AstNode for Arguments { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2660,7 +2672,7 @@ impl AstNode for Arguments { AnyNode::from(self) } } -impl AstNode for Arg { +impl AstNode for Arg { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2688,7 +2700,35 @@ impl AstNode for Arg { AnyNode::from(self) } } -impl AstNode for Keyword { +impl AstNode for ArgWithDefault { + fn cast(kind: AnyNode) -> Option + where + Self: Sized, + { + if let AnyNode::ArgWithDefault(node) = kind { + Some(node) + } else { + None + } + } + + fn cast_ref(kind: AnyNodeRef) -> Option<&Self> { + if let AnyNodeRef::ArgWithDefault(node) = kind { + Some(node) + } else { + None + } + } + + fn as_any_node_ref(&self) -> AnyNodeRef { + AnyNodeRef::from(self) + } + + fn into_any_node(self) -> AnyNode { + AnyNode::from(self) + } +} +impl AstNode for Keyword { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2716,7 +2756,7 @@ impl AstNode for Keyword { AnyNode::from(self) } } -impl AstNode for Alias { +impl AstNode for Alias { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2744,12 +2784,12 @@ impl AstNode for Alias { AnyNode::from(self) } } -impl AstNode for Withitem { +impl AstNode for WithItem { fn cast(kind: AnyNode) -> Option where Self: Sized, { - if let AnyNode::Withitem(node) = kind { + if let AnyNode::WithItem(node) = kind { Some(node) } else { None @@ -2757,7 +2797,7 @@ impl AstNode for Withitem { } fn cast_ref(kind: AnyNodeRef) -> Option<&Self> { - if let AnyNodeRef::Withitem(node) = kind { + if let AnyNodeRef::WithItem(node) = kind { Some(node) } else { None @@ -2772,7 +2812,7 @@ impl AstNode for Withitem { AnyNode::from(self) } } -impl AstNode for MatchCase { +impl AstNode for MatchCase { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2801,7 +2841,7 @@ impl AstNode for MatchCase { } } -impl AstNode for Decorator { +impl AstNode for Decorator { fn cast(kind: AnyNode) -> Option where Self: Sized, @@ -2924,10 +2964,10 @@ impl From for AnyNode { } } -impl From for AnyNode { - fn from(handler: Excepthandler) -> Self { +impl From for AnyNode { + fn from(handler: ExceptHandler) -> Self { match handler { - Excepthandler::ExceptHandler(handler) => AnyNode::ExcepthandlerExceptHandler(handler), + ExceptHandler::ExceptHandler(handler) => AnyNode::ExceptHandlerExceptHandler(handler), } } } @@ -2940,410 +2980,410 @@ impl From for AnyNode { } } -impl From for AnyNode { - fn from(node: ModModule) -> Self { +impl From for AnyNode { + fn from(node: ast::ModModule) -> Self { AnyNode::ModModule(node) } } -impl From for AnyNode { - fn from(node: ModInteractive) -> Self { +impl From for AnyNode { + fn from(node: ast::ModInteractive) -> Self { AnyNode::ModInteractive(node) } } -impl From for AnyNode { - fn from(node: ModExpression) -> Self { +impl From for AnyNode { + fn from(node: ast::ModExpression) -> Self { AnyNode::ModExpression(node) } } -impl From for AnyNode { - fn from(node: ModFunctionType) -> Self { +impl From for AnyNode { + fn from(node: ast::ModFunctionType) -> Self { AnyNode::ModFunctionType(node) } } -impl From for AnyNode { - fn from(node: StmtFunctionDef) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtFunctionDef) -> Self { AnyNode::StmtFunctionDef(node) } } -impl From for AnyNode { - fn from(node: StmtAsyncFunctionDef) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtAsyncFunctionDef) -> Self { AnyNode::StmtAsyncFunctionDef(node) } } -impl From for AnyNode { - fn from(node: StmtClassDef) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtClassDef) -> Self { AnyNode::StmtClassDef(node) } } -impl From for AnyNode { - fn from(node: StmtReturn) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtReturn) -> Self { AnyNode::StmtReturn(node) } } -impl From for AnyNode { - fn from(node: StmtDelete) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtDelete) -> Self { AnyNode::StmtDelete(node) } } -impl From for AnyNode { - fn from(node: StmtAssign) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtAssign) -> Self { AnyNode::StmtAssign(node) } } -impl From for AnyNode { - fn from(node: StmtAugAssign) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtAugAssign) -> Self { AnyNode::StmtAugAssign(node) } } -impl From for AnyNode { - fn from(node: StmtAnnAssign) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtAnnAssign) -> Self { AnyNode::StmtAnnAssign(node) } } -impl From for AnyNode { - fn from(node: StmtFor) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtFor) -> Self { AnyNode::StmtFor(node) } } -impl From for AnyNode { - fn from(node: StmtAsyncFor) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtAsyncFor) -> Self { AnyNode::StmtAsyncFor(node) } } -impl From for AnyNode { - fn from(node: StmtWhile) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtWhile) -> Self { AnyNode::StmtWhile(node) } } -impl From for AnyNode { - fn from(node: StmtIf) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtIf) -> Self { AnyNode::StmtIf(node) } } -impl From for AnyNode { - fn from(node: StmtWith) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtWith) -> Self { AnyNode::StmtWith(node) } } -impl From for AnyNode { - fn from(node: StmtAsyncWith) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtAsyncWith) -> Self { AnyNode::StmtAsyncWith(node) } } -impl From for AnyNode { - fn from(node: StmtMatch) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtMatch) -> Self { AnyNode::StmtMatch(node) } } -impl From for AnyNode { - fn from(node: StmtRaise) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtRaise) -> Self { AnyNode::StmtRaise(node) } } -impl From for AnyNode { - fn from(node: StmtTry) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtTry) -> Self { AnyNode::StmtTry(node) } } -impl From for AnyNode { - fn from(node: StmtTryStar) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtTryStar) -> Self { AnyNode::StmtTryStar(node) } } -impl From for AnyNode { - fn from(node: StmtAssert) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtAssert) -> Self { AnyNode::StmtAssert(node) } } -impl From for AnyNode { - fn from(node: StmtImport) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtImport) -> Self { AnyNode::StmtImport(node) } } -impl From for AnyNode { - fn from(node: StmtImportFrom) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtImportFrom) -> Self { AnyNode::StmtImportFrom(node) } } -impl From for AnyNode { - fn from(node: StmtGlobal) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtGlobal) -> Self { AnyNode::StmtGlobal(node) } } -impl From for AnyNode { - fn from(node: StmtNonlocal) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtNonlocal) -> Self { AnyNode::StmtNonlocal(node) } } -impl From for AnyNode { - fn from(node: StmtExpr) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtExpr) -> Self { AnyNode::StmtExpr(node) } } -impl From for AnyNode { - fn from(node: StmtPass) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtPass) -> Self { AnyNode::StmtPass(node) } } -impl From for AnyNode { - fn from(node: StmtBreak) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtBreak) -> Self { AnyNode::StmtBreak(node) } } -impl From for AnyNode { - fn from(node: StmtContinue) -> Self { +impl From for AnyNode { + fn from(node: ast::StmtContinue) -> Self { AnyNode::StmtContinue(node) } } -impl From for AnyNode { - fn from(node: ExprBoolOp) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprBoolOp) -> Self { AnyNode::ExprBoolOp(node) } } -impl From for AnyNode { - fn from(node: ExprNamedExpr) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprNamedExpr) -> Self { AnyNode::ExprNamedExpr(node) } } -impl From for AnyNode { - fn from(node: ExprBinOp) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprBinOp) -> Self { AnyNode::ExprBinOp(node) } } -impl From for AnyNode { - fn from(node: ExprUnaryOp) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprUnaryOp) -> Self { AnyNode::ExprUnaryOp(node) } } -impl From for AnyNode { - fn from(node: ExprLambda) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprLambda) -> Self { AnyNode::ExprLambda(node) } } -impl From for AnyNode { - fn from(node: ExprIfExp) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprIfExp) -> Self { AnyNode::ExprIfExp(node) } } -impl From for AnyNode { - fn from(node: ExprDict) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprDict) -> Self { AnyNode::ExprDict(node) } } -impl From for AnyNode { - fn from(node: ExprSet) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprSet) -> Self { AnyNode::ExprSet(node) } } -impl From for AnyNode { - fn from(node: ExprListComp) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprListComp) -> Self { AnyNode::ExprListComp(node) } } -impl From for AnyNode { - fn from(node: ExprSetComp) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprSetComp) -> Self { AnyNode::ExprSetComp(node) } } -impl From for AnyNode { - fn from(node: ExprDictComp) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprDictComp) -> Self { AnyNode::ExprDictComp(node) } } -impl From for AnyNode { - fn from(node: ExprGeneratorExp) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprGeneratorExp) -> Self { AnyNode::ExprGeneratorExp(node) } } -impl From for AnyNode { - fn from(node: ExprAwait) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprAwait) -> Self { AnyNode::ExprAwait(node) } } -impl From for AnyNode { - fn from(node: ExprYield) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprYield) -> Self { AnyNode::ExprYield(node) } } -impl From for AnyNode { - fn from(node: ExprYieldFrom) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprYieldFrom) -> Self { AnyNode::ExprYieldFrom(node) } } -impl From for AnyNode { - fn from(node: ExprCompare) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprCompare) -> Self { AnyNode::ExprCompare(node) } } -impl From for AnyNode { - fn from(node: ExprCall) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprCall) -> Self { AnyNode::ExprCall(node) } } -impl From for AnyNode { - fn from(node: ExprFormattedValue) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprFormattedValue) -> Self { AnyNode::ExprFormattedValue(node) } } -impl From for AnyNode { - fn from(node: ExprJoinedStr) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprJoinedStr) -> Self { AnyNode::ExprJoinedStr(node) } } -impl From for AnyNode { - fn from(node: ExprConstant) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprConstant) -> Self { AnyNode::ExprConstant(node) } } -impl From for AnyNode { - fn from(node: ExprAttribute) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprAttribute) -> Self { AnyNode::ExprAttribute(node) } } -impl From for AnyNode { - fn from(node: ExprSubscript) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprSubscript) -> Self { AnyNode::ExprSubscript(node) } } -impl From for AnyNode { - fn from(node: ExprStarred) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprStarred) -> Self { AnyNode::ExprStarred(node) } } -impl From for AnyNode { - fn from(node: ExprName) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprName) -> Self { AnyNode::ExprName(node) } } -impl From for AnyNode { - fn from(node: ExprList) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprList) -> Self { AnyNode::ExprList(node) } } -impl From for AnyNode { - fn from(node: ExprTuple) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprTuple) -> Self { AnyNode::ExprTuple(node) } } -impl From for AnyNode { - fn from(node: ExprSlice) -> Self { +impl From for AnyNode { + fn from(node: ast::ExprSlice) -> Self { AnyNode::ExprSlice(node) } } -impl From for AnyNode { - fn from(node: ExcepthandlerExceptHandler) -> Self { - AnyNode::ExcepthandlerExceptHandler(node) +impl From for AnyNode { + fn from(node: ast::ExceptHandlerExceptHandler) -> Self { + AnyNode::ExceptHandlerExceptHandler(node) } } -impl From for AnyNode { - fn from(node: PatternMatchValue) -> Self { +impl From for AnyNode { + fn from(node: ast::PatternMatchValue) -> Self { AnyNode::PatternMatchValue(node) } } -impl From for AnyNode { - fn from(node: PatternMatchSingleton) -> Self { +impl From for AnyNode { + fn from(node: ast::PatternMatchSingleton) -> Self { AnyNode::PatternMatchSingleton(node) } } -impl From for AnyNode { - fn from(node: PatternMatchSequence) -> Self { +impl From for AnyNode { + fn from(node: ast::PatternMatchSequence) -> Self { AnyNode::PatternMatchSequence(node) } } -impl From for AnyNode { - fn from(node: PatternMatchMapping) -> Self { +impl From for AnyNode { + fn from(node: ast::PatternMatchMapping) -> Self { AnyNode::PatternMatchMapping(node) } } -impl From for AnyNode { - fn from(node: PatternMatchClass) -> Self { +impl From for AnyNode { + fn from(node: ast::PatternMatchClass) -> Self { AnyNode::PatternMatchClass(node) } } -impl From for AnyNode { - fn from(node: PatternMatchStar) -> Self { +impl From for AnyNode { + fn from(node: ast::PatternMatchStar) -> Self { AnyNode::PatternMatchStar(node) } } -impl From for AnyNode { - fn from(node: PatternMatchAs) -> Self { +impl From for AnyNode { + fn from(node: ast::PatternMatchAs) -> Self { AnyNode::PatternMatchAs(node) } } -impl From for AnyNode { - fn from(node: PatternMatchOr) -> Self { +impl From for AnyNode { + fn from(node: ast::PatternMatchOr) -> Self { AnyNode::PatternMatchOr(node) } } -impl From for AnyNode { - fn from(node: TypeIgnoreTypeIgnore) -> Self { +impl From for AnyNode { + fn from(node: ast::TypeIgnoreTypeIgnore) -> Self { AnyNode::TypeIgnoreTypeIgnore(node) } } @@ -3363,6 +3403,11 @@ impl From for AnyNode { AnyNode::Arg(node) } } +impl From for AnyNode { + fn from(node: ArgWithDefault) -> Self { + AnyNode::ArgWithDefault(node) + } +} impl From for AnyNode { fn from(node: Keyword) -> Self { AnyNode::Keyword(node) @@ -3373,9 +3418,9 @@ impl From for AnyNode { AnyNode::Alias(node) } } -impl From for AnyNode { - fn from(node: Withitem) -> Self { - AnyNode::Withitem(node) +impl From for AnyNode { + fn from(node: WithItem) -> Self { + AnyNode::WithItem(node) } } impl From for AnyNode { @@ -3450,7 +3495,7 @@ impl Ranged for AnyNode { AnyNode::ExprList(node) => node.range(), AnyNode::ExprTuple(node) => node.range(), AnyNode::ExprSlice(node) => node.range(), - AnyNode::ExcepthandlerExceptHandler(node) => node.range(), + AnyNode::ExceptHandlerExceptHandler(node) => node.range(), AnyNode::PatternMatchValue(node) => node.range(), AnyNode::PatternMatchSingleton(node) => node.range(), AnyNode::PatternMatchSequence(node) => node.range(), @@ -3463,9 +3508,10 @@ impl Ranged for AnyNode { AnyNode::Comprehension(node) => node.range(), AnyNode::Arguments(node) => node.range(), AnyNode::Arg(node) => node.range(), + AnyNode::ArgWithDefault(node) => node.range(), AnyNode::Keyword(node) => node.range(), AnyNode::Alias(node) => node.range(), - AnyNode::Withitem(node) => node.range(), + AnyNode::WithItem(node) => node.range(), AnyNode::MatchCase(node) => node.range(), AnyNode::Decorator(node) => node.range(), } @@ -3474,82 +3520,83 @@ impl Ranged for AnyNode { #[derive(Copy, Clone, Debug, is_macro::Is, PartialEq)] pub enum AnyNodeRef<'a> { - ModModule(&'a ModModule), - ModInteractive(&'a ModInteractive), - ModExpression(&'a ModExpression), - ModFunctionType(&'a ModFunctionType), - StmtFunctionDef(&'a StmtFunctionDef), - StmtAsyncFunctionDef(&'a StmtAsyncFunctionDef), - StmtClassDef(&'a StmtClassDef), - StmtReturn(&'a StmtReturn), - StmtDelete(&'a StmtDelete), - StmtAssign(&'a StmtAssign), - StmtAugAssign(&'a StmtAugAssign), - StmtAnnAssign(&'a StmtAnnAssign), - StmtFor(&'a StmtFor), - StmtAsyncFor(&'a StmtAsyncFor), - StmtWhile(&'a StmtWhile), - StmtIf(&'a StmtIf), - StmtWith(&'a StmtWith), - StmtAsyncWith(&'a StmtAsyncWith), - StmtMatch(&'a StmtMatch), - StmtRaise(&'a StmtRaise), - StmtTry(&'a StmtTry), - StmtTryStar(&'a StmtTryStar), - StmtAssert(&'a StmtAssert), - StmtImport(&'a StmtImport), - StmtImportFrom(&'a StmtImportFrom), - StmtGlobal(&'a StmtGlobal), - StmtNonlocal(&'a StmtNonlocal), - StmtExpr(&'a StmtExpr), - StmtPass(&'a StmtPass), - StmtBreak(&'a StmtBreak), - StmtContinue(&'a StmtContinue), - ExprBoolOp(&'a ExprBoolOp), - ExprNamedExpr(&'a ExprNamedExpr), - ExprBinOp(&'a ExprBinOp), - ExprUnaryOp(&'a ExprUnaryOp), - ExprLambda(&'a ExprLambda), - ExprIfExp(&'a ExprIfExp), - ExprDict(&'a ExprDict), - ExprSet(&'a ExprSet), - ExprListComp(&'a ExprListComp), - ExprSetComp(&'a ExprSetComp), - ExprDictComp(&'a ExprDictComp), - ExprGeneratorExp(&'a ExprGeneratorExp), - ExprAwait(&'a ExprAwait), - ExprYield(&'a ExprYield), - ExprYieldFrom(&'a ExprYieldFrom), - ExprCompare(&'a ExprCompare), - ExprCall(&'a ExprCall), - ExprFormattedValue(&'a ExprFormattedValue), - ExprJoinedStr(&'a ExprJoinedStr), - ExprConstant(&'a ExprConstant), - ExprAttribute(&'a ExprAttribute), - ExprSubscript(&'a ExprSubscript), - ExprStarred(&'a ExprStarred), - ExprName(&'a ExprName), - ExprList(&'a ExprList), - ExprTuple(&'a ExprTuple), - ExprSlice(&'a ExprSlice), - ExcepthandlerExceptHandler(&'a ExcepthandlerExceptHandler), - PatternMatchValue(&'a PatternMatchValue), - PatternMatchSingleton(&'a PatternMatchSingleton), - PatternMatchSequence(&'a PatternMatchSequence), - PatternMatchMapping(&'a PatternMatchMapping), - PatternMatchClass(&'a PatternMatchClass), - PatternMatchStar(&'a PatternMatchStar), - PatternMatchAs(&'a PatternMatchAs), - PatternMatchOr(&'a PatternMatchOr), - TypeIgnoreTypeIgnore(&'a TypeIgnoreTypeIgnore), - Comprehension(&'a Comprehension), - Arguments(&'a Arguments), - Arg(&'a Arg), - Keyword(&'a Keyword), - Alias(&'a Alias), - Withitem(&'a Withitem), - MatchCase(&'a MatchCase), - Decorator(&'a Decorator), + ModModule(&'a ast::ModModule), + ModInteractive(&'a ast::ModInteractive), + ModExpression(&'a ast::ModExpression), + ModFunctionType(&'a ast::ModFunctionType), + StmtFunctionDef(&'a ast::StmtFunctionDef), + StmtAsyncFunctionDef(&'a ast::StmtAsyncFunctionDef), + StmtClassDef(&'a ast::StmtClassDef), + StmtReturn(&'a ast::StmtReturn), + StmtDelete(&'a ast::StmtDelete), + StmtAssign(&'a ast::StmtAssign), + StmtAugAssign(&'a ast::StmtAugAssign), + StmtAnnAssign(&'a ast::StmtAnnAssign), + StmtFor(&'a ast::StmtFor), + StmtAsyncFor(&'a ast::StmtAsyncFor), + StmtWhile(&'a ast::StmtWhile), + StmtIf(&'a ast::StmtIf), + StmtWith(&'a ast::StmtWith), + StmtAsyncWith(&'a ast::StmtAsyncWith), + StmtMatch(&'a ast::StmtMatch), + StmtRaise(&'a ast::StmtRaise), + StmtTry(&'a ast::StmtTry), + StmtTryStar(&'a ast::StmtTryStar), + StmtAssert(&'a ast::StmtAssert), + StmtImport(&'a ast::StmtImport), + StmtImportFrom(&'a ast::StmtImportFrom), + StmtGlobal(&'a ast::StmtGlobal), + StmtNonlocal(&'a ast::StmtNonlocal), + StmtExpr(&'a ast::StmtExpr), + StmtPass(&'a ast::StmtPass), + StmtBreak(&'a ast::StmtBreak), + StmtContinue(&'a ast::StmtContinue), + ExprBoolOp(&'a ast::ExprBoolOp), + ExprNamedExpr(&'a ast::ExprNamedExpr), + ExprBinOp(&'a ast::ExprBinOp), + ExprUnaryOp(&'a ast::ExprUnaryOp), + ExprLambda(&'a ast::ExprLambda), + ExprIfExp(&'a ast::ExprIfExp), + ExprDict(&'a ast::ExprDict), + ExprSet(&'a ast::ExprSet), + ExprListComp(&'a ast::ExprListComp), + ExprSetComp(&'a ast::ExprSetComp), + ExprDictComp(&'a ast::ExprDictComp), + ExprGeneratorExp(&'a ast::ExprGeneratorExp), + ExprAwait(&'a ast::ExprAwait), + ExprYield(&'a ast::ExprYield), + ExprYieldFrom(&'a ast::ExprYieldFrom), + ExprCompare(&'a ast::ExprCompare), + ExprCall(&'a ast::ExprCall), + ExprFormattedValue(&'a ast::ExprFormattedValue), + ExprJoinedStr(&'a ast::ExprJoinedStr), + ExprConstant(&'a ast::ExprConstant), + ExprAttribute(&'a ast::ExprAttribute), + ExprSubscript(&'a ast::ExprSubscript), + ExprStarred(&'a ast::ExprStarred), + ExprName(&'a ast::ExprName), + ExprList(&'a ast::ExprList), + ExprTuple(&'a ast::ExprTuple), + ExprSlice(&'a ast::ExprSlice), + ExceptHandlerExceptHandler(&'a ast::ExceptHandlerExceptHandler), + PatternMatchValue(&'a ast::PatternMatchValue), + PatternMatchSingleton(&'a ast::PatternMatchSingleton), + PatternMatchSequence(&'a ast::PatternMatchSequence), + PatternMatchMapping(&'a ast::PatternMatchMapping), + PatternMatchClass(&'a ast::PatternMatchClass), + PatternMatchStar(&'a ast::PatternMatchStar), + PatternMatchAs(&'a ast::PatternMatchAs), + PatternMatchOr(&'a ast::PatternMatchOr), + TypeIgnoreTypeIgnore(&'a ast::TypeIgnoreTypeIgnore), + Comprehension(&'a Comprehension), + Arguments(&'a Arguments), + Arg(&'a Arg), + ArgWithDefault(&'a ArgWithDefault), + Keyword(&'a Keyword), + Alias(&'a Alias), + WithItem(&'a WithItem), + MatchCase(&'a MatchCase), + Decorator(&'a Decorator), } impl AnyNodeRef<'_> { @@ -3613,7 +3660,7 @@ impl AnyNodeRef<'_> { AnyNodeRef::ExprList(node) => NonNull::from(*node).cast(), AnyNodeRef::ExprTuple(node) => NonNull::from(*node).cast(), AnyNodeRef::ExprSlice(node) => NonNull::from(*node).cast(), - AnyNodeRef::ExcepthandlerExceptHandler(node) => NonNull::from(*node).cast(), + AnyNodeRef::ExceptHandlerExceptHandler(node) => NonNull::from(*node).cast(), AnyNodeRef::PatternMatchValue(node) => NonNull::from(*node).cast(), AnyNodeRef::PatternMatchSingleton(node) => NonNull::from(*node).cast(), AnyNodeRef::PatternMatchSequence(node) => NonNull::from(*node).cast(), @@ -3626,9 +3673,10 @@ impl AnyNodeRef<'_> { AnyNodeRef::Comprehension(node) => NonNull::from(*node).cast(), AnyNodeRef::Arguments(node) => NonNull::from(*node).cast(), AnyNodeRef::Arg(node) => NonNull::from(*node).cast(), + AnyNodeRef::ArgWithDefault(node) => NonNull::from(*node).cast(), AnyNodeRef::Keyword(node) => NonNull::from(*node).cast(), AnyNodeRef::Alias(node) => NonNull::from(*node).cast(), - AnyNodeRef::Withitem(node) => NonNull::from(*node).cast(), + AnyNodeRef::WithItem(node) => NonNull::from(*node).cast(), AnyNodeRef::MatchCase(node) => NonNull::from(*node).cast(), AnyNodeRef::Decorator(node) => NonNull::from(*node).cast(), } @@ -3636,7 +3684,7 @@ impl AnyNodeRef<'_> { /// Compares two any node refs by their pointers (referential equality). pub fn ptr_eq(self, other: AnyNodeRef) -> bool { - self.as_ptr().eq(&other.as_ptr()) + self.as_ptr().eq(&other.as_ptr()) && self.kind() == other.kind() } /// Returns the node's [`kind`](NodeKind) that has no data associated and is [`Copy`]. @@ -3700,7 +3748,7 @@ impl AnyNodeRef<'_> { AnyNodeRef::ExprList(_) => NodeKind::ExprList, AnyNodeRef::ExprTuple(_) => NodeKind::ExprTuple, AnyNodeRef::ExprSlice(_) => NodeKind::ExprSlice, - AnyNodeRef::ExcepthandlerExceptHandler(_) => NodeKind::ExcepthandlerExceptHandler, + AnyNodeRef::ExceptHandlerExceptHandler(_) => NodeKind::ExceptHandlerExceptHandler, AnyNodeRef::PatternMatchValue(_) => NodeKind::PatternMatchValue, AnyNodeRef::PatternMatchSingleton(_) => NodeKind::PatternMatchSingleton, AnyNodeRef::PatternMatchSequence(_) => NodeKind::PatternMatchSequence, @@ -3713,9 +3761,10 @@ impl AnyNodeRef<'_> { AnyNodeRef::Comprehension(_) => NodeKind::Comprehension, AnyNodeRef::Arguments(_) => NodeKind::Arguments, AnyNodeRef::Arg(_) => NodeKind::Arg, + AnyNodeRef::ArgWithDefault(_) => NodeKind::ArgWithDefault, AnyNodeRef::Keyword(_) => NodeKind::Keyword, AnyNodeRef::Alias(_) => NodeKind::Alias, - AnyNodeRef::Withitem(_) => NodeKind::Withitem, + AnyNodeRef::WithItem(_) => NodeKind::WithItem, AnyNodeRef::MatchCase(_) => NodeKind::MatchCase, AnyNodeRef::Decorator(_) => NodeKind::Decorator, } @@ -3782,7 +3831,7 @@ impl AnyNodeRef<'_> { | AnyNodeRef::ExprList(_) | AnyNodeRef::ExprTuple(_) | AnyNodeRef::ExprSlice(_) - | AnyNodeRef::ExcepthandlerExceptHandler(_) + | AnyNodeRef::ExceptHandlerExceptHandler(_) | AnyNodeRef::PatternMatchValue(_) | AnyNodeRef::PatternMatchSingleton(_) | AnyNodeRef::PatternMatchSequence(_) @@ -3795,9 +3844,10 @@ impl AnyNodeRef<'_> { | AnyNodeRef::Comprehension(_) | AnyNodeRef::Arguments(_) | AnyNodeRef::Arg(_) + | AnyNodeRef::ArgWithDefault(_) | AnyNodeRef::Keyword(_) | AnyNodeRef::Alias(_) - | AnyNodeRef::Withitem(_) + | AnyNodeRef::WithItem(_) | AnyNodeRef::MatchCase(_) | AnyNodeRef::Decorator(_) => false, } @@ -3864,7 +3914,7 @@ impl AnyNodeRef<'_> { | AnyNodeRef::StmtPass(_) | AnyNodeRef::StmtBreak(_) | AnyNodeRef::StmtContinue(_) - | AnyNodeRef::ExcepthandlerExceptHandler(_) + | AnyNodeRef::ExceptHandlerExceptHandler(_) | AnyNodeRef::PatternMatchValue(_) | AnyNodeRef::PatternMatchSingleton(_) | AnyNodeRef::PatternMatchSequence(_) @@ -3877,9 +3927,10 @@ impl AnyNodeRef<'_> { | AnyNodeRef::Comprehension(_) | AnyNodeRef::Arguments(_) | AnyNodeRef::Arg(_) + | AnyNodeRef::ArgWithDefault(_) | AnyNodeRef::Keyword(_) | AnyNodeRef::Alias(_) - | AnyNodeRef::Withitem(_) + | AnyNodeRef::WithItem(_) | AnyNodeRef::MatchCase(_) | AnyNodeRef::Decorator(_) => false, } @@ -3946,7 +3997,7 @@ impl AnyNodeRef<'_> { | AnyNodeRef::ExprList(_) | AnyNodeRef::ExprTuple(_) | AnyNodeRef::ExprSlice(_) - | AnyNodeRef::ExcepthandlerExceptHandler(_) + | AnyNodeRef::ExceptHandlerExceptHandler(_) | AnyNodeRef::PatternMatchValue(_) | AnyNodeRef::PatternMatchSingleton(_) | AnyNodeRef::PatternMatchSequence(_) @@ -3959,9 +4010,10 @@ impl AnyNodeRef<'_> { | AnyNodeRef::Comprehension(_) | AnyNodeRef::Arguments(_) | AnyNodeRef::Arg(_) + | AnyNodeRef::ArgWithDefault(_) | AnyNodeRef::Keyword(_) | AnyNodeRef::Alias(_) - | AnyNodeRef::Withitem(_) + | AnyNodeRef::WithItem(_) | AnyNodeRef::MatchCase(_) | AnyNodeRef::Decorator(_) => false, } @@ -4036,14 +4088,15 @@ impl AnyNodeRef<'_> { | AnyNodeRef::ExprList(_) | AnyNodeRef::ExprTuple(_) | AnyNodeRef::ExprSlice(_) - | AnyNodeRef::ExcepthandlerExceptHandler(_) + | AnyNodeRef::ExceptHandlerExceptHandler(_) | AnyNodeRef::TypeIgnoreTypeIgnore(_) | AnyNodeRef::Comprehension(_) | AnyNodeRef::Arguments(_) | AnyNodeRef::Arg(_) + | AnyNodeRef::ArgWithDefault(_) | AnyNodeRef::Keyword(_) | AnyNodeRef::Alias(_) - | AnyNodeRef::Withitem(_) + | AnyNodeRef::WithItem(_) | AnyNodeRef::MatchCase(_) | AnyNodeRef::Decorator(_) => false, } @@ -4051,7 +4104,7 @@ impl AnyNodeRef<'_> { pub const fn is_except_handler(self) -> bool { match self { - AnyNodeRef::ExcepthandlerExceptHandler(_) => true, + AnyNodeRef::ExceptHandlerExceptHandler(_) => true, AnyNodeRef::ModModule(_) | AnyNodeRef::ModInteractive(_) @@ -4123,9 +4176,10 @@ impl AnyNodeRef<'_> { | AnyNodeRef::Comprehension(_) | AnyNodeRef::Arguments(_) | AnyNodeRef::Arg(_) + | AnyNodeRef::ArgWithDefault(_) | AnyNodeRef::Keyword(_) | AnyNodeRef::Alias(_) - | AnyNodeRef::Withitem(_) + | AnyNodeRef::WithItem(_) | AnyNodeRef::MatchCase(_) | AnyNodeRef::Decorator(_) => false, } @@ -4201,423 +4255,442 @@ impl AnyNodeRef<'_> { | AnyNodeRef::PatternMatchStar(_) | AnyNodeRef::PatternMatchAs(_) | AnyNodeRef::PatternMatchOr(_) - | AnyNodeRef::ExcepthandlerExceptHandler(_) + | AnyNodeRef::ExceptHandlerExceptHandler(_) | AnyNodeRef::Comprehension(_) | AnyNodeRef::Arguments(_) | AnyNodeRef::Arg(_) + | AnyNodeRef::ArgWithDefault(_) | AnyNodeRef::Keyword(_) | AnyNodeRef::Alias(_) - | AnyNodeRef::Withitem(_) + | AnyNodeRef::WithItem(_) | AnyNodeRef::MatchCase(_) | AnyNodeRef::Decorator(_) => false, } } + + pub const fn is_node_with_body(self) -> bool { + matches!( + self, + AnyNodeRef::StmtIf(_) + | AnyNodeRef::StmtFor(_) + | AnyNodeRef::StmtAsyncFor(_) + | AnyNodeRef::StmtWhile(_) + | AnyNodeRef::StmtWith(_) + | AnyNodeRef::StmtAsyncWith(_) + | AnyNodeRef::StmtMatch(_) + | AnyNodeRef::StmtFunctionDef(_) + | AnyNodeRef::StmtAsyncFunctionDef(_) + | AnyNodeRef::StmtClassDef(_) + | AnyNodeRef::StmtTry(_) + | AnyNodeRef::StmtTryStar(_) + ) + } } -impl<'a> From<&'a ModModule> for AnyNodeRef<'a> { - fn from(node: &'a ModModule) -> Self { +impl<'a> From<&'a ast::ModModule> for AnyNodeRef<'a> { + fn from(node: &'a ast::ModModule) -> Self { AnyNodeRef::ModModule(node) } } -impl<'a> From<&'a ModInteractive> for AnyNodeRef<'a> { - fn from(node: &'a ModInteractive) -> Self { +impl<'a> From<&'a ast::ModInteractive> for AnyNodeRef<'a> { + fn from(node: &'a ast::ModInteractive) -> Self { AnyNodeRef::ModInteractive(node) } } -impl<'a> From<&'a ModExpression> for AnyNodeRef<'a> { - fn from(node: &'a ModExpression) -> Self { +impl<'a> From<&'a ast::ModExpression> for AnyNodeRef<'a> { + fn from(node: &'a ast::ModExpression) -> Self { AnyNodeRef::ModExpression(node) } } -impl<'a> From<&'a ModFunctionType> for AnyNodeRef<'a> { - fn from(node: &'a ModFunctionType) -> Self { +impl<'a> From<&'a ast::ModFunctionType> for AnyNodeRef<'a> { + fn from(node: &'a ast::ModFunctionType) -> Self { AnyNodeRef::ModFunctionType(node) } } -impl<'a> From<&'a StmtFunctionDef> for AnyNodeRef<'a> { - fn from(node: &'a StmtFunctionDef) -> Self { +impl<'a> From<&'a ast::StmtFunctionDef> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtFunctionDef) -> Self { AnyNodeRef::StmtFunctionDef(node) } } -impl<'a> From<&'a StmtAsyncFunctionDef> for AnyNodeRef<'a> { - fn from(node: &'a StmtAsyncFunctionDef) -> Self { +impl<'a> From<&'a ast::StmtAsyncFunctionDef> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtAsyncFunctionDef) -> Self { AnyNodeRef::StmtAsyncFunctionDef(node) } } -impl<'a> From<&'a StmtClassDef> for AnyNodeRef<'a> { - fn from(node: &'a StmtClassDef) -> Self { +impl<'a> From<&'a ast::StmtClassDef> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtClassDef) -> Self { AnyNodeRef::StmtClassDef(node) } } -impl<'a> From<&'a StmtReturn> for AnyNodeRef<'a> { - fn from(node: &'a StmtReturn) -> Self { +impl<'a> From<&'a ast::StmtReturn> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtReturn) -> Self { AnyNodeRef::StmtReturn(node) } } -impl<'a> From<&'a StmtDelete> for AnyNodeRef<'a> { - fn from(node: &'a StmtDelete) -> Self { +impl<'a> From<&'a ast::StmtDelete> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtDelete) -> Self { AnyNodeRef::StmtDelete(node) } } -impl<'a> From<&'a StmtAssign> for AnyNodeRef<'a> { - fn from(node: &'a StmtAssign) -> Self { +impl<'a> From<&'a ast::StmtAssign> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtAssign) -> Self { AnyNodeRef::StmtAssign(node) } } -impl<'a> From<&'a StmtAugAssign> for AnyNodeRef<'a> { - fn from(node: &'a StmtAugAssign) -> Self { +impl<'a> From<&'a ast::StmtAugAssign> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtAugAssign) -> Self { AnyNodeRef::StmtAugAssign(node) } } -impl<'a> From<&'a StmtAnnAssign> for AnyNodeRef<'a> { - fn from(node: &'a StmtAnnAssign) -> Self { +impl<'a> From<&'a ast::StmtAnnAssign> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtAnnAssign) -> Self { AnyNodeRef::StmtAnnAssign(node) } } -impl<'a> From<&'a StmtFor> for AnyNodeRef<'a> { - fn from(node: &'a StmtFor) -> Self { +impl<'a> From<&'a ast::StmtFor> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtFor) -> Self { AnyNodeRef::StmtFor(node) } } -impl<'a> From<&'a StmtAsyncFor> for AnyNodeRef<'a> { - fn from(node: &'a StmtAsyncFor) -> Self { +impl<'a> From<&'a ast::StmtAsyncFor> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtAsyncFor) -> Self { AnyNodeRef::StmtAsyncFor(node) } } -impl<'a> From<&'a StmtWhile> for AnyNodeRef<'a> { - fn from(node: &'a StmtWhile) -> Self { +impl<'a> From<&'a ast::StmtWhile> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtWhile) -> Self { AnyNodeRef::StmtWhile(node) } } -impl<'a> From<&'a StmtIf> for AnyNodeRef<'a> { - fn from(node: &'a StmtIf) -> Self { +impl<'a> From<&'a ast::StmtIf> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtIf) -> Self { AnyNodeRef::StmtIf(node) } } -impl<'a> From<&'a StmtWith> for AnyNodeRef<'a> { - fn from(node: &'a StmtWith) -> Self { +impl<'a> From<&'a ast::StmtWith> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtWith) -> Self { AnyNodeRef::StmtWith(node) } } -impl<'a> From<&'a StmtAsyncWith> for AnyNodeRef<'a> { - fn from(node: &'a StmtAsyncWith) -> Self { +impl<'a> From<&'a ast::StmtAsyncWith> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtAsyncWith) -> Self { AnyNodeRef::StmtAsyncWith(node) } } -impl<'a> From<&'a StmtMatch> for AnyNodeRef<'a> { - fn from(node: &'a StmtMatch) -> Self { +impl<'a> From<&'a ast::StmtMatch> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtMatch) -> Self { AnyNodeRef::StmtMatch(node) } } -impl<'a> From<&'a StmtRaise> for AnyNodeRef<'a> { - fn from(node: &'a StmtRaise) -> Self { +impl<'a> From<&'a ast::StmtRaise> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtRaise) -> Self { AnyNodeRef::StmtRaise(node) } } -impl<'a> From<&'a StmtTry> for AnyNodeRef<'a> { - fn from(node: &'a StmtTry) -> Self { +impl<'a> From<&'a ast::StmtTry> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtTry) -> Self { AnyNodeRef::StmtTry(node) } } -impl<'a> From<&'a StmtTryStar> for AnyNodeRef<'a> { - fn from(node: &'a StmtTryStar) -> Self { +impl<'a> From<&'a ast::StmtTryStar> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtTryStar) -> Self { AnyNodeRef::StmtTryStar(node) } } -impl<'a> From<&'a StmtAssert> for AnyNodeRef<'a> { - fn from(node: &'a StmtAssert) -> Self { +impl<'a> From<&'a ast::StmtAssert> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtAssert) -> Self { AnyNodeRef::StmtAssert(node) } } -impl<'a> From<&'a StmtImport> for AnyNodeRef<'a> { - fn from(node: &'a StmtImport) -> Self { +impl<'a> From<&'a ast::StmtImport> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtImport) -> Self { AnyNodeRef::StmtImport(node) } } -impl<'a> From<&'a StmtImportFrom> for AnyNodeRef<'a> { - fn from(node: &'a StmtImportFrom) -> Self { +impl<'a> From<&'a ast::StmtImportFrom> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtImportFrom) -> Self { AnyNodeRef::StmtImportFrom(node) } } -impl<'a> From<&'a StmtGlobal> for AnyNodeRef<'a> { - fn from(node: &'a StmtGlobal) -> Self { +impl<'a> From<&'a ast::StmtGlobal> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtGlobal) -> Self { AnyNodeRef::StmtGlobal(node) } } -impl<'a> From<&'a StmtNonlocal> for AnyNodeRef<'a> { - fn from(node: &'a StmtNonlocal) -> Self { +impl<'a> From<&'a ast::StmtNonlocal> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtNonlocal) -> Self { AnyNodeRef::StmtNonlocal(node) } } -impl<'a> From<&'a StmtExpr> for AnyNodeRef<'a> { - fn from(node: &'a StmtExpr) -> Self { +impl<'a> From<&'a ast::StmtExpr> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtExpr) -> Self { AnyNodeRef::StmtExpr(node) } } -impl<'a> From<&'a StmtPass> for AnyNodeRef<'a> { - fn from(node: &'a StmtPass) -> Self { +impl<'a> From<&'a ast::StmtPass> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtPass) -> Self { AnyNodeRef::StmtPass(node) } } -impl<'a> From<&'a StmtBreak> for AnyNodeRef<'a> { - fn from(node: &'a StmtBreak) -> Self { +impl<'a> From<&'a ast::StmtBreak> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtBreak) -> Self { AnyNodeRef::StmtBreak(node) } } -impl<'a> From<&'a StmtContinue> for AnyNodeRef<'a> { - fn from(node: &'a StmtContinue) -> Self { +impl<'a> From<&'a ast::StmtContinue> for AnyNodeRef<'a> { + fn from(node: &'a ast::StmtContinue) -> Self { AnyNodeRef::StmtContinue(node) } } -impl<'a> From<&'a ExprBoolOp> for AnyNodeRef<'a> { - fn from(node: &'a ExprBoolOp) -> Self { +impl<'a> From<&'a ast::ExprBoolOp> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprBoolOp) -> Self { AnyNodeRef::ExprBoolOp(node) } } -impl<'a> From<&'a ExprNamedExpr> for AnyNodeRef<'a> { - fn from(node: &'a ExprNamedExpr) -> Self { +impl<'a> From<&'a ast::ExprNamedExpr> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprNamedExpr) -> Self { AnyNodeRef::ExprNamedExpr(node) } } -impl<'a> From<&'a ExprBinOp> for AnyNodeRef<'a> { - fn from(node: &'a ExprBinOp) -> Self { +impl<'a> From<&'a ast::ExprBinOp> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprBinOp) -> Self { AnyNodeRef::ExprBinOp(node) } } -impl<'a> From<&'a ExprUnaryOp> for AnyNodeRef<'a> { - fn from(node: &'a ExprUnaryOp) -> Self { +impl<'a> From<&'a ast::ExprUnaryOp> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprUnaryOp) -> Self { AnyNodeRef::ExprUnaryOp(node) } } -impl<'a> From<&'a ExprLambda> for AnyNodeRef<'a> { - fn from(node: &'a ExprLambda) -> Self { +impl<'a> From<&'a ast::ExprLambda> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprLambda) -> Self { AnyNodeRef::ExprLambda(node) } } -impl<'a> From<&'a ExprIfExp> for AnyNodeRef<'a> { - fn from(node: &'a ExprIfExp) -> Self { +impl<'a> From<&'a ast::ExprIfExp> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprIfExp) -> Self { AnyNodeRef::ExprIfExp(node) } } -impl<'a> From<&'a ExprDict> for AnyNodeRef<'a> { - fn from(node: &'a ExprDict) -> Self { +impl<'a> From<&'a ast::ExprDict> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprDict) -> Self { AnyNodeRef::ExprDict(node) } } -impl<'a> From<&'a ExprSet> for AnyNodeRef<'a> { - fn from(node: &'a ExprSet) -> Self { +impl<'a> From<&'a ast::ExprSet> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprSet) -> Self { AnyNodeRef::ExprSet(node) } } -impl<'a> From<&'a ExprListComp> for AnyNodeRef<'a> { - fn from(node: &'a ExprListComp) -> Self { +impl<'a> From<&'a ast::ExprListComp> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprListComp) -> Self { AnyNodeRef::ExprListComp(node) } } -impl<'a> From<&'a ExprSetComp> for AnyNodeRef<'a> { - fn from(node: &'a ExprSetComp) -> Self { +impl<'a> From<&'a ast::ExprSetComp> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprSetComp) -> Self { AnyNodeRef::ExprSetComp(node) } } -impl<'a> From<&'a ExprDictComp> for AnyNodeRef<'a> { - fn from(node: &'a ExprDictComp) -> Self { +impl<'a> From<&'a ast::ExprDictComp> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprDictComp) -> Self { AnyNodeRef::ExprDictComp(node) } } -impl<'a> From<&'a ExprGeneratorExp> for AnyNodeRef<'a> { - fn from(node: &'a ExprGeneratorExp) -> Self { +impl<'a> From<&'a ast::ExprGeneratorExp> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprGeneratorExp) -> Self { AnyNodeRef::ExprGeneratorExp(node) } } -impl<'a> From<&'a ExprAwait> for AnyNodeRef<'a> { - fn from(node: &'a ExprAwait) -> Self { +impl<'a> From<&'a ast::ExprAwait> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprAwait) -> Self { AnyNodeRef::ExprAwait(node) } } -impl<'a> From<&'a ExprYield> for AnyNodeRef<'a> { - fn from(node: &'a ExprYield) -> Self { +impl<'a> From<&'a ast::ExprYield> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprYield) -> Self { AnyNodeRef::ExprYield(node) } } -impl<'a> From<&'a ExprYieldFrom> for AnyNodeRef<'a> { - fn from(node: &'a ExprYieldFrom) -> Self { +impl<'a> From<&'a ast::ExprYieldFrom> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprYieldFrom) -> Self { AnyNodeRef::ExprYieldFrom(node) } } -impl<'a> From<&'a ExprCompare> for AnyNodeRef<'a> { - fn from(node: &'a ExprCompare) -> Self { +impl<'a> From<&'a ast::ExprCompare> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprCompare) -> Self { AnyNodeRef::ExprCompare(node) } } -impl<'a> From<&'a ExprCall> for AnyNodeRef<'a> { - fn from(node: &'a ExprCall) -> Self { +impl<'a> From<&'a ast::ExprCall> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprCall) -> Self { AnyNodeRef::ExprCall(node) } } -impl<'a> From<&'a ExprFormattedValue> for AnyNodeRef<'a> { - fn from(node: &'a ExprFormattedValue) -> Self { +impl<'a> From<&'a ast::ExprFormattedValue> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprFormattedValue) -> Self { AnyNodeRef::ExprFormattedValue(node) } } -impl<'a> From<&'a ExprJoinedStr> for AnyNodeRef<'a> { - fn from(node: &'a ExprJoinedStr) -> Self { +impl<'a> From<&'a ast::ExprJoinedStr> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprJoinedStr) -> Self { AnyNodeRef::ExprJoinedStr(node) } } -impl<'a> From<&'a ExprConstant> for AnyNodeRef<'a> { - fn from(node: &'a ExprConstant) -> Self { +impl<'a> From<&'a ast::ExprConstant> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprConstant) -> Self { AnyNodeRef::ExprConstant(node) } } -impl<'a> From<&'a ExprAttribute> for AnyNodeRef<'a> { - fn from(node: &'a ExprAttribute) -> Self { +impl<'a> From<&'a ast::ExprAttribute> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprAttribute) -> Self { AnyNodeRef::ExprAttribute(node) } } -impl<'a> From<&'a ExprSubscript> for AnyNodeRef<'a> { - fn from(node: &'a ExprSubscript) -> Self { +impl<'a> From<&'a ast::ExprSubscript> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprSubscript) -> Self { AnyNodeRef::ExprSubscript(node) } } -impl<'a> From<&'a ExprStarred> for AnyNodeRef<'a> { - fn from(node: &'a ExprStarred) -> Self { +impl<'a> From<&'a ast::ExprStarred> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprStarred) -> Self { AnyNodeRef::ExprStarred(node) } } -impl<'a> From<&'a ExprName> for AnyNodeRef<'a> { - fn from(node: &'a ExprName) -> Self { +impl<'a> From<&'a ast::ExprName> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprName) -> Self { AnyNodeRef::ExprName(node) } } -impl<'a> From<&'a ExprList> for AnyNodeRef<'a> { - fn from(node: &'a ExprList) -> Self { +impl<'a> From<&'a ast::ExprList> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprList) -> Self { AnyNodeRef::ExprList(node) } } -impl<'a> From<&'a ExprTuple> for AnyNodeRef<'a> { - fn from(node: &'a ExprTuple) -> Self { +impl<'a> From<&'a ast::ExprTuple> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprTuple) -> Self { AnyNodeRef::ExprTuple(node) } } -impl<'a> From<&'a ExprSlice> for AnyNodeRef<'a> { - fn from(node: &'a ExprSlice) -> Self { +impl<'a> From<&'a ast::ExprSlice> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExprSlice) -> Self { AnyNodeRef::ExprSlice(node) } } -impl<'a> From<&'a ExcepthandlerExceptHandler> for AnyNodeRef<'a> { - fn from(node: &'a ExcepthandlerExceptHandler) -> Self { - AnyNodeRef::ExcepthandlerExceptHandler(node) +impl<'a> From<&'a ast::ExceptHandlerExceptHandler> for AnyNodeRef<'a> { + fn from(node: &'a ast::ExceptHandlerExceptHandler) -> Self { + AnyNodeRef::ExceptHandlerExceptHandler(node) } } -impl<'a> From<&'a PatternMatchValue> for AnyNodeRef<'a> { - fn from(node: &'a PatternMatchValue) -> Self { +impl<'a> From<&'a ast::PatternMatchValue> for AnyNodeRef<'a> { + fn from(node: &'a ast::PatternMatchValue) -> Self { AnyNodeRef::PatternMatchValue(node) } } -impl<'a> From<&'a PatternMatchSingleton> for AnyNodeRef<'a> { - fn from(node: &'a PatternMatchSingleton) -> Self { +impl<'a> From<&'a ast::PatternMatchSingleton> for AnyNodeRef<'a> { + fn from(node: &'a ast::PatternMatchSingleton) -> Self { AnyNodeRef::PatternMatchSingleton(node) } } -impl<'a> From<&'a PatternMatchSequence> for AnyNodeRef<'a> { - fn from(node: &'a PatternMatchSequence) -> Self { +impl<'a> From<&'a ast::PatternMatchSequence> for AnyNodeRef<'a> { + fn from(node: &'a ast::PatternMatchSequence) -> Self { AnyNodeRef::PatternMatchSequence(node) } } -impl<'a> From<&'a PatternMatchMapping> for AnyNodeRef<'a> { - fn from(node: &'a PatternMatchMapping) -> Self { +impl<'a> From<&'a ast::PatternMatchMapping> for AnyNodeRef<'a> { + fn from(node: &'a ast::PatternMatchMapping) -> Self { AnyNodeRef::PatternMatchMapping(node) } } -impl<'a> From<&'a PatternMatchClass> for AnyNodeRef<'a> { - fn from(node: &'a PatternMatchClass) -> Self { +impl<'a> From<&'a ast::PatternMatchClass> for AnyNodeRef<'a> { + fn from(node: &'a ast::PatternMatchClass) -> Self { AnyNodeRef::PatternMatchClass(node) } } -impl<'a> From<&'a PatternMatchStar> for AnyNodeRef<'a> { - fn from(node: &'a PatternMatchStar) -> Self { +impl<'a> From<&'a ast::PatternMatchStar> for AnyNodeRef<'a> { + fn from(node: &'a ast::PatternMatchStar) -> Self { AnyNodeRef::PatternMatchStar(node) } } -impl<'a> From<&'a PatternMatchAs> for AnyNodeRef<'a> { - fn from(node: &'a PatternMatchAs) -> Self { +impl<'a> From<&'a ast::PatternMatchAs> for AnyNodeRef<'a> { + fn from(node: &'a ast::PatternMatchAs) -> Self { AnyNodeRef::PatternMatchAs(node) } } -impl<'a> From<&'a PatternMatchOr> for AnyNodeRef<'a> { - fn from(node: &'a PatternMatchOr) -> Self { +impl<'a> From<&'a ast::PatternMatchOr> for AnyNodeRef<'a> { + fn from(node: &'a ast::PatternMatchOr) -> Self { AnyNodeRef::PatternMatchOr(node) } } -impl<'a> From<&'a TypeIgnoreTypeIgnore> for AnyNodeRef<'a> { - fn from(node: &'a TypeIgnoreTypeIgnore) -> Self { +impl<'a> From<&'a ast::TypeIgnoreTypeIgnore> for AnyNodeRef<'a> { + fn from(node: &'a ast::TypeIgnoreTypeIgnore) -> Self { AnyNodeRef::TypeIgnoreTypeIgnore(node) } } @@ -4722,11 +4795,11 @@ impl<'a> From<&'a Pattern> for AnyNodeRef<'a> { } } -impl<'a> From<&'a Excepthandler> for AnyNodeRef<'a> { - fn from(handler: &'a Excepthandler) -> Self { +impl<'a> From<&'a ExceptHandler> for AnyNodeRef<'a> { + fn from(handler: &'a ExceptHandler) -> Self { match handler { - Excepthandler::ExceptHandler(handler) => { - AnyNodeRef::ExcepthandlerExceptHandler(handler) + ExceptHandler::ExceptHandler(handler) => { + AnyNodeRef::ExceptHandlerExceptHandler(handler) } } } @@ -4755,6 +4828,11 @@ impl<'a> From<&'a Arg> for AnyNodeRef<'a> { AnyNodeRef::Arg(node) } } +impl<'a> From<&'a ArgWithDefault> for AnyNodeRef<'a> { + fn from(node: &'a ArgWithDefault) -> Self { + AnyNodeRef::ArgWithDefault(node) + } +} impl<'a> From<&'a Keyword> for AnyNodeRef<'a> { fn from(node: &'a Keyword) -> Self { AnyNodeRef::Keyword(node) @@ -4765,9 +4843,9 @@ impl<'a> From<&'a Alias> for AnyNodeRef<'a> { AnyNodeRef::Alias(node) } } -impl<'a> From<&'a Withitem> for AnyNodeRef<'a> { - fn from(node: &'a Withitem) -> Self { - AnyNodeRef::Withitem(node) +impl<'a> From<&'a WithItem> for AnyNodeRef<'a> { + fn from(node: &'a WithItem) -> Self { + AnyNodeRef::WithItem(node) } } impl<'a> From<&'a MatchCase> for AnyNodeRef<'a> { @@ -4837,7 +4915,7 @@ impl Ranged for AnyNodeRef<'_> { AnyNodeRef::ExprList(node) => node.range(), AnyNodeRef::ExprTuple(node) => node.range(), AnyNodeRef::ExprSlice(node) => node.range(), - AnyNodeRef::ExcepthandlerExceptHandler(node) => node.range(), + AnyNodeRef::ExceptHandlerExceptHandler(node) => node.range(), AnyNodeRef::PatternMatchValue(node) => node.range(), AnyNodeRef::PatternMatchSingleton(node) => node.range(), AnyNodeRef::PatternMatchSequence(node) => node.range(), @@ -4850,9 +4928,10 @@ impl Ranged for AnyNodeRef<'_> { AnyNodeRef::Comprehension(node) => node.range(), AnyNodeRef::Arguments(node) => node.range(), AnyNodeRef::Arg(node) => node.range(), + AnyNodeRef::ArgWithDefault(node) => node.range(), AnyNodeRef::Keyword(node) => node.range(), AnyNodeRef::Alias(node) => node.range(), - AnyNodeRef::Withitem(node) => node.range(), + AnyNodeRef::WithItem(node) => node.range(), AnyNodeRef::MatchCase(node) => node.range(), AnyNodeRef::Decorator(node) => node.range(), } @@ -4919,7 +4998,7 @@ pub enum NodeKind { ExprList, ExprTuple, ExprSlice, - ExcepthandlerExceptHandler, + ExceptHandlerExceptHandler, PatternMatchValue, PatternMatchSingleton, PatternMatchSequence, @@ -4932,9 +5011,10 @@ pub enum NodeKind { Comprehension, Arguments, Arg, + ArgWithDefault, Keyword, Alias, - Withitem, + WithItem, MatchCase, Decorator, } diff --git a/crates/ruff_python_ast/src/prelude.rs b/crates/ruff_python_ast/src/prelude.rs deleted file mode 100644 index 76505ecd01..0000000000 --- a/crates/ruff_python_ast/src/prelude.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub use crate::node::AstNode; -pub use rustpython_ast::*; diff --git a/crates/ruff_python_ast/src/source_code/comment_ranges.rs b/crates/ruff_python_ast/src/source_code/comment_ranges.rs index cbbb414b54..189addc418 100644 --- a/crates/ruff_python_ast/src/source_code/comment_ranges.rs +++ b/crates/ruff_python_ast/src/source_code/comment_ranges.rs @@ -25,8 +25,8 @@ impl Debug for CommentRanges { } impl<'a> IntoIterator for &'a CommentRanges { - type Item = &'a TextRange; type IntoIter = std::slice::Iter<'a, TextRange>; + type Item = &'a TextRange; fn into_iter(self) -> Self::IntoIter { self.raw.iter() diff --git a/crates/ruff_python_ast/src/source_code/generator.rs b/crates/ruff_python_ast/src/source_code/generator.rs index fbdedb4bcc..e406122e2e 100644 --- a/crates/ruff_python_ast/src/source_code/generator.rs +++ b/crates/ruff_python_ast/src/source_code/generator.rs @@ -1,11 +1,12 @@ //! Generate Python source code from an abstract syntax tree (AST). +use rustpython_ast::ArgWithDefault; use std::ops::Deref; use rustpython_literal::escape::{AsciiEscape, Escape, UnicodeEscape}; use rustpython_parser::ast::{ - self, Alias, Arg, Arguments, Boolop, Cmpop, Comprehension, Constant, ConversionFlag, - Excepthandler, Expr, Identifier, MatchCase, Operator, Pattern, Stmt, Suite, Withitem, + self, Alias, Arg, Arguments, BoolOp, CmpOp, Comprehension, Constant, ConversionFlag, + ExceptHandler, Expr, Identifier, MatchCase, Operator, Pattern, Stmt, Suite, WithItem, }; use ruff_python_whitespace::LineEnding; @@ -131,7 +132,7 @@ impl<'a> Generator<'a> { } } - fn body(&mut self, stmts: &[Stmt]) { + fn body(&mut self, stmts: &[Stmt]) { self.indent_depth = self.indent_depth.saturating_add(1); for stmt in stmts { self.unparse_stmt(stmt); @@ -183,13 +184,13 @@ impl<'a> Generator<'a> { self.buffer } - pub(crate) fn unparse_suite(&mut self, suite: &Suite) { + pub fn unparse_suite(&mut self, suite: &Suite) { for stmt in suite { self.unparse_stmt(stmt); } } - pub(crate) fn unparse_stmt(&mut self, ast: &Stmt) { + pub(crate) fn unparse_stmt(&mut self, ast: &Stmt) { macro_rules! statement { ($body:block) => {{ self.newline(); @@ -466,7 +467,7 @@ impl<'a> Generator<'a> { }); self.body(body); - let mut orelse_: &[Stmt] = orelse; + let mut orelse_: &[Stmt] = orelse; loop { if orelse_.len() == 1 && matches!(orelse_[0], Stmt::If(_)) { if let Stmt::If(ast::StmtIf { @@ -501,7 +502,7 @@ impl<'a> Generator<'a> { let mut first = true; for item in items { self.p_delim(&mut first, ", "); - self.unparse_withitem(item); + self.unparse_with_item(item); } self.p(":"); }); @@ -513,7 +514,7 @@ impl<'a> Generator<'a> { let mut first = true; for item in items { self.p_delim(&mut first, ", "); - self.unparse_withitem(item); + self.unparse_with_item(item); } self.p(":"); }); @@ -568,7 +569,7 @@ impl<'a> Generator<'a> { for handler in handlers { statement!({ - self.unparse_excepthandler(handler, false); + self.unparse_except_handler(handler, false); }); } @@ -599,7 +600,7 @@ impl<'a> Generator<'a> { for handler in handlers { statement!({ - self.unparse_excepthandler(handler, true); + self.unparse_except_handler(handler, true); }); } @@ -717,9 +718,9 @@ impl<'a> Generator<'a> { } } - fn unparse_excepthandler(&mut self, ast: &Excepthandler, star: bool) { + fn unparse_except_handler(&mut self, ast: &ExceptHandler, star: bool) { match ast { - Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { + ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { type_, name, body, @@ -743,7 +744,7 @@ impl<'a> Generator<'a> { } } - fn unparse_pattern(&mut self, ast: &Pattern) { + fn unparse_pattern(&mut self, ast: &Pattern) { match ast { Pattern::MatchValue(ast::PatternMatchValue { value, @@ -830,7 +831,7 @@ impl<'a> Generator<'a> { } } - fn unparse_match_case(&mut self, ast: &MatchCase) { + fn unparse_match_case(&mut self, ast: &MatchCase) { self.p("case "); self.unparse_pattern(&ast.pattern); if let Some(guard) = &ast.guard { @@ -841,7 +842,7 @@ impl<'a> Generator<'a> { self.body(&ast.body); } - pub(crate) fn unparse_expr(&mut self, ast: &Expr, level: u8) { + pub(crate) fn unparse_expr(&mut self, ast: &Expr, level: u8) { macro_rules! opprec { ($opty:ident, $x:expr, $enu:path, $($var:ident($op:literal, $prec:ident)),*$(,)?) => { match $x { @@ -870,7 +871,7 @@ impl<'a> Generator<'a> { values, range: _range, }) => { - let (op, prec) = opprec!(bin, op, Boolop, And("and", AND), Or("or", OR)); + let (op, prec) = opprec!(bin, op, BoolOp, And("and", AND), Or("or", OR)); group_if!(prec, { let mut first = true; for val in values { @@ -929,7 +930,7 @@ impl<'a> Generator<'a> { let (op, prec) = opprec!( un, op, - rustpython_parser::ast::Unaryop, + rustpython_parser::ast::UnaryOp, Invert("~", INVERT), Not("not ", NOT), UAdd("+", UADD), @@ -1087,16 +1088,16 @@ impl<'a> Generator<'a> { self.unparse_expr(left, new_lvl); for (op, cmp) in ops.iter().zip(comparators) { let op = match op { - Cmpop::Eq => " == ", - Cmpop::NotEq => " != ", - Cmpop::Lt => " < ", - Cmpop::LtE => " <= ", - Cmpop::Gt => " > ", - Cmpop::GtE => " >= ", - Cmpop::Is => " is ", - Cmpop::IsNot => " is not ", - Cmpop::In => " in ", - Cmpop::NotIn => " not in ", + CmpOp::Eq => " == ", + CmpOp::NotEq => " != ", + CmpOp::Lt => " < ", + CmpOp::LtE => " <= ", + CmpOp::Gt => " > ", + CmpOp::GtE => " >= ", + CmpOp::Is => " is ", + CmpOp::IsNot => " is not ", + CmpOp::In => " in ", + CmpOp::NotIn => " not in ", }; self.p(op); self.unparse_expr(cmp, new_lvl); @@ -1190,7 +1191,7 @@ impl<'a> Generator<'a> { self.p("*"); self.unparse_expr(value, precedence::MAX); } - Expr::Name(ast::ExprName { id, .. }) => self.p_id(id), + Expr::Name(ast::ExprName { id, .. }) => self.p(id.as_str()), Expr::List(ast::ExprList { elts, .. }) => { self.p("["); let mut first = true; @@ -1288,16 +1289,11 @@ impl<'a> Generator<'a> { } } - fn unparse_args(&mut self, args: &Arguments) { + fn unparse_args(&mut self, args: &Arguments) { let mut first = true; - let defaults_start = args.posonlyargs.len() + args.args.len() - args.defaults.len(); - for (i, arg) in args.posonlyargs.iter().chain(&args.args).enumerate() { + for (i, arg_with_default) in args.posonlyargs.iter().chain(&args.args).enumerate() { self.p_delim(&mut first, ", "); - self.unparse_arg(arg); - if let Some(i) = i.checked_sub(defaults_start) { - self.p("="); - self.unparse_expr(&args.defaults[i], precedence::COMMA); - } + self.unparse_arg_with_default(arg_with_default); self.p_if(i + 1 == args.posonlyargs.len(), ", /"); } if args.vararg.is_some() || !args.kwonlyargs.is_empty() { @@ -1307,17 +1303,9 @@ impl<'a> Generator<'a> { if let Some(vararg) = &args.vararg { self.unparse_arg(vararg); } - let defaults_start = args.kwonlyargs.len() - args.kw_defaults.len(); - for (i, kwarg) in args.kwonlyargs.iter().enumerate() { + for kwarg in &args.kwonlyargs { self.p_delim(&mut first, ", "); - self.unparse_arg(kwarg); - if let Some(default) = i - .checked_sub(defaults_start) - .and_then(|i| args.kw_defaults.get(i)) - { - self.p("="); - self.unparse_expr(default, precedence::COMMA); - } + self.unparse_arg_with_default(kwarg); } if let Some(kwarg) = &args.kwarg { self.p_delim(&mut first, ", "); @@ -1326,7 +1314,7 @@ impl<'a> Generator<'a> { } } - fn unparse_arg(&mut self, arg: &Arg) { + fn unparse_arg(&mut self, arg: &Arg) { self.p_id(&arg.arg); if let Some(ann) = &arg.annotation { self.p(": "); @@ -1334,7 +1322,15 @@ impl<'a> Generator<'a> { } } - fn unparse_comp(&mut self, generators: &[Comprehension]) { + fn unparse_arg_with_default(&mut self, arg_with_default: &ArgWithDefault) { + self.unparse_arg(&arg_with_default.def); + if let Some(default) = &arg_with_default.default { + self.p("="); + self.unparse_expr(default, precedence::COMMA); + } + } + + fn unparse_comp(&mut self, generators: &[Comprehension]) { for comp in generators { self.p(if comp.is_async { " async for " @@ -1351,18 +1347,13 @@ impl<'a> Generator<'a> { } } - fn unparse_fstring_body(&mut self, values: &[Expr], is_spec: bool) { + fn unparse_fstring_body(&mut self, values: &[Expr], is_spec: bool) { for value in values { self.unparse_fstring_elem(value, is_spec); } } - fn unparse_formatted( - &mut self, - val: &Expr, - conversion: ConversionFlag, - spec: Option<&Expr>, - ) { + fn unparse_formatted(&mut self, val: &Expr, conversion: ConversionFlag, spec: Option<&Expr>) { let mut generator = Generator::new(self.indent, self.quote, self.line_ending); generator.unparse_expr(val, precedence::FORMATTED_VALUE); let brace = if generator.buffer.starts_with('{') { @@ -1388,7 +1379,7 @@ impl<'a> Generator<'a> { self.p("}"); } - fn unparse_fstring_elem(&mut self, expr: &Expr, is_spec: bool) { + fn unparse_fstring_elem(&mut self, expr: &Expr, is_spec: bool) { match expr { Expr::Constant(ast::ExprConstant { value, .. }) => { if let Constant::Str(s) = value { @@ -1418,7 +1409,7 @@ impl<'a> Generator<'a> { self.p(&s); } - fn unparse_joinedstr(&mut self, values: &[Expr], is_spec: bool) { + fn unparse_joinedstr(&mut self, values: &[Expr], is_spec: bool) { if is_spec { self.unparse_fstring_body(values, is_spec); } else { @@ -1437,7 +1428,7 @@ impl<'a> Generator<'a> { } } - fn unparse_alias(&mut self, alias: &Alias) { + fn unparse_alias(&mut self, alias: &Alias) { self.p_id(&alias.name); if let Some(asname) = &alias.asname { self.p(" as "); @@ -1445,9 +1436,9 @@ impl<'a> Generator<'a> { } } - fn unparse_withitem(&mut self, withitem: &Withitem) { - self.unparse_expr(&withitem.context_expr, precedence::MAX); - if let Some(optional_vars) = &withitem.optional_vars { + fn unparse_with_item(&mut self, with_item: &WithItem) { + self.unparse_expr(&with_item.context_expr, precedence::MAX); + if let Some(optional_vars) = &with_item.optional_vars { self.p(" as "); self.unparse_expr(optional_vars, precedence::MAX); } diff --git a/crates/ruff_python_ast/src/source_code/indexer.rs b/crates/ruff_python_ast/src/source_code/indexer.rs index 6cfdf693ca..23227965a5 100644 --- a/crates/ruff_python_ast/src/source_code/indexer.rs +++ b/crates/ruff_python_ast/src/source_code/indexer.rs @@ -49,10 +49,7 @@ impl Indexer { } // Newlines after a newline never form a continuation. - if !matches!( - prev_token, - Some(Tok::Newline | Tok::NonLogicalNewline) | None - ) { + if !matches!(prev_token, Some(Tok::Newline | Tok::NonLogicalNewline)) { continuation_lines.push(line_start); } @@ -204,7 +201,7 @@ if True: ] ); - let contents = r#" + let contents = r" x = 1; import sys import os @@ -218,7 +215,7 @@ if True: x = 1; \ import os -"# +" .trim(); let lxr: Vec = lexer::lex(contents, Mode::Module).collect(); let indexer = Indexer::from_tokens(lxr.as_slice(), &Locator::new(contents)); diff --git a/crates/ruff_python_ast/src/source_code/line_index.rs b/crates/ruff_python_ast/src/source_code/line_index.rs index 1096f1d1bf..157d75868e 100644 --- a/crates/ruff_python_ast/src/source_code/line_index.rs +++ b/crates/ruff_python_ast/src/source_code/line_index.rs @@ -245,12 +245,11 @@ impl IndexKind { pub struct OneIndexed(NonZeroUsize); impl OneIndexed { + /// The largest value that can be represented by this integer type + pub const MAX: Self = unwrap(Self::new(usize::MAX)); // SAFETY: These constants are being initialized with non-zero values /// The smallest value that can be represented by this integer type. pub const MIN: Self = unwrap(Self::new(1)); - /// The largest value that can be represented by this integer type - pub const MAX: Self = unwrap(Self::new(usize::MAX)); - pub const ONE: NonZeroUsize = unwrap(NonZeroUsize::new(1)); /// Creates a non-zero if the given value is not zero. diff --git a/crates/ruff_python_ast/src/source_code/mod.rs b/crates/ruff_python_ast/src/source_code/mod.rs index acd988c02e..829cc98515 100644 --- a/crates/ruff_python_ast/src/source_code/mod.rs +++ b/crates/ruff_python_ast/src/source_code/mod.rs @@ -202,7 +202,7 @@ impl SourceFile { .get_or_init(|| LineIndex::from_source_text(self.source_text())) } - /// Returns `Some` with the source text if set, or `None`. + /// Returns the source code. #[inline] pub fn source_text(&self) -> &str { &self.inner.code diff --git a/crates/ruff_python_ast/src/statement_visitor.rs b/crates/ruff_python_ast/src/statement_visitor.rs index 805da33b42..df35b6bb2e 100644 --- a/crates/ruff_python_ast/src/statement_visitor.rs +++ b/crates/ruff_python_ast/src/statement_visitor.rs @@ -1,6 +1,6 @@ //! Specialized AST visitor trait and walk functions that only visit statements. -use rustpython_parser::ast::{self, Excepthandler, MatchCase, Stmt}; +use rustpython_parser::ast::{self, ExceptHandler, MatchCase, Stmt}; /// A trait for AST visitors that only need to visit statements. pub trait StatementVisitor<'a> { @@ -10,8 +10,8 @@ pub trait StatementVisitor<'a> { fn visit_stmt(&mut self, stmt: &'a Stmt) { walk_stmt(self, stmt); } - fn visit_excepthandler(&mut self, excepthandler: &'a Excepthandler) { - walk_excepthandler(self, excepthandler); + fn visit_except_handler(&mut self, except_handler: &'a ExceptHandler) { + walk_except_handler(self, except_handler); } fn visit_match_case(&mut self, match_case: &'a MatchCase) { walk_match_case(self, match_case); @@ -70,8 +70,8 @@ pub fn walk_stmt<'a, V: StatementVisitor<'a> + ?Sized>(visitor: &mut V, stmt: &' range: _range, }) => { visitor.visit_body(body); - for excepthandler in handlers { - visitor.visit_excepthandler(excepthandler); + for except_handler in handlers { + visitor.visit_except_handler(except_handler); } visitor.visit_body(orelse); visitor.visit_body(finalbody); @@ -84,8 +84,8 @@ pub fn walk_stmt<'a, V: StatementVisitor<'a> + ?Sized>(visitor: &mut V, stmt: &' range: _range, }) => { visitor.visit_body(body); - for excepthandler in handlers { - visitor.visit_excepthandler(excepthandler); + for except_handler in handlers { + visitor.visit_except_handler(except_handler); } visitor.visit_body(orelse); visitor.visit_body(finalbody); @@ -94,12 +94,12 @@ pub fn walk_stmt<'a, V: StatementVisitor<'a> + ?Sized>(visitor: &mut V, stmt: &' } } -pub fn walk_excepthandler<'a, V: StatementVisitor<'a> + ?Sized>( +pub fn walk_except_handler<'a, V: StatementVisitor<'a> + ?Sized>( visitor: &mut V, - excepthandler: &'a Excepthandler, + except_handler: &'a ExceptHandler, ) { - match excepthandler { - Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { body, .. }) => { + match except_handler { + ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { body, .. }) => { visitor.visit_body(body); } } diff --git a/crates/ruff_python_ast/src/str.rs b/crates/ruff_python_ast/src/str.rs index 7da1af24fe..5421641c21 100644 --- a/crates/ruff_python_ast/src/str.rs +++ b/crates/ruff_python_ast/src/str.rs @@ -1,23 +1,140 @@ use ruff_text_size::{TextLen, TextRange}; +/// Includes all permutations of `r`, `u`, `f`, and `fr` (`ur` is invalid, as is `uf`). This +/// includes all possible orders, and all possible casings, for both single and triple quotes. +/// /// See: +#[rustfmt::skip] const TRIPLE_QUOTE_STR_PREFIXES: &[&str] = &[ - "u\"\"\"", "u'''", "r\"\"\"", "r'''", "U\"\"\"", "U'''", "R\"\"\"", "R'''", "\"\"\"", "'''", + "FR\"\"\"", + "Fr\"\"\"", + "fR\"\"\"", + "fr\"\"\"", + "RF\"\"\"", + "Rf\"\"\"", + "rF\"\"\"", + "rf\"\"\"", + "FR'''", + "Fr'''", + "fR'''", + "fr'''", + "RF'''", + "Rf'''", + "rF'''", + "rf'''", + "R\"\"\"", + "r\"\"\"", + "R'''", + "r'''", + "F\"\"\"", + "f\"\"\"", + "F'''", + "f'''", + "U\"\"\"", + "u\"\"\"", + "U'''", + "u'''", + "\"\"\"", + "'''", ]; + +#[rustfmt::skip] const SINGLE_QUOTE_STR_PREFIXES: &[&str] = &[ - "u\"", "u'", "r\"", "r'", "U\"", "U'", "R\"", "R'", "\"", "'", + "FR\"", + "Fr\"", + "fR\"", + "fr\"", + "RF\"", + "Rf\"", + "rF\"", + "rf\"", + "FR'", + "Fr'", + "fR'", + "fr'", + "RF'", + "Rf'", + "rF'", + "rf'", + "R\"", + "r\"", + "R'", + "r'", + "F\"", + "f\"", + "F'", + "f'", + "U\"", + "u\"", + "U'", + "u'", + "\"", + "'", ]; + +/// Includes all permutations of `b` and `rb`. This includes all possible orders, and all possible +/// casings, for both single and triple quotes. +/// +/// See: +#[rustfmt::skip] pub const TRIPLE_QUOTE_BYTE_PREFIXES: &[&str] = &[ - "br'''", "rb'''", "bR'''", "Rb'''", "Br'''", "rB'''", "RB'''", "BR'''", "b'''", "br\"\"\"", - "rb\"\"\"", "bR\"\"\"", "Rb\"\"\"", "Br\"\"\"", "rB\"\"\"", "RB\"\"\"", "BR\"\"\"", "b\"\"\"", + "BR\"\"\"", + "Br\"\"\"", + "bR\"\"\"", + "br\"\"\"", + "RB\"\"\"", + "Rb\"\"\"", + "rB\"\"\"", + "rb\"\"\"", + "BR'''", + "Br'''", + "bR'''", + "br'''", + "RB'''", + "Rb'''", + "rB'''", + "rb'''", "B\"\"\"", + "b\"\"\"", + "B'''", + "b'''", ]; + +#[rustfmt::skip] pub const SINGLE_QUOTE_BYTE_PREFIXES: &[&str] = &[ - "br'", "rb'", "bR'", "Rb'", "Br'", "rB'", "RB'", "BR'", "b'", "br\"", "rb\"", "bR\"", "Rb\"", - "Br\"", "rB\"", "RB\"", "BR\"", "b\"", "B\"", + "BR\"", + "Br\"", + "bR\"", + "br\"", + "RB\"", + "Rb\"", + "rB\"", + "rb\"", + "BR'", + "Br'", + "bR'", + "br'", + "RB'", + "Rb'", + "rB'", + "rb'", + "B\"", + "b\"", + "B'", + "b'", +]; + +#[rustfmt::skip] +const TRIPLE_QUOTE_SUFFIXES: &[&str] = &[ + "\"\"\"", + "'''", +]; + +#[rustfmt::skip] +const SINGLE_QUOTE_SUFFIXES: &[&str] = &[ + "\"", + "'", ]; -const TRIPLE_QUOTE_SUFFIXES: &[&str] = &["\"\"\"", "'''"]; -const SINGLE_QUOTE_SUFFIXES: &[&str] = &["\"", "'"]; /// Strip the leading and trailing quotes from a string. /// Assumes that the string is a valid string literal, but does not verify that the string @@ -103,11 +220,15 @@ pub fn is_implicit_concatenation(content: &str) -> bool { let mut rest = &content[leading_quote_str.len()..content.len() - trailing_quote_str.len()]; while let Some(index) = rest.find(trailing_quote_str) { let mut chars = rest[..index].chars().rev(); + if let Some('\\') = chars.next() { - // If the quote is double-escaped, then it's _not_ escaped, so the string is - // implicitly concatenated. - if let Some('\\') = chars.next() { - return true; + if chars.next() == Some('\\') { + // Either `\\'` or `\\\'` need to test one more character + + // If the quote is preceded by `//` then it is not escaped, instead the backslash is escaped. + if chars.next() != Some('\\') { + return true; + } } } else { // If the quote is _not_ escaped, then it's implicitly concatenated. @@ -182,5 +303,6 @@ mod tests { // Negative cases with escaped quotes. assert!(!is_implicit_concatenation(r#""abc\"def""#)); + assert!(!is_implicit_concatenation(r#"'\\\' ""'"#)); } } diff --git a/crates/ruff_python_ast/src/types.rs b/crates/ruff_python_ast/src/types.rs index 7596cfae44..baa2839e1f 100644 --- a/crates/ruff_python_ast/src/types.rs +++ b/crates/ruff_python_ast/src/types.rs @@ -28,7 +28,7 @@ impl<'a, T> AsRef for RefEquality<'a, T> { impl<'a, T> Clone for RefEquality<'a, T> { fn clone(&self) -> Self { - Self(self.0) + *self } } diff --git a/crates/ruff_python_ast/src/visitor.rs b/crates/ruff_python_ast/src/visitor.rs index 2864df5acb..60cfc2a90c 100644 --- a/crates/ruff_python_ast/src/visitor.rs +++ b/crates/ruff_python_ast/src/visitor.rs @@ -2,10 +2,9 @@ pub mod preorder; -use rustpython_ast::Decorator; use rustpython_parser::ast::{ - self, Alias, Arg, Arguments, Boolop, Cmpop, Comprehension, Constant, Excepthandler, Expr, - ExprContext, Keyword, MatchCase, Operator, Pattern, Stmt, Unaryop, Withitem, + self, Alias, Arg, Arguments, BoolOp, CmpOp, Comprehension, Constant, Decorator, ExceptHandler, + Expr, ExprContext, Keyword, MatchCase, Operator, Pattern, Stmt, UnaryOp, WithItem, }; /// A trait for AST visitors. Visits all nodes in the AST recursively in evaluation-order. @@ -20,7 +19,7 @@ pub trait Visitor<'a> { walk_stmt(self, stmt); } fn visit_annotation(&mut self, expr: &'a Expr) { - walk_expr(self, expr); + walk_annotation(self, expr); } fn visit_decorator(&mut self, decorator: &'a Decorator) { walk_decorator(self, decorator); @@ -34,26 +33,26 @@ pub trait Visitor<'a> { fn visit_expr_context(&mut self, expr_context: &'a ExprContext) { walk_expr_context(self, expr_context); } - fn visit_boolop(&mut self, boolop: &'a Boolop) { - walk_boolop(self, boolop); + fn visit_bool_op(&mut self, bool_op: &'a BoolOp) { + walk_bool_op(self, bool_op); } fn visit_operator(&mut self, operator: &'a Operator) { walk_operator(self, operator); } - fn visit_unaryop(&mut self, unaryop: &'a Unaryop) { - walk_unaryop(self, unaryop); + fn visit_unary_op(&mut self, unary_op: &'a UnaryOp) { + walk_unary_op(self, unary_op); } - fn visit_cmpop(&mut self, cmpop: &'a Cmpop) { - walk_cmpop(self, cmpop); + fn visit_cmp_op(&mut self, cmp_op: &'a CmpOp) { + walk_cmp_op(self, cmp_op); } fn visit_comprehension(&mut self, comprehension: &'a Comprehension) { walk_comprehension(self, comprehension); } - fn visit_excepthandler(&mut self, excepthandler: &'a Excepthandler) { - walk_excepthandler(self, excepthandler); + fn visit_except_handler(&mut self, except_handler: &'a ExceptHandler) { + walk_except_handler(self, except_handler); } fn visit_format_spec(&mut self, format_spec: &'a Expr) { - walk_expr(self, format_spec); + walk_format_spec(self, format_spec); } fn visit_arguments(&mut self, arguments: &'a Arguments) { walk_arguments(self, arguments); @@ -67,8 +66,8 @@ pub trait Visitor<'a> { fn visit_alias(&mut self, alias: &'a Alias) { walk_alias(self, alias); } - fn visit_withitem(&mut self, withitem: &'a Withitem) { - walk_withitem(self, withitem); + fn visit_with_item(&mut self, with_item: &'a WithItem) { + walk_with_item(self, with_item); } fn visit_match_case(&mut self, match_case: &'a MatchCase) { walk_match_case(self, match_case); @@ -96,10 +95,10 @@ pub fn walk_stmt<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, stmt: &'a Stmt) { returns, .. }) => { - visitor.visit_arguments(args); for decorator in decorator_list { visitor.visit_decorator(decorator); } + visitor.visit_arguments(args); for expr in returns { visitor.visit_annotation(expr); } @@ -112,10 +111,10 @@ pub fn walk_stmt<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, stmt: &'a Stmt) { returns, .. }) => { - visitor.visit_arguments(args); for decorator in decorator_list { visitor.visit_decorator(decorator); } + visitor.visit_arguments(args); for expr in returns { visitor.visit_annotation(expr); } @@ -128,15 +127,15 @@ pub fn walk_stmt<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, stmt: &'a Stmt) { decorator_list, .. }) => { + for decorator in decorator_list { + visitor.visit_decorator(decorator); + } for expr in bases { visitor.visit_expr(expr); } for keyword in keywords { visitor.visit_keyword(keyword); } - for decorator in decorator_list { - visitor.visit_decorator(decorator); - } visitor.visit_body(body); } Stmt::Return(ast::StmtReturn { @@ -167,9 +166,9 @@ pub fn walk_stmt<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, stmt: &'a Stmt) { value, range: _range, }) => { - visitor.visit_expr(target); - visitor.visit_operator(op); visitor.visit_expr(value); + visitor.visit_operator(op); + visitor.visit_expr(target); } Stmt::AnnAssign(ast::StmtAnnAssign { target, @@ -177,10 +176,10 @@ pub fn walk_stmt<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, stmt: &'a Stmt) { value, .. }) => { - visitor.visit_annotation(annotation); if let Some(expr) = value { visitor.visit_expr(expr); } + visitor.visit_annotation(annotation); visitor.visit_expr(target); } Stmt::For(ast::StmtFor { @@ -228,14 +227,14 @@ pub fn walk_stmt<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, stmt: &'a Stmt) { visitor.visit_body(orelse); } Stmt::With(ast::StmtWith { items, body, .. }) => { - for withitem in items { - visitor.visit_withitem(withitem); + for with_item in items { + visitor.visit_with_item(with_item); } visitor.visit_body(body); } Stmt::AsyncWith(ast::StmtAsyncWith { items, body, .. }) => { - for withitem in items { - visitor.visit_withitem(withitem); + for with_item in items { + visitor.visit_with_item(with_item); } visitor.visit_body(body); } @@ -269,8 +268,8 @@ pub fn walk_stmt<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, stmt: &'a Stmt) { range: _range, }) => { visitor.visit_body(body); - for excepthandler in handlers { - visitor.visit_excepthandler(excepthandler); + for except_handler in handlers { + visitor.visit_except_handler(except_handler); } visitor.visit_body(orelse); visitor.visit_body(finalbody); @@ -283,8 +282,8 @@ pub fn walk_stmt<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, stmt: &'a Stmt) { range: _range, }) => { visitor.visit_body(body); - for excepthandler in handlers { - visitor.visit_excepthandler(excepthandler); + for except_handler in handlers { + visitor.visit_except_handler(except_handler); } visitor.visit_body(orelse); visitor.visit_body(finalbody); @@ -322,6 +321,10 @@ pub fn walk_stmt<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, stmt: &'a Stmt) { } } +pub fn walk_annotation<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, expr: &'a Expr) { + visitor.visit_expr(expr); +} + pub fn walk_decorator<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, decorator: &'a Decorator) { visitor.visit_expr(&decorator.expression); } @@ -333,7 +336,7 @@ pub fn walk_expr<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, expr: &'a Expr) { values, range: _range, }) => { - visitor.visit_boolop(op); + visitor.visit_bool_op(op); for expr in values { visitor.visit_expr(expr); } @@ -361,7 +364,7 @@ pub fn walk_expr<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, expr: &'a Expr) { operand, range: _range, }) => { - visitor.visit_unaryop(op); + visitor.visit_unary_op(op); visitor.visit_expr(operand); } Expr::Lambda(ast::ExprLambda { @@ -467,8 +470,8 @@ pub fn walk_expr<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, expr: &'a Expr) { range: _range, }) => { visitor.visit_expr(left); - for cmpop in ops { - visitor.visit_cmpop(cmpop); + for cmp_op in ops { + visitor.visit_cmp_op(cmp_op); } for expr in comparators { visitor.visit_expr(expr); @@ -588,12 +591,12 @@ pub fn walk_comprehension<'a, V: Visitor<'a> + ?Sized>( } } -pub fn walk_excepthandler<'a, V: Visitor<'a> + ?Sized>( +pub fn walk_except_handler<'a, V: Visitor<'a> + ?Sized>( visitor: &mut V, - excepthandler: &'a Excepthandler, + except_handler: &'a ExceptHandler, ) { - match excepthandler { - Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { type_, body, .. }) => { + match except_handler { + ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { type_, body, .. }) => { if let Some(expr) = type_ { visitor.visit_expr(expr); } @@ -602,28 +605,43 @@ pub fn walk_excepthandler<'a, V: Visitor<'a> + ?Sized>( } } +pub fn walk_format_spec<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, format_spec: &'a Expr) { + visitor.visit_expr(format_spec); +} + pub fn walk_arguments<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, arguments: &'a Arguments) { + // Defaults are evaluated before annotations. for arg in &arguments.posonlyargs { - visitor.visit_arg(arg); + if let Some(default) = &arg.default { + visitor.visit_expr(default); + } } for arg in &arguments.args { - visitor.visit_arg(arg); + if let Some(default) = &arg.default { + visitor.visit_expr(default); + } + } + for arg in &arguments.kwonlyargs { + if let Some(default) = &arg.default { + visitor.visit_expr(default); + } + } + + for arg in &arguments.posonlyargs { + visitor.visit_arg(&arg.def); + } + for arg in &arguments.args { + visitor.visit_arg(&arg.def); } if let Some(arg) = &arguments.vararg { visitor.visit_arg(arg); } for arg in &arguments.kwonlyargs { - visitor.visit_arg(arg); - } - for expr in &arguments.kw_defaults { - visitor.visit_expr(expr); + visitor.visit_arg(&arg.def); } if let Some(arg) = &arguments.kwarg { visitor.visit_arg(arg); } - for expr in &arguments.defaults { - visitor.visit_expr(expr); - } } pub fn walk_arg<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, arg: &'a Arg) { @@ -636,9 +654,9 @@ pub fn walk_keyword<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, keyword: &'a K visitor.visit_expr(&keyword.value); } -pub fn walk_withitem<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, withitem: &'a Withitem) { - visitor.visit_expr(&withitem.context_expr); - if let Some(expr) = &withitem.optional_vars { +pub fn walk_with_item<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, with_item: &'a WithItem) { + visitor.visit_expr(&with_item.context_expr); + if let Some(expr) = &with_item.optional_vars { visitor.visit_expr(expr); } } @@ -719,16 +737,16 @@ pub fn walk_expr_context<'a, V: Visitor<'a> + ?Sized>( } #[allow(unused_variables)] -pub fn walk_boolop<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, boolop: &'a Boolop) {} +pub fn walk_bool_op<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, bool_op: &'a BoolOp) {} #[allow(unused_variables)] pub fn walk_operator<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, operator: &'a Operator) {} #[allow(unused_variables)] -pub fn walk_unaryop<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, unaryop: &'a Unaryop) {} +pub fn walk_unary_op<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, unary_op: &'a UnaryOp) {} #[allow(unused_variables)] -pub fn walk_cmpop<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, cmpop: &'a Cmpop) {} +pub fn walk_cmp_op<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, cmp_op: &'a CmpOp) {} #[allow(unused_variables)] pub fn walk_alias<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, alias: &'a Alias) {} diff --git a/crates/ruff_python_ast/src/visitor/preorder.rs b/crates/ruff_python_ast/src/visitor/preorder.rs index 974469ce0a..4a21e82bad 100644 --- a/crates/ruff_python_ast/src/visitor/preorder.rs +++ b/crates/ruff_python_ast/src/visitor/preorder.rs @@ -1,4 +1,8 @@ -use crate::prelude::*; +use rustpython_ast::{ArgWithDefault, Mod, TypeIgnore}; +use rustpython_parser::ast::{ + self, Alias, Arg, Arguments, BoolOp, CmpOp, Comprehension, Constant, Decorator, ExceptHandler, + Expr, Keyword, MatchCase, Operator, Pattern, Stmt, UnaryOp, WithItem, +}; /// Visitor that traverses all nodes recursively in pre-order. pub trait PreorderVisitor<'a> { @@ -11,7 +15,7 @@ pub trait PreorderVisitor<'a> { } fn visit_annotation(&mut self, expr: &'a Expr) { - walk_expr(self, expr); + walk_annotation(self, expr); } fn visit_expr(&mut self, expr: &'a Expr) { @@ -26,32 +30,32 @@ pub trait PreorderVisitor<'a> { walk_constant(self, constant); } - fn visit_boolop(&mut self, boolop: &'a Boolop) { - walk_boolop(self, boolop); + fn visit_bool_op(&mut self, bool_op: &'a BoolOp) { + walk_bool_op(self, bool_op); } fn visit_operator(&mut self, operator: &'a Operator) { walk_operator(self, operator); } - fn visit_unaryop(&mut self, unaryop: &'a Unaryop) { - walk_unaryop(self, unaryop); + fn visit_unary_op(&mut self, unary_op: &'a UnaryOp) { + walk_unary_op(self, unary_op); } - fn visit_cmpop(&mut self, cmpop: &'a Cmpop) { - walk_cmpop(self, cmpop); + fn visit_cmp_op(&mut self, cmp_op: &'a CmpOp) { + walk_cmp_op(self, cmp_op); } fn visit_comprehension(&mut self, comprehension: &'a Comprehension) { walk_comprehension(self, comprehension); } - fn visit_excepthandler(&mut self, excepthandler: &'a Excepthandler) { - walk_excepthandler(self, excepthandler); + fn visit_except_handler(&mut self, except_handler: &'a ExceptHandler) { + walk_except_handler(self, except_handler); } fn visit_format_spec(&mut self, format_spec: &'a Expr) { - walk_expr(self, format_spec); + walk_format_spec(self, format_spec); } fn visit_arguments(&mut self, arguments: &'a Arguments) { @@ -62,6 +66,10 @@ pub trait PreorderVisitor<'a> { walk_arg(self, arg); } + fn visit_arg_with_default(&mut self, arg_with_default: &'a ArgWithDefault) { + walk_arg_with_default(self, arg_with_default); + } + fn visit_keyword(&mut self, keyword: &'a Keyword) { walk_keyword(self, keyword); } @@ -70,8 +78,8 @@ pub trait PreorderVisitor<'a> { walk_alias(self, alias); } - fn visit_withitem(&mut self, withitem: &'a Withitem) { - walk_withitem(self, withitem); + fn visit_with_item(&mut self, with_item: &'a WithItem) { + walk_with_item(self, with_item); } fn visit_match_case(&mut self, match_case: &'a MatchCase) { @@ -96,7 +104,7 @@ where V: PreorderVisitor<'a> + ?Sized, { match module { - Mod::Module(ModModule { + Mod::Module(ast::ModModule { body, range: _, type_ignores, @@ -106,9 +114,9 @@ where visitor.visit_type_ignore(ignore); } } - Mod::Interactive(ModInteractive { body, range: _ }) => visitor.visit_body(body), - Mod::Expression(ModExpression { body, range: _ }) => visitor.visit_expr(body), - Mod::FunctionType(ModFunctionType { + Mod::Interactive(ast::ModInteractive { body, range: _ }) => visitor.visit_body(body), + Mod::Expression(ast::ModExpression { body, range: _ }) => visitor.visit_expr(body), + Mod::FunctionType(ast::ModFunctionType { range: _, argtypes, returns, @@ -136,19 +144,19 @@ where V: PreorderVisitor<'a> + ?Sized, { match stmt { - Stmt::Expr(StmtExpr { + Stmt::Expr(ast::StmtExpr { value, range: _range, }) => visitor.visit_expr(value), - Stmt::FunctionDef(StmtFunctionDef { + Stmt::FunctionDef(ast::StmtFunctionDef { args, body, decorator_list, returns, .. }) - | Stmt::AsyncFunctionDef(StmtAsyncFunctionDef { + | Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { args, body, decorator_list, @@ -168,7 +176,7 @@ where visitor.visit_body(body); } - Stmt::ClassDef(StmtClassDef { + Stmt::ClassDef(ast::StmtClassDef { bases, keywords, body, @@ -190,7 +198,7 @@ where visitor.visit_body(body); } - Stmt::Return(StmtReturn { + Stmt::Return(ast::StmtReturn { value, range: _range, }) => { @@ -199,7 +207,7 @@ where } } - Stmt::Delete(StmtDelete { + Stmt::Delete(ast::StmtDelete { targets, range: _range, }) => { @@ -208,7 +216,7 @@ where } } - Stmt::Assign(StmtAssign { + Stmt::Assign(ast::StmtAssign { targets, value, range: _, @@ -221,7 +229,7 @@ where visitor.visit_expr(value); } - Stmt::AugAssign(StmtAugAssign { + Stmt::AugAssign(ast::StmtAugAssign { target, op, value, @@ -232,7 +240,7 @@ where visitor.visit_expr(value); } - Stmt::AnnAssign(StmtAnnAssign { + Stmt::AnnAssign(ast::StmtAnnAssign { target, annotation, value, @@ -246,14 +254,14 @@ where } } - Stmt::For(StmtFor { + Stmt::For(ast::StmtFor { target, iter, body, orelse, .. }) - | Stmt::AsyncFor(StmtAsyncFor { + | Stmt::AsyncFor(ast::StmtAsyncFor { target, iter, body, @@ -266,7 +274,7 @@ where visitor.visit_body(orelse); } - Stmt::While(StmtWhile { + Stmt::While(ast::StmtWhile { test, body, orelse, @@ -277,7 +285,7 @@ where visitor.visit_body(orelse); } - Stmt::If(StmtIf { + Stmt::If(ast::StmtIf { test, body, orelse, @@ -288,25 +296,25 @@ where visitor.visit_body(orelse); } - Stmt::With(StmtWith { + Stmt::With(ast::StmtWith { items, body, type_comment: _, range: _, }) - | Stmt::AsyncWith(StmtAsyncWith { + | Stmt::AsyncWith(ast::StmtAsyncWith { items, body, type_comment: _, range: _, }) => { - for withitem in items { - visitor.visit_withitem(withitem); + for with_item in items { + visitor.visit_with_item(with_item); } visitor.visit_body(body); } - Stmt::Match(StmtMatch { + Stmt::Match(ast::StmtMatch { subject, cases, range: _range, @@ -317,7 +325,7 @@ where } } - Stmt::Raise(StmtRaise { + Stmt::Raise(ast::StmtRaise { exc, cause, range: _range, @@ -330,14 +338,14 @@ where }; } - Stmt::Try(StmtTry { + Stmt::Try(ast::StmtTry { body, handlers, orelse, finalbody, range: _range, }) - | Stmt::TryStar(StmtTryStar { + | Stmt::TryStar(ast::StmtTryStar { body, handlers, orelse, @@ -345,14 +353,14 @@ where range: _range, }) => { visitor.visit_body(body); - for excepthandler in handlers { - visitor.visit_excepthandler(excepthandler); + for except_handler in handlers { + visitor.visit_except_handler(except_handler); } visitor.visit_body(orelse); visitor.visit_body(finalbody); } - Stmt::Assert(StmtAssert { + Stmt::Assert(ast::StmtAssert { test, msg, range: _range, @@ -363,7 +371,7 @@ where } } - Stmt::Import(StmtImport { + Stmt::Import(ast::StmtImport { names, range: _range, }) => { @@ -372,7 +380,7 @@ where } } - Stmt::ImportFrom(StmtImportFrom { + Stmt::ImportFrom(ast::StmtImportFrom { range: _, module: _, names, @@ -391,6 +399,10 @@ where } } +pub fn walk_annotation<'a, V: PreorderVisitor<'a> + ?Sized>(visitor: &mut V, expr: &'a Expr) { + visitor.visit_expr(expr); +} + pub fn walk_decorator<'a, V>(visitor: &mut V, decorator: &'a Decorator) where V: PreorderVisitor<'a> + ?Sized, @@ -403,24 +415,24 @@ where V: PreorderVisitor<'a> + ?Sized, { match expr { - Expr::BoolOp(ExprBoolOp { + Expr::BoolOp(ast::ExprBoolOp { op, values, range: _range, }) => match values.as_slice() { [left, rest @ ..] => { visitor.visit_expr(left); - visitor.visit_boolop(op); + visitor.visit_bool_op(op); for expr in rest { visitor.visit_expr(expr); } } [] => { - visitor.visit_boolop(op); + visitor.visit_bool_op(op); } }, - Expr::NamedExpr(ExprNamedExpr { + Expr::NamedExpr(ast::ExprNamedExpr { target, value, range: _range, @@ -429,7 +441,7 @@ where visitor.visit_expr(value); } - Expr::BinOp(ExprBinOp { + Expr::BinOp(ast::ExprBinOp { left, op, right, @@ -440,16 +452,16 @@ where visitor.visit_expr(right); } - Expr::UnaryOp(ExprUnaryOp { + Expr::UnaryOp(ast::ExprUnaryOp { op, operand, range: _range, }) => { - visitor.visit_unaryop(op); + visitor.visit_unary_op(op); visitor.visit_expr(operand); } - Expr::Lambda(ExprLambda { + Expr::Lambda(ast::ExprLambda { args, body, range: _range, @@ -458,18 +470,19 @@ where visitor.visit_expr(body); } - Expr::IfExp(ExprIfExp { + Expr::IfExp(ast::ExprIfExp { test, body, orelse, range: _range, }) => { - visitor.visit_expr(test); + // `body if test else orelse` visitor.visit_expr(body); + visitor.visit_expr(test); visitor.visit_expr(orelse); } - Expr::Dict(ExprDict { + Expr::Dict(ast::ExprDict { keys, values, range: _range, @@ -482,7 +495,7 @@ where } } - Expr::Set(ExprSet { + Expr::Set(ast::ExprSet { elts, range: _range, }) => { @@ -491,7 +504,7 @@ where } } - Expr::ListComp(ExprListComp { + Expr::ListComp(ast::ExprListComp { elt, generators, range: _range, @@ -502,7 +515,7 @@ where } } - Expr::SetComp(ExprSetComp { + Expr::SetComp(ast::ExprSetComp { elt, generators, range: _range, @@ -513,7 +526,7 @@ where } } - Expr::DictComp(ExprDictComp { + Expr::DictComp(ast::ExprDictComp { key, value, generators, @@ -527,7 +540,7 @@ where } } - Expr::GeneratorExp(ExprGeneratorExp { + Expr::GeneratorExp(ast::ExprGeneratorExp { elt, generators, range: _range, @@ -538,16 +551,16 @@ where } } - Expr::Await(ExprAwait { + Expr::Await(ast::ExprAwait { value, range: _range, }) - | Expr::YieldFrom(ExprYieldFrom { + | Expr::YieldFrom(ast::ExprYieldFrom { value, range: _range, }) => visitor.visit_expr(value), - Expr::Yield(ExprYield { + Expr::Yield(ast::ExprYield { value, range: _range, }) => { @@ -556,7 +569,7 @@ where } } - Expr::Compare(ExprCompare { + Expr::Compare(ast::ExprCompare { left, ops, comparators, @@ -565,12 +578,12 @@ where visitor.visit_expr(left); for (op, comparator) in ops.iter().zip(comparators) { - visitor.visit_cmpop(op); + visitor.visit_cmp_op(op); visitor.visit_expr(comparator); } } - Expr::Call(ExprCall { + Expr::Call(ast::ExprCall { func, args, keywords, @@ -585,7 +598,7 @@ where } } - Expr::FormattedValue(ExprFormattedValue { + Expr::FormattedValue(ast::ExprFormattedValue { value, format_spec, .. }) => { visitor.visit_expr(value); @@ -595,7 +608,7 @@ where } } - Expr::JoinedStr(ExprJoinedStr { + Expr::JoinedStr(ast::ExprJoinedStr { values, range: _range, }) => { @@ -604,13 +617,13 @@ where } } - Expr::Constant(ExprConstant { + Expr::Constant(ast::ExprConstant { value, range: _, kind: _, }) => visitor.visit_constant(value), - Expr::Attribute(ExprAttribute { + Expr::Attribute(ast::ExprAttribute { value, attr: _, ctx: _, @@ -619,7 +632,7 @@ where visitor.visit_expr(value); } - Expr::Subscript(ExprSubscript { + Expr::Subscript(ast::ExprSubscript { value, slice, ctx: _, @@ -628,7 +641,7 @@ where visitor.visit_expr(value); visitor.visit_expr(slice); } - Expr::Starred(ExprStarred { + Expr::Starred(ast::ExprStarred { value, ctx: _, range: _range, @@ -636,13 +649,13 @@ where visitor.visit_expr(value); } - Expr::Name(ExprName { + Expr::Name(ast::ExprName { id: _, ctx: _, range: _, }) => {} - Expr::List(ExprList { + Expr::List(ast::ExprList { elts, ctx: _, range: _range, @@ -651,7 +664,7 @@ where visitor.visit_expr(expr); } } - Expr::Tuple(ExprTuple { + Expr::Tuple(ast::ExprTuple { elts, ctx: _, range: _range, @@ -661,7 +674,7 @@ where } } - Expr::Slice(ExprSlice { + Expr::Slice(ast::ExprSlice { lower, upper, step, @@ -703,12 +716,12 @@ where } } -pub fn walk_excepthandler<'a, V>(visitor: &mut V, excepthandler: &'a Excepthandler) +pub fn walk_except_handler<'a, V>(visitor: &mut V, except_handler: &'a ExceptHandler) where V: PreorderVisitor<'a> + ?Sized, { - match excepthandler { - Excepthandler::ExceptHandler(ExcepthandlerExceptHandler { + match except_handler { + ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { range: _, type_, name: _, @@ -722,38 +735,27 @@ where } } +pub fn walk_format_spec<'a, V: PreorderVisitor<'a> + ?Sized>( + visitor: &mut V, + format_spec: &'a Expr, +) { + visitor.visit_expr(format_spec); +} + pub fn walk_arguments<'a, V>(visitor: &mut V, arguments: &'a Arguments) where V: PreorderVisitor<'a> + ?Sized, { - let non_default_args_len = - arguments.posonlyargs.len() + arguments.args.len() - arguments.defaults.len(); - - let mut args_iter = arguments.posonlyargs.iter().chain(&arguments.args); - - for _ in 0..non_default_args_len { - visitor.visit_arg(args_iter.next().unwrap()); - } - - for (arg, default) in args_iter.zip(&arguments.defaults) { - visitor.visit_arg(arg); - visitor.visit_expr(default); + for arg in arguments.posonlyargs.iter().chain(&arguments.args) { + visitor.visit_arg_with_default(arg); } if let Some(arg) = &arguments.vararg { visitor.visit_arg(arg); } - let non_default_kwargs_len = arguments.kwonlyargs.len() - arguments.kw_defaults.len(); - let mut kwargsonly_iter = arguments.kwonlyargs.iter(); - - for _ in 0..non_default_kwargs_len { - visitor.visit_arg(kwargsonly_iter.next().unwrap()); - } - - for (arg, default) in kwargsonly_iter.zip(&arguments.kw_defaults) { - visitor.visit_arg(arg); - visitor.visit_expr(default); + for arg in &arguments.kwonlyargs { + visitor.visit_arg_with_default(arg); } if let Some(arg) = &arguments.kwarg { @@ -770,6 +772,16 @@ where } } +pub fn walk_arg_with_default<'a, V>(visitor: &mut V, arg_with_default: &'a ArgWithDefault) +where + V: PreorderVisitor<'a> + ?Sized, +{ + visitor.visit_arg(&arg_with_default.def); + if let Some(expr) = &arg_with_default.default { + visitor.visit_expr(expr); + } +} + #[inline] pub fn walk_keyword<'a, V>(visitor: &mut V, keyword: &'a Keyword) where @@ -778,13 +790,13 @@ where visitor.visit_expr(&keyword.value); } -pub fn walk_withitem<'a, V>(visitor: &mut V, withitem: &'a Withitem) +pub fn walk_with_item<'a, V>(visitor: &mut V, with_item: &'a WithItem) where V: PreorderVisitor<'a> + ?Sized, { - visitor.visit_expr(&withitem.context_expr); + visitor.visit_expr(&with_item.context_expr); - if let Some(expr) = &withitem.optional_vars { + if let Some(expr) = &with_item.optional_vars { visitor.visit_expr(expr); } } @@ -805,19 +817,19 @@ where V: PreorderVisitor<'a> + ?Sized, { match pattern { - Pattern::MatchValue(PatternMatchValue { + Pattern::MatchValue(ast::PatternMatchValue { value, range: _range, }) => visitor.visit_expr(value), - Pattern::MatchSingleton(PatternMatchSingleton { + Pattern::MatchSingleton(ast::PatternMatchSingleton { value, range: _range, }) => { visitor.visit_constant(value); } - Pattern::MatchSequence(PatternMatchSequence { + Pattern::MatchSequence(ast::PatternMatchSequence { patterns, range: _range, }) => { @@ -826,7 +838,7 @@ where } } - Pattern::MatchMapping(PatternMatchMapping { + Pattern::MatchMapping(ast::PatternMatchMapping { keys, patterns, range: _, @@ -838,7 +850,7 @@ where } } - Pattern::MatchClass(PatternMatchClass { + Pattern::MatchClass(ast::PatternMatchClass { cls, patterns, kwd_attrs: _, @@ -857,7 +869,7 @@ where Pattern::MatchStar(_) => {} - Pattern::MatchAs(PatternMatchAs { + Pattern::MatchAs(ast::PatternMatchAs { pattern, range: _, name: _, @@ -867,7 +879,7 @@ where } } - Pattern::MatchOr(PatternMatchOr { + Pattern::MatchOr(ast::PatternMatchOr { patterns, range: _range, }) => { @@ -885,7 +897,7 @@ where { } -pub fn walk_boolop<'a, V>(_visitor: &mut V, _boolop: &'a Boolop) +pub fn walk_bool_op<'a, V>(_visitor: &mut V, _bool_op: &'a BoolOp) where V: PreorderVisitor<'a> + ?Sized, { @@ -899,14 +911,14 @@ where } #[inline] -pub fn walk_unaryop<'a, V>(_visitor: &mut V, _unaryop: &'a Unaryop) +pub fn walk_unary_op<'a, V>(_visitor: &mut V, _unary_op: &'a UnaryOp) where V: PreorderVisitor<'a> + ?Sized, { } #[inline] -pub fn walk_cmpop<'a, V>(_visitor: &mut V, _cmpop: &'a Cmpop) +pub fn walk_cmp_op<'a, V>(_visitor: &mut V, _cmp_op: &'a CmpOp) where V: PreorderVisitor<'a> + ?Sized, { @@ -921,18 +933,20 @@ where #[cfg(test)] mod tests { - use crate::node::AnyNodeRef; - use crate::visitor::preorder::{ - walk_alias, walk_arg, walk_arguments, walk_comprehension, walk_excepthandler, walk_expr, - walk_keyword, walk_match_case, walk_module, walk_pattern, walk_stmt, walk_type_ignore, - walk_withitem, Alias, Arg, Arguments, Boolop, Cmpop, Comprehension, Constant, - Excepthandler, Expr, Keyword, MatchCase, Mod, Operator, Pattern, PreorderVisitor, Stmt, - String, TypeIgnore, Unaryop, Withitem, - }; + use std::fmt::{Debug, Write}; + use insta::assert_snapshot; use rustpython_parser::lexer::lex; use rustpython_parser::{parse_tokens, Mode}; - use std::fmt::{Debug, Write}; + + use crate::node::AnyNodeRef; + use crate::visitor::preorder::{ + walk_alias, walk_arg, walk_arguments, walk_comprehension, walk_except_handler, walk_expr, + walk_keyword, walk_match_case, walk_module, walk_pattern, walk_stmt, walk_type_ignore, + walk_with_item, Alias, Arg, Arguments, BoolOp, CmpOp, Comprehension, Constant, + ExceptHandler, Expr, Keyword, MatchCase, Mod, Operator, Pattern, PreorderVisitor, Stmt, + TypeIgnore, UnaryOp, WithItem, + }; #[test] fn function_arguments() { @@ -1078,25 +1092,31 @@ class A: walk_expr(self, expr); self.exit_node(); } + fn visit_expr(&mut self, expr: &Expr) { self.enter_node(expr); walk_expr(self, expr); self.exit_node(); } + fn visit_constant(&mut self, constant: &Constant) { self.emit(&constant); } - fn visit_boolop(&mut self, boolop: &Boolop) { - self.emit(&boolop); + + fn visit_bool_op(&mut self, bool_op: &BoolOp) { + self.emit(&bool_op); } + fn visit_operator(&mut self, operator: &Operator) { self.emit(&operator); } - fn visit_unaryop(&mut self, unaryop: &Unaryop) { - self.emit(&unaryop); + + fn visit_unary_op(&mut self, unary_op: &UnaryOp) { + self.emit(&unary_op); } - fn visit_cmpop(&mut self, cmpop: &Cmpop) { - self.emit(&cmpop); + + fn visit_cmp_op(&mut self, cmp_op: &CmpOp) { + self.emit(&cmp_op); } fn visit_comprehension(&mut self, comprehension: &Comprehension) { @@ -1104,51 +1124,61 @@ class A: walk_comprehension(self, comprehension); self.exit_node(); } - fn visit_excepthandler(&mut self, excepthandler: &Excepthandler) { - self.enter_node(excepthandler); - walk_excepthandler(self, excepthandler); + + fn visit_except_handler(&mut self, except_handler: &ExceptHandler) { + self.enter_node(except_handler); + walk_except_handler(self, except_handler); self.exit_node(); } + fn visit_format_spec(&mut self, format_spec: &Expr) { self.enter_node(format_spec); walk_expr(self, format_spec); self.exit_node(); } + fn visit_arguments(&mut self, arguments: &Arguments) { self.enter_node(arguments); walk_arguments(self, arguments); self.exit_node(); } + fn visit_arg(&mut self, arg: &Arg) { self.enter_node(arg); walk_arg(self, arg); self.exit_node(); } + fn visit_keyword(&mut self, keyword: &Keyword) { self.enter_node(keyword); walk_keyword(self, keyword); self.exit_node(); } + fn visit_alias(&mut self, alias: &Alias) { self.enter_node(alias); walk_alias(self, alias); self.exit_node(); } - fn visit_withitem(&mut self, withitem: &Withitem) { - self.enter_node(withitem); - walk_withitem(self, withitem); + + fn visit_with_item(&mut self, with_item: &WithItem) { + self.enter_node(with_item); + walk_with_item(self, with_item); self.exit_node(); } + fn visit_match_case(&mut self, match_case: &MatchCase) { self.enter_node(match_case); walk_match_case(self, match_case); self.exit_node(); } + fn visit_pattern(&mut self, pattern: &Pattern) { self.enter_node(pattern); walk_pattern(self, pattern); self.exit_node(); } + fn visit_type_ignore(&mut self, type_ignore: &TypeIgnore) { self.enter_node(type_ignore); walk_type_ignore(self, type_ignore); diff --git a/crates/ruff_python_formatter/Cargo.toml b/crates/ruff_python_formatter/Cargo.toml index bd5526bc43..381c3ec6c9 100644 --- a/crates/ruff_python_formatter/Cargo.toml +++ b/crates/ruff_python_formatter/Cargo.toml @@ -2,8 +2,13 @@ name = "ruff_python_formatter" version = "0.0.0" publish = false +authors = { workspace = true } edition = { workspace = true } rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } [dependencies] ruff_formatter = { path = "../ruff_formatter" } @@ -12,6 +17,7 @@ ruff_python_ast = { path = "../ruff_python_ast" } ruff_text_size = { workspace = true } anyhow = { workspace = true } +bitflags = { workspace = true } clap = { workspace = true } countme = "3.0.1" is-macro = { workspace = true } @@ -19,10 +25,25 @@ itertools = { workspace = true } once_cell = { workspace = true } rustc-hash = { workspace = true } rustpython-parser = { workspace = true } +serde = { workspace = true, optional = true } +smallvec = { workspace = true } +thiserror = { workspace = true } +unic-ucd-ident = "0.9.0" [dev-dependencies] -ruff_testing_macros = { path = "../ruff_testing_macros" } +ruff_formatter = { path = "../ruff_formatter", features = ["serde"]} -insta = { workspace = true, features = [] } -test-case = { workspace = true } +insta = { workspace = true, features = ["glob"] } +serde = { workspace = true } +serde_json = { workspace = true } similar = { workspace = true } + +[[test]] +name = "ruff_python_formatter_fixtures" +path = "tests/fixtures.rs" +test = true +required-features = [ "serde" ] + +[features] +serde = ["dep:serde", "ruff_formatter/serde"] +default = ["serde"] diff --git a/crates/ruff_python_formatter/Docs.md b/crates/ruff_python_formatter/Docs.md deleted file mode 100644 index 326dd33b81..0000000000 --- a/crates/ruff_python_formatter/Docs.md +++ /dev/null @@ -1,8 +0,0 @@ -# Rust Python Formatter - -For the formatter, we would like to implement `Format` from the rust_formatter crate for all AST -nodes, defined in the rustpython_parser crate. This violates rust's orphan rules. We therefore -generate in `generate.py` a newtype for each AST node with implementations of `FormatNodeRule`, -`FormatRule`, `AsFormat` and `IntoFormat` on it. - -![excalidraw showing the relationships between the different types](orphan_rules_in_the_formatter.svg) diff --git a/crates/ruff_python_formatter/README.md b/crates/ruff_python_formatter/README.md new file mode 100644 index 0000000000..f4d63100e4 --- /dev/null +++ b/crates/ruff_python_formatter/README.md @@ -0,0 +1,294 @@ +# Rust Python Formatter + +The goal of our formatter is to be compatible with Black except for rare edge cases (mostly +involving comment placement). + +## Implementing a node + +Formatting each node follows roughly the same structure. We start with a `Format{{Node}}` struct +that implements Default (and `AsFormat`/`IntoFormat` impls in `generated.rs`, see orphan rules below). + +```rust +#[derive(Default)] +pub struct FormatStmtReturn; +``` + +We implement `FormatNodeRule<{{Node}}> for Format{{Node}}`. Inside, we destructure the item to make +sure we're not missing any field. If we want to write multiple items, we use an efficient `write!` +call, for single items `.format().fmt(f)` or `.fmt(f)` is sufficient. + +```rust +impl FormatNodeRule for FormatStmtReturn { + fn fmt_fields(&self, item: &StmtReturn, f: &mut PyFormatter) -> FormatResult<()> { + // Here we destructure item and make sure each field is listed. + // We generally don't need range is it's underscore-ignored + let StmtReturn { range: _, value } = item; + // Implement some formatting logic, in this case no space (and no value) after a return with + // no value + if let Some(value) = value { + write!( + f, + [ + text("return"), + // There are multiple different space and newline types (e.g. + // `soft_line_break_or_space()`, check the builders module), this one will + // always be translate to a normal ascii whitespace character + space(), + // `return a, b` is valid, but if it wraps we'd need parentheses. + // This is different from `(a, b).count(1)` where the parentheses around the + // tuple are mandatory + value.format().with_options(Parenthesize::IfBreaks) + ] + ) + } else { + text("return").fmt(f) + } + } +} +``` + +Check the `builders` module for the primitives that you can use. + +If something such as list or a tuple can break into multiple lines if it is too long for a single +line, wrap it into a `group`. Ignoring comments, we could format a tuple with two items like this: + +```rust +write!( + f, + [group(&format_args![ + text("("), + soft_block_indent(&format_args![ + item1.format() + text(","), + soft_line_break_or_space(), + item2.format(), + if_group_breaks(&text(",")) + ]), + text(")") + ])] +) +``` + +If everything fits on a single line, the group doesn't break and we get something like `("a", "b")`. +If it doesn't, we get something like + +```Python +( + "a", + "b", +) +``` + +For a list of expression, you don't need to format it manually but can use the `JoinBuilder` util, +accessible through `.join_with`. Finish will write to the formatter internally. + +```rust +f.join_with(&format_args!(text(","), soft_line_break_or_space())) + .entries(self.elts.iter().formatted()) + .finish()?; +// Here we need a trailing comma on the last entry of an expanded group since we have more +// than one element +write!(f, [if_group_breaks(&text(","))]) +``` + +If you need avoid second mutable borrows with a builder, you can use `format_with(|f| { ... })` as +a formattable element similar to `text()` or `group()`. + +## Comments + +Comments can either be own line or end-of-line and can be marked as `Leading`, `Trailing` and `Dangling`. + +```python +# Leading comment (always own line) +print("hello world") # Trailing comment (end-of-line) +# Trailing comment (own line) +``` + +Comments are automatically attached as `Leading` or `Trailing` to a node close to them, or `Dangling` +if there are only tokens and no nodes surrounding it. Categorization is automatic but sometimes +needs to be overridden in +[`place_comment`](https://github.com/astral-sh/ruff/blob/be11cae619d5a24adb4da34e64d3c5f270f9727b/crates/ruff_python_formatter/src/comments/placement.rs#L13) +in `placement.rs`, which this section is about. + +```Python +[ + # This needs to be handled as a dangling comment +] +``` + +Here, the comment is dangling because it is preceded by `[`, which is a non-trivia token but not a +node, and followed by `]`, which is also a non-trivia token but not a node. In the `FormatExprList` +implementation, we have to call `dangling_comments` manually and stub out the +`fmt_dangling_comments` default from `FormatNodeRule`. + +```rust +impl FormatNodeRule for FormatExprList { + fn fmt_fields(&self, item: &ExprList, f: &mut PyFormatter) -> FormatResult<()> { + // ... + + write!( + f, + [group(&format_args![ + text("["), + dangling_comments(dangling), // Gets all the comments marked as dangling for the node + soft_block_indent(&items), + text("]") + ])] + ) + } + + fn fmt_dangling_comments(&self, _node: &ExprList, _f: &mut PyFormatter) -> FormatResult<()> { + // Handled as part of `fmt_fields` + Ok(()) + } +} +``` + +A related common challenge is that we want to attach comments to tokens (think keywords and +syntactically meaningful characters such as `:`) that have no node on their own. A slightly +simplified version of the `while` node in our AST looks like the following: + +```rust +pub struct StmtWhile { + pub range: TextRange, + pub test: Box>, + pub body: Vec>, + pub orelse: Vec>, +} +``` + +That means in + +```python +while True: # Trailing condition comment + if f(): + break + # trailing while comment +# leading else comment +else: + print("while-else") +``` + +the `else` has no node, we're just getting the statements in its body. + +The preceding token of the leading else comment is the `break`, which has a node, the following +token is the `else`, which lacks a node, so by default the comment would be marked as trailing +the `break` and wrongly formatted as such. We can identify these cases by looking for comments +between two bodies that have the same indentation level as the keyword, e.g. in our case the +leading else comment is inside the `while` node (which spans the entire snippet) and on the same +level as the `else`. We identify those case in +[`handle_in_between_bodies_own_line_comment`](https://github.com/astral-sh/ruff/blob/be11cae619d5a24adb4da34e64d3c5f270f9727b/crates/ruff_python_formatter/src/comments/placement.rs#L196) +and mark them as dangling for manual formatting later. Similarly, we find and mark comment after +the colon(s) in +[`handle_trailing_end_of_line_condition_comment`](https://github.com/astral-sh/ruff/blob/main/crates/ruff_python_formatter/src/comments/placement.rs#L518) +. + +The comments don't carry any extra information such as why we marked the comment as trailing, +instead they are sorted into one list of leading, one list of trailing and one list of dangling +comments per node. In `FormatStmtWhile`, we can have multiple types of dangling comments, so we +have to split the dangling list into after-colon-comments, before-else-comments, etc. by some +element separating them (e.g. all comments trailing the colon come before the first statement in +the body) and manually insert them in the right position. + +A simplified implementation with only those two kinds of comments: + +```rust +fn fmt_fields(&self, item: &StmtWhile, f: &mut PyFormatter) -> FormatResult<()> { + + // ... + + // See FormatStmtWhile for the real, more complex implementation + let first_while_body_stmt = item.body.first().unwrap().end(); + let trailing_condition_comments_end = + dangling_comments.partition_point(|comment| comment.slice().end() < first_while_body_stmt); + let (trailing_condition_comments, or_else_comments) = + dangling_comments.split_at(trailing_condition_comments_end); + + write!( + f, + [ + text("while"), + space(), + test.format(), + text(":"), + trailing_comments(trailing_condition_comments), + block_indent(&body.format()) + leading_comments(or_else_comments), + text("else:"), + block_indent(&orelse.format()) + ] + )?; +} +``` + +## Development notes + +Handling parentheses and comments are two major challenges in a Python formatter. + +We have copied the majority of tests over from Black and use [insta](https://insta.rs/docs/cli/) for +snapshot testing with the diff between Ruff and Black, Black output and Ruff output. We put +additional test cases in `resources/test/fixtures/ruff`. + +The full Ruff test suite is slow, `cargo test -p ruff_python_formatter` is a lot faster. + +There is a `ruff_python_formatter` binary that avoid building and linking the main `ruff` crate. + +You can use `scratch.py` as a playground, e.g. +`cargo run --bin ruff_python_formatter -- --emit stdout scratch.py`, which additional `--print-ir` +and `--print-comments` options. + +The origin of Ruff's formatter is the [Rome formatter](https://github.com/rome/tools/tree/main/crates/rome_json_formatter), +e.g. the ruff_formatter crate is forked from the [rome_formatter crate](https://github.com/rome/tools/tree/main/crates/rome_formatter). +The Rome repository can be a helpful reference when implementing something in the Ruff formatter + +### Checking entire projects + +It's possible to format an entire project: + +```shell +cargo run --bin ruff_dev -- format-dev --write my_project +``` + +This will format all files that `ruff check` would lint and computes the similarity index, the +fraction of changed lines. The similarity index is 1 if there were no changes at all, while 0 means +we changed every single line. If you run this on a black formatted projects, this tells you how +similar the ruff formatter is to black for the given project, with our goal being as close to 1 as +possible. + +There are three common problems with the formatter: The second formatting pass looks different than +the first (formatter instability or lack of idempotency), we print invalid syntax (e.g. missing +parentheses around multiline expressions) and panics (mostly in debug assertions). We test for all +of these using the `--stability-check` option in the `format-dev` subcommand: + +The easiest is to check CPython: + +```shell +git clone --branch 3.10 https://github.com/python/cpython.git crates/ruff/resources/test/cpython +cargo run --bin ruff_dev -- format-dev --stability-check crates/ruff/resources/test/cpython +``` + +It is also possible large number of repositories using ruff. This dataset is large (~60GB), so we +only do this occasionally: + +```shell +curl https://raw.githubusercontent.com/akx/ruff-usage-aggregate/master/data/known-github-tomls.jsonl > github_search.jsonl +python scripts/check_ecosystem.py --checkouts target/checkouts --projects github_search.jsonl -v $(which true) $(which true) +cargo run --bin ruff_dev -- format-dev --stability-check --multi-project target/checkouts +``` + +Compared to `ruff check`, `cargo run --bin ruff_dev -- format-dev` has 4 additional options: + +- `--write`: Format the files and write them back to disk +- `--stability-check`: Format twice (but don't write to disk) and check for differences and crashes +- `--multi-project`: Treat every subdirectory as a separate project. Useful for ecosystem checks. +- `--error-file`: Use together with `--multi-project`, this writes all errors (but not status + messages) to a file. + +## The orphan rules and trait structure + +For the formatter, we would like to implement `Format` from the rust_formatter crate for all AST +nodes, defined in the rustpython_parser crate. This violates Rust's orphan rules. We therefore +generate in `generate.py` a newtype for each AST node with implementations of `FormatNodeRule`, +`FormatRule`, `AsFormat` and `IntoFormat` on it. + +![excalidraw showing the relationships between the different types](orphan_rules_in_the_formatter.svg) diff --git a/crates/ruff_python_formatter/generate.py b/crates/ruff_python_formatter/generate.py index f4e83d2edf..ad94528941 100644 --- a/crates/ruff_python_formatter/generate.py +++ b/crates/ruff_python_formatter/generate.py @@ -28,7 +28,10 @@ nodes_file = ( node_lines = ( nodes_file.split("pub enum AnyNode {")[1].split("}")[0].strip().splitlines() ) -nodes = [node_line.split("(")[1].split("<")[0] for node_line in node_lines] +nodes = [ + node_line.split("(")[1].split(")")[0].split("::")[-1].split("<")[0] + for node_line in node_lines +] print(nodes) # %% @@ -133,7 +136,7 @@ for node in nodes: fn format(&self) -> Self::Format<'_> {{ FormatRefWithRule::new( self, - crate::{groups[group_for_node(node)]}::{to_camel_case(node)}::Format{node}::default(), + crate::{groups[group_for_node(node)]}::{to_camel_case(node)}::Format{node}, ) }} }} @@ -146,7 +149,7 @@ for node in nodes: fn into_format(self) -> Self::Format {{ FormatOwnedWithRule::new( self, - crate::{groups[group_for_node(node)]}::{to_camel_case(node)}::Format{node}::default(), + crate::{groups[group_for_node(node)]}::{to_camel_case(node)}::Format{node}, ) }} }} diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/conditional_expression.py b/crates/ruff_python_formatter/resources/test/fixtures/black/conditional_expression.py new file mode 100644 index 0000000000..bbe56623c6 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/conditional_expression.py @@ -0,0 +1,67 @@ +long_kwargs_single_line = my_function( + foo="test, this is a sample value", + bar=some_long_value_name_foo_bar_baz if some_boolean_variable else some_fallback_value_foo_bar_baz, + baz="hello, this is a another value", +) + +multiline_kwargs_indented = my_function( + foo="test, this is a sample value", + bar=some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz, + baz="hello, this is a another value", +) + +imploding_kwargs = my_function( + foo="test, this is a sample value", + bar=a + if foo + else b, + baz="hello, this is a another value", +) + +imploding_line = ( + 1 + if 1 + 1 == 2 + else 0 +) + +exploding_line = "hello this is a slightly long string" if some_long_value_name_foo_bar_baz else "this one is a little shorter" + +positional_argument_test(some_long_value_name_foo_bar_baz if some_boolean_variable else some_fallback_value_foo_bar_baz) + +def weird_default_argument(x=some_long_value_name_foo_bar_baz + if SOME_CONSTANT + else some_fallback_value_foo_bar_baz): + pass + +nested = "hello this is a slightly long string" if (some_long_value_name_foo_bar_baz if + nesting_test_expressions else some_fallback_value_foo_bar_baz) \ + else "this one is a little shorter" + +generator_expression = ( + some_long_value_name_foo_bar_baz if some_boolean_variable else some_fallback_value_foo_bar_baz for some_boolean_variable in some_iterable +) + + +def limit_offset_sql(self, low_mark, high_mark): + """Return LIMIT/OFFSET SQL clause.""" + limit, offset = self._get_limit_offset_params(low_mark, high_mark) + return " ".join( + sql + for sql in ( + "LIMIT %d" % limit if limit else None, + ("OFFSET %d" % offset) if offset else None, + ) + if sql + ) + + +def something(): + clone._iterable_class = ( + NamedValuesListIterable + if named + else FlatValuesListIterable + if flat + else ValuesListIterable + ) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/conditional_expression.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/conditional_expression.py.expect new file mode 100644 index 0000000000..122ea7860d --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/conditional_expression.py.expect @@ -0,0 +1,90 @@ +long_kwargs_single_line = my_function( + foo="test, this is a sample value", + bar=( + some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz + ), + baz="hello, this is a another value", +) + +multiline_kwargs_indented = my_function( + foo="test, this is a sample value", + bar=( + some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz + ), + baz="hello, this is a another value", +) + +imploding_kwargs = my_function( + foo="test, this is a sample value", + bar=a if foo else b, + baz="hello, this is a another value", +) + +imploding_line = 1 if 1 + 1 == 2 else 0 + +exploding_line = ( + "hello this is a slightly long string" + if some_long_value_name_foo_bar_baz + else "this one is a little shorter" +) + +positional_argument_test( + some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz +) + + +def weird_default_argument( + x=( + some_long_value_name_foo_bar_baz + if SOME_CONSTANT + else some_fallback_value_foo_bar_baz + ), +): + pass + + +nested = ( + "hello this is a slightly long string" + if ( + some_long_value_name_foo_bar_baz + if nesting_test_expressions + else some_fallback_value_foo_bar_baz + ) + else "this one is a little shorter" +) + +generator_expression = ( + ( + some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz + ) + for some_boolean_variable in some_iterable +) + + +def limit_offset_sql(self, low_mark, high_mark): + """Return LIMIT/OFFSET SQL clause.""" + limit, offset = self._get_limit_offset_params(low_mark, high_mark) + return " ".join( + sql + for sql in ( + "LIMIT %d" % limit if limit else None, + ("OFFSET %d" % offset) if offset else None, + ) + if sql + ) + + +def something(): + clone._iterable_class = ( + NamedValuesListIterable + if named + else FlatValuesListIterable if flat else ValuesListIterable + ) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/blackd_diff.py b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/blackd_diff.py new file mode 100644 index 0000000000..c5278325db --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/blackd_diff.py @@ -0,0 +1,6 @@ +def abc (): + return ["hello", "world", + "!"] + +print( "Incorrect formatting" +) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/blackd_diff.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/blackd_diff.py.expect new file mode 100644 index 0000000000..c5278325db --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/blackd_diff.py.expect @@ -0,0 +1,6 @@ +def abc (): + return ["hello", "world", + "!"] + +print( "Incorrect formatting" +) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/debug_visitor.py b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/debug_visitor.py new file mode 100644 index 0000000000..d1d1ba1216 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/debug_visitor.py @@ -0,0 +1,32 @@ +@dataclass +class DebugVisitor(Visitor[T]): + tree_depth: int = 0 + + def visit_default(self, node: LN) -> Iterator[T]: + indent = ' ' * (2 * self.tree_depth) + if isinstance(node, Node): + _type = type_repr(node.type) + out(f'{indent}{_type}', fg='yellow') + self.tree_depth += 1 + for child in node.children: + yield from self.visit(child) + + self.tree_depth -= 1 + out(f'{indent}/{_type}', fg='yellow', bold=False) + else: + _type = token.tok_name.get(node.type, str(node.type)) + out(f'{indent}{_type}', fg='blue', nl=False) + if node.prefix: + # We don't have to handle prefixes for `Node` objects since + # that delegates to the first child anyway. + out(f' {node.prefix!r}', fg='green', bold=False, nl=False) + out(f' {node.value!r}', fg='blue', bold=False) + + @classmethod + def show(cls, code: str) -> None: + """Pretty-prints a given string of `code`. + + Convenience method for debugging. + """ + v: DebugVisitor[None] = DebugVisitor() + list(v.visit(lib2to3_parse(code))) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/debug_visitor.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/debug_visitor.py.expect new file mode 100644 index 0000000000..d1d1ba1216 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/debug_visitor.py.expect @@ -0,0 +1,32 @@ +@dataclass +class DebugVisitor(Visitor[T]): + tree_depth: int = 0 + + def visit_default(self, node: LN) -> Iterator[T]: + indent = ' ' * (2 * self.tree_depth) + if isinstance(node, Node): + _type = type_repr(node.type) + out(f'{indent}{_type}', fg='yellow') + self.tree_depth += 1 + for child in node.children: + yield from self.visit(child) + + self.tree_depth -= 1 + out(f'{indent}/{_type}', fg='yellow', bold=False) + else: + _type = token.tok_name.get(node.type, str(node.type)) + out(f'{indent}{_type}', fg='blue', nl=False) + if node.prefix: + # We don't have to handle prefixes for `Node` objects since + # that delegates to the first child anyway. + out(f' {node.prefix!r}', fg='green', bold=False, nl=False) + out(f' {node.value!r}', fg='blue', bold=False) + + @classmethod + def show(cls, code: str) -> None: + """Pretty-prints a given string of `code`. + + Convenience method for debugging. + """ + v: DebugVisitor[None] = DebugVisitor() + list(v.visit(lib2to3_parse(code))) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/decorators.py b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/decorators.py new file mode 100644 index 0000000000..46e37f69ed --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/decorators.py @@ -0,0 +1,150 @@ +# This file doesn't use the standard decomposition. +# Decorator syntax test cases are separated by double # comments. +# Those before the 'output' comment are valid under the old syntax. +# Those after the 'ouput' comment require PEP614 relaxed syntax. +# Do not remove the double # separator before the first test case, it allows +# the comment before the test case to be ignored. + +## + +@decorator +def f(): + ... + +## + +@decorator() +def f(): + ... + +## + +@decorator(arg) +def f(): + ... + +## + +@decorator(kwarg=0) +def f(): + ... + +## + +@decorator(*args) +def f(): + ... + +## + +@decorator(**kwargs) +def f(): + ... + +## + +@decorator(*args, **kwargs) +def f(): + ... + +## + +@decorator(*args, **kwargs,) +def f(): + ... + +## + +@dotted.decorator +def f(): + ... + +## + +@dotted.decorator(arg) +def f(): + ... + +## + +@dotted.decorator(kwarg=0) +def f(): + ... + +## + +@dotted.decorator(*args) +def f(): + ... + +## + +@dotted.decorator(**kwargs) +def f(): + ... + +## + +@dotted.decorator(*args, **kwargs) +def f(): + ... + +## + +@dotted.decorator(*args, **kwargs,) +def f(): + ... + +## + +@double.dotted.decorator +def f(): + ... + +## + +@double.dotted.decorator(arg) +def f(): + ... + +## + +@double.dotted.decorator(kwarg=0) +def f(): + ... + +## + +@double.dotted.decorator(*args) +def f(): + ... + +## + +@double.dotted.decorator(**kwargs) +def f(): + ... + +## + +@double.dotted.decorator(*args, **kwargs) +def f(): + ... + +## + +@double.dotted.decorator(*args, **kwargs,) +def f(): + ... + +## + +@_(sequence["decorator"]) +def f(): + ... + +## + +@eval("sequence['decorator']") +def f(): + ... diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/decorators.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/decorators.py.expect new file mode 100644 index 0000000000..df17e1e749 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/decorators.py.expect @@ -0,0 +1,29 @@ +## + +@decorator()() +def f(): + ... + +## + +@(decorator) +def f(): + ... + +## + +@sequence["decorator"] +def f(): + ... + +## + +@decorator[List[str]] +def f(): + ... + +## + +@var := decorator +def f(): + ... diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/docstring_no_string_normalization.py b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/docstring_no_string_normalization.py new file mode 100644 index 0000000000..3116529c65 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/docstring_no_string_normalization.py @@ -0,0 +1,123 @@ +class ALonelyClass: + ''' + A multiline class docstring. + ''' + def AnEquallyLonelyMethod(self): + ''' + A multiline method docstring''' + pass + + +def one_function(): + '''This is a docstring with a single line of text.''' + pass + + +def shockingly_the_quotes_are_normalized(): + '''This is a multiline docstring. + This is a multiline docstring. + This is a multiline docstring. + ''' + pass + + +def foo(): + """This is a docstring with + some lines of text here + """ + return + + +def baz(): + '''"This" is a string with some + embedded "quotes"''' + return + + +def poit(): + """ + Lorem ipsum dolor sit amet. + + Consectetur adipiscing elit: + - sed do eiusmod tempor incididunt ut labore + - dolore magna aliqua + - enim ad minim veniam + - quis nostrud exercitation ullamco laboris nisi + - aliquip ex ea commodo consequat + """ + pass + + +def under_indent(): + """ + These lines are indented in a way that does not +make sense. + """ + pass + + +def over_indent(): + """ + This has a shallow indent + - But some lines are deeper + - And the closing quote is too deep + """ + pass + + +def single_line(): + """But with a newline after it! + + """ + pass + + +def this(): + r""" + 'hey ho' + """ + + +def that(): + """ "hey yah" """ + + +def and_that(): + """ + "hey yah" """ + + +def and_this(): + ''' + "hey yah"''' + + +def believe_it_or_not_this_is_in_the_py_stdlib(): ''' +"hey yah"''' + + +def shockingly_the_quotes_are_normalized_v2(): + ''' + Docstring Docstring Docstring + ''' + pass + + +def backslash_space(): + '\ ' + + +def multiline_backslash_1(): + ''' + hey\there\ + \ ''' + + +def multiline_backslash_2(): + ''' + hey there \ ''' + + +def multiline_backslash_3(): + ''' + already escaped \\ ''' diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/docstring_no_string_normalization.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/docstring_no_string_normalization.py.expect new file mode 100644 index 0000000000..8aefa4b2c2 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/docstring_no_string_normalization.py.expect @@ -0,0 +1,123 @@ +class ALonelyClass: + ''' + A multiline class docstring. + ''' + + def AnEquallyLonelyMethod(self): + ''' + A multiline method docstring''' + pass + + +def one_function(): + '''This is a docstring with a single line of text.''' + pass + + +def shockingly_the_quotes_are_normalized(): + '''This is a multiline docstring. + This is a multiline docstring. + This is a multiline docstring. + ''' + pass + + +def foo(): + """This is a docstring with + some lines of text here + """ + return + + +def baz(): + '''"This" is a string with some + embedded "quotes"''' + return + + +def poit(): + """ + Lorem ipsum dolor sit amet. + + Consectetur adipiscing elit: + - sed do eiusmod tempor incididunt ut labore + - dolore magna aliqua + - enim ad minim veniam + - quis nostrud exercitation ullamco laboris nisi + - aliquip ex ea commodo consequat + """ + pass + + +def under_indent(): + """ + These lines are indented in a way that does not + make sense. + """ + pass + + +def over_indent(): + """ + This has a shallow indent + - But some lines are deeper + - And the closing quote is too deep + """ + pass + + +def single_line(): + """But with a newline after it!""" + pass + + +def this(): + r""" + 'hey ho' + """ + + +def that(): + """ "hey yah" """ + + +def and_that(): + """ + "hey yah" """ + + +def and_this(): + ''' + "hey yah"''' + + +def believe_it_or_not_this_is_in_the_py_stdlib(): + ''' + "hey yah"''' + + +def shockingly_the_quotes_are_normalized_v2(): + ''' + Docstring Docstring Docstring + ''' + pass + + +def backslash_space(): + '\ ' + + +def multiline_backslash_1(): + ''' + hey\there\ + \ ''' + + +def multiline_backslash_2(): + ''' + hey there \ ''' + + +def multiline_backslash_3(): + ''' + already escaped \\''' diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/docstring_preview_no_string_normalization.py b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/docstring_preview_no_string_normalization.py new file mode 100644 index 0000000000..338cc01d33 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/docstring_preview_no_string_normalization.py @@ -0,0 +1,10 @@ +def do_not_touch_this_prefix(): + R"""There was a bug where docstring prefixes would be normalized even with -S.""" + + +def do_not_touch_this_prefix2(): + FR'There was a bug where docstring prefixes would be normalized even with -S.' + + +def do_not_touch_this_prefix3(): + u'''There was a bug where docstring prefixes would be normalized even with -S.''' diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/docstring_preview_no_string_normalization.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/docstring_preview_no_string_normalization.py.expect new file mode 100644 index 0000000000..338cc01d33 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/docstring_preview_no_string_normalization.py.expect @@ -0,0 +1,10 @@ +def do_not_touch_this_prefix(): + R"""There was a bug where docstring prefixes would be normalized even with -S.""" + + +def do_not_touch_this_prefix2(): + FR'There was a bug where docstring prefixes would be normalized even with -S.' + + +def do_not_touch_this_prefix3(): + u'''There was a bug where docstring prefixes would be normalized even with -S.''' diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/force_py36.py b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/force_py36.py new file mode 100644 index 0000000000..106e97214d --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/force_py36.py @@ -0,0 +1,3 @@ +# The input source must not contain any Py36-specific syntax (e.g. argument type +# annotations, trailing comma after *rest) or this test becomes invalid. +def long_function_name(argument_one, argument_two, argument_three, argument_four, argument_five, argument_six, *rest): ... diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/force_py36.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/force_py36.py.expect new file mode 100644 index 0000000000..bb26932707 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/force_py36.py.expect @@ -0,0 +1,12 @@ +# The input source must not contain any Py36-specific syntax (e.g. argument type +# annotations, trailing comma after *rest) or this test becomes invalid. +def long_function_name( + argument_one, + argument_two, + argument_three, + argument_four, + argument_five, + argument_six, + *rest, +): + ... diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/force_pyi.py b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/force_pyi.py new file mode 100644 index 0000000000..9c8c40cc96 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/force_pyi.py @@ -0,0 +1,30 @@ +from typing import Union + +@bird +def zoo(): ... + +class A: ... +@bar +class B: + def BMethod(self) -> None: ... + @overload + def BMethod(self, arg : List[str]) -> None: ... + +class C: ... +@hmm +class D: ... +class E: ... + +@baz +def foo() -> None: + ... + +class F (A , C): ... +def spam() -> None: ... + +@overload +def spam(arg: str) -> str: ... + +var : int = 1 + +def eggs() -> Union[str, int]: ... diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/force_pyi.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/force_pyi.py.expect new file mode 100644 index 0000000000..4349ba0a53 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/force_pyi.py.expect @@ -0,0 +1,32 @@ +from typing import Union + +@bird +def zoo(): ... + +class A: ... + +@bar +class B: + def BMethod(self) -> None: ... + @overload + def BMethod(self, arg: List[str]) -> None: ... + +class C: ... + +@hmm +class D: ... + +class E: ... + +@baz +def foo() -> None: ... + +class F(A, C): ... + +def spam() -> None: ... +@overload +def spam(arg: str) -> str: ... + +var: int = 1 + +def eggs() -> Union[str, int]: ... diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/linelength6.py b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/linelength6.py new file mode 100644 index 0000000000..4fb342726f --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/linelength6.py @@ -0,0 +1,5 @@ +# Regression test for #3427, which reproes only with line length <= 6 +def f(): + """ + x + """ diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/linelength6.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/linelength6.py.expect new file mode 100644 index 0000000000..4fb342726f --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/linelength6.py.expect @@ -0,0 +1,5 @@ +# Regression test for #3427, which reproes only with line length <= 6 +def f(): + """ + x + """ diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/long_strings_flag_disabled.py b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/long_strings_flag_disabled.py new file mode 100644 index 0000000000..db3954e3ab --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/long_strings_flag_disabled.py @@ -0,0 +1,292 @@ +x = "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." + +x += "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." + +y = "Short string" + +print( + "This is a really long string inside of a print statement with extra arguments attached at the end of it.", + x, + y, + z, +) + +print( + "This is a really long string inside of a print statement with no extra arguments attached at the end of it." +) + +D1 = { + "The First": "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + "The Second": "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D2 = { + 1.0: "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + 2.0: "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D3 = { + x: "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + y: "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D4 = { + "A long and ridiculous {}".format( + string_key + ): "This is a really really really long string that has to go i,side of a dictionary. It is soooo bad.", + some_func( + "calling", "some", "stuff" + ): "This is a really really really long string that has to go inside of a dictionary. It is {soooo} bad (#{x}).".format( + sooo="soooo", x=2 + ), + "A %s %s" + % ( + "formatted", + "string", + ): "This is a really really really long string that has to go inside of a dictionary. It is %s bad (#%d)." + % ("soooo", 2), +} + +func_with_keywords( + my_arg, + my_kwarg="Long keyword strings also need to be wrapped, but they will probably need to be handled a little bit differently.", +) + +bad_split1 = ( + "But what should happen when code has already been formatted but in the wrong way? Like" + " with a space at the end instead of the beginning. Or what about when it is split too soon?" +) + +bad_split2 = ( + "But what should happen when code has already " + "been formatted but in the wrong way? Like " + "with a space at the end instead of the " + "beginning. Or what about when it is split too " + "soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split." +) + +bad_split3 = ( + "What if we have inline comments on " # First Comment + "each line of a bad split? In that " # Second Comment + "case, we should just leave it alone." # Third Comment +) + +bad_split_func1( + "But what should happen when code has already " + "been formatted but in the wrong way? Like " + "with a space at the end instead of the " + "beginning. Or what about when it is split too " + "soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split.", + xxx, + yyy, + zzz, +) + +bad_split_func2( + xxx, + yyy, + zzz, + long_string_kwarg="But what should happen when code has already been formatted but in the wrong way? Like " + "with a space at the end instead of the beginning. Or what about when it is split too " + "soon?", +) + +bad_split_func3( + ( + "But what should happen when code has already " + r"been formatted but in the wrong way? Like " + "with a space at the end instead of the " + r"beginning. Or what about when it is split too " + r"soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split." + ), + xxx, + yyy, + zzz, +) + +raw_string = r"This is a long raw string. When re-formatting this string, black needs to make sure it prepends the 'r' onto the new string." + +fmt_string1 = "We also need to be sure to preserve any and all {} which may or may not be attached to the string in question.".format( + "method calls" +) + +fmt_string2 = "But what about when the string is {} but {}".format( + "short", + "the method call is really really really really really really really really long?", +) + +old_fmt_string1 = ( + "While we are on the topic of %s, we should also note that old-style formatting must also be preserved, since some %s still uses it." + % ("formatting", "code") +) + +old_fmt_string2 = "This is a %s %s %s %s" % ( + "really really really really really", + "old", + "way to format strings!", + "Use f-strings instead!", +) + +old_fmt_string3 = ( + "Whereas only the strings after the percent sign were long in the last example, this example uses a long initial string as well. This is another %s %s %s %s" + % ( + "really really really really really", + "old", + "way to format strings!", + "Use f-strings instead!", + ) +) + +fstring = f"f-strings definitely make things more {difficult} than they need to be for {{black}}. But boy they sure are handy. The problem is that some lines will need to have the 'f' whereas others do not. This {line}, for example, needs one." + +fstring_with_no_fexprs = f"Some regular string that needs to get split certainly but is NOT an fstring by any means whatsoever." + +comment_string = "Long lines with inline comments should have their comments appended to the reformatted string's enclosing right parentheses." # This comment gets thrown to the top. + +arg_comment_string = print( + "Long lines with inline comments which are apart of (and not the only member of) an argument list should have their comments appended to the reformatted string's enclosing left parentheses.", # This comment stays on the bottom. + "Arg #2", + "Arg #3", + "Arg #4", + "Arg #5", +) + +pragma_comment_string1 = "Lines which end with an inline pragma comment of the form `# : <...>` should be left alone." # noqa: E501 + +pragma_comment_string2 = "Lines which end with an inline pragma comment of the form `# : <...>` should be left alone." # noqa + +"""This is a really really really long triple quote string and it should not be touched.""" + +triple_quote_string = """This is a really really really long triple quote string assignment and it should not be touched.""" + +assert ( + some_type_of_boolean_expression +), "Followed by a really really really long string that is used to provide context to the AssertionError exception." + +assert ( + some_type_of_boolean_expression +), "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic string {}.".format( + "formatting" +) + +assert some_type_of_boolean_expression, ( + "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic string %s." + % "formatting" +) + +assert some_type_of_boolean_expression, ( + "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic %s %s." + % ("string", "formatting") +) + +some_function_call( + "With a reallly generic name and with a really really long string that is, at some point down the line, " + + added + + " to a variable and then added to another string." +) + +some_function_call( + "With a reallly generic name and with a really really long string that is, at some point down the line, " + + added + + " to a variable and then added to another string. But then what happens when the final string is also supppppperrrrr long?! Well then that second (realllllllly long) string should be split too.", + "and a second argument", + and_a_third, +) + +return "A really really really really really really really really really really really really really long {} {}".format( + "return", "value" +) + +func_with_bad_comma( + "This is a really long string argument to a function that has a trailing comma which should NOT be there.", +) + +func_with_bad_comma( + "This is a really long string argument to a function that has a trailing comma which should NOT be there.", # comment after comma +) + +func_with_bad_comma( + ( + "This is a really long string argument to a function that has a trailing comma" + " which should NOT be there." + ), +) + +func_with_bad_comma( + ( + "This is a really long string argument to a function that has a trailing comma" + " which should NOT be there." + ), # comment after comma +) + +func_with_bad_parens_that_wont_fit_in_one_line( + ("short string that should have parens stripped"), x, y, z +) + +func_with_bad_parens_that_wont_fit_in_one_line( + x, y, ("short string that should have parens stripped"), z +) + +func_with_bad_parens( + ("short string that should have parens stripped"), + x, + y, + z, +) + +func_with_bad_parens( + x, + y, + ("short string that should have parens stripped"), + z, +) + +annotated_variable: Final = ( + "This is a large " + + STRING + + " that has been " + + CONCATENATED + + "using the '+' operator." +) +annotated_variable: Final = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." +annotated_variable: Literal[ + "fakse_literal" +] = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." + +backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\" +backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\\\" +backslashes = "This is a really 'long' string with \"embedded double quotes\" and 'single' quotes that also handles checking for an odd number of backslashes \\\", like this...\\\\\\" + +short_string = "Hi" " there." + +func_call(short_string=("Hi" " there.")) + +raw_strings = r"Don't" " get" r" merged" " unless they are all raw." + + +def foo(): + yield "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." + + +x = f"This is a {{really}} long string that needs to be split without a doubt (i.e. most definitely). In short, this {string} that can't possibly be {{expected}} to fit all together on one line. In {fact} it may even take up three or more lines... like four or five... but probably just four." + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # type: ignore + " of it." +) + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # noqa + " of it." +) + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # pylint: disable=some-pylint-check + " of it." +) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/long_strings_flag_disabled.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/long_strings_flag_disabled.py.expect new file mode 100644 index 0000000000..db3954e3ab --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/long_strings_flag_disabled.py.expect @@ -0,0 +1,292 @@ +x = "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." + +x += "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." + +y = "Short string" + +print( + "This is a really long string inside of a print statement with extra arguments attached at the end of it.", + x, + y, + z, +) + +print( + "This is a really long string inside of a print statement with no extra arguments attached at the end of it." +) + +D1 = { + "The First": "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + "The Second": "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D2 = { + 1.0: "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + 2.0: "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D3 = { + x: "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + y: "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D4 = { + "A long and ridiculous {}".format( + string_key + ): "This is a really really really long string that has to go i,side of a dictionary. It is soooo bad.", + some_func( + "calling", "some", "stuff" + ): "This is a really really really long string that has to go inside of a dictionary. It is {soooo} bad (#{x}).".format( + sooo="soooo", x=2 + ), + "A %s %s" + % ( + "formatted", + "string", + ): "This is a really really really long string that has to go inside of a dictionary. It is %s bad (#%d)." + % ("soooo", 2), +} + +func_with_keywords( + my_arg, + my_kwarg="Long keyword strings also need to be wrapped, but they will probably need to be handled a little bit differently.", +) + +bad_split1 = ( + "But what should happen when code has already been formatted but in the wrong way? Like" + " with a space at the end instead of the beginning. Or what about when it is split too soon?" +) + +bad_split2 = ( + "But what should happen when code has already " + "been formatted but in the wrong way? Like " + "with a space at the end instead of the " + "beginning. Or what about when it is split too " + "soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split." +) + +bad_split3 = ( + "What if we have inline comments on " # First Comment + "each line of a bad split? In that " # Second Comment + "case, we should just leave it alone." # Third Comment +) + +bad_split_func1( + "But what should happen when code has already " + "been formatted but in the wrong way? Like " + "with a space at the end instead of the " + "beginning. Or what about when it is split too " + "soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split.", + xxx, + yyy, + zzz, +) + +bad_split_func2( + xxx, + yyy, + zzz, + long_string_kwarg="But what should happen when code has already been formatted but in the wrong way? Like " + "with a space at the end instead of the beginning. Or what about when it is split too " + "soon?", +) + +bad_split_func3( + ( + "But what should happen when code has already " + r"been formatted but in the wrong way? Like " + "with a space at the end instead of the " + r"beginning. Or what about when it is split too " + r"soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split." + ), + xxx, + yyy, + zzz, +) + +raw_string = r"This is a long raw string. When re-formatting this string, black needs to make sure it prepends the 'r' onto the new string." + +fmt_string1 = "We also need to be sure to preserve any and all {} which may or may not be attached to the string in question.".format( + "method calls" +) + +fmt_string2 = "But what about when the string is {} but {}".format( + "short", + "the method call is really really really really really really really really long?", +) + +old_fmt_string1 = ( + "While we are on the topic of %s, we should also note that old-style formatting must also be preserved, since some %s still uses it." + % ("formatting", "code") +) + +old_fmt_string2 = "This is a %s %s %s %s" % ( + "really really really really really", + "old", + "way to format strings!", + "Use f-strings instead!", +) + +old_fmt_string3 = ( + "Whereas only the strings after the percent sign were long in the last example, this example uses a long initial string as well. This is another %s %s %s %s" + % ( + "really really really really really", + "old", + "way to format strings!", + "Use f-strings instead!", + ) +) + +fstring = f"f-strings definitely make things more {difficult} than they need to be for {{black}}. But boy they sure are handy. The problem is that some lines will need to have the 'f' whereas others do not. This {line}, for example, needs one." + +fstring_with_no_fexprs = f"Some regular string that needs to get split certainly but is NOT an fstring by any means whatsoever." + +comment_string = "Long lines with inline comments should have their comments appended to the reformatted string's enclosing right parentheses." # This comment gets thrown to the top. + +arg_comment_string = print( + "Long lines with inline comments which are apart of (and not the only member of) an argument list should have their comments appended to the reformatted string's enclosing left parentheses.", # This comment stays on the bottom. + "Arg #2", + "Arg #3", + "Arg #4", + "Arg #5", +) + +pragma_comment_string1 = "Lines which end with an inline pragma comment of the form `# : <...>` should be left alone." # noqa: E501 + +pragma_comment_string2 = "Lines which end with an inline pragma comment of the form `# : <...>` should be left alone." # noqa + +"""This is a really really really long triple quote string and it should not be touched.""" + +triple_quote_string = """This is a really really really long triple quote string assignment and it should not be touched.""" + +assert ( + some_type_of_boolean_expression +), "Followed by a really really really long string that is used to provide context to the AssertionError exception." + +assert ( + some_type_of_boolean_expression +), "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic string {}.".format( + "formatting" +) + +assert some_type_of_boolean_expression, ( + "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic string %s." + % "formatting" +) + +assert some_type_of_boolean_expression, ( + "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic %s %s." + % ("string", "formatting") +) + +some_function_call( + "With a reallly generic name and with a really really long string that is, at some point down the line, " + + added + + " to a variable and then added to another string." +) + +some_function_call( + "With a reallly generic name and with a really really long string that is, at some point down the line, " + + added + + " to a variable and then added to another string. But then what happens when the final string is also supppppperrrrr long?! Well then that second (realllllllly long) string should be split too.", + "and a second argument", + and_a_third, +) + +return "A really really really really really really really really really really really really really long {} {}".format( + "return", "value" +) + +func_with_bad_comma( + "This is a really long string argument to a function that has a trailing comma which should NOT be there.", +) + +func_with_bad_comma( + "This is a really long string argument to a function that has a trailing comma which should NOT be there.", # comment after comma +) + +func_with_bad_comma( + ( + "This is a really long string argument to a function that has a trailing comma" + " which should NOT be there." + ), +) + +func_with_bad_comma( + ( + "This is a really long string argument to a function that has a trailing comma" + " which should NOT be there." + ), # comment after comma +) + +func_with_bad_parens_that_wont_fit_in_one_line( + ("short string that should have parens stripped"), x, y, z +) + +func_with_bad_parens_that_wont_fit_in_one_line( + x, y, ("short string that should have parens stripped"), z +) + +func_with_bad_parens( + ("short string that should have parens stripped"), + x, + y, + z, +) + +func_with_bad_parens( + x, + y, + ("short string that should have parens stripped"), + z, +) + +annotated_variable: Final = ( + "This is a large " + + STRING + + " that has been " + + CONCATENATED + + "using the '+' operator." +) +annotated_variable: Final = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." +annotated_variable: Literal[ + "fakse_literal" +] = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." + +backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\" +backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\\\" +backslashes = "This is a really 'long' string with \"embedded double quotes\" and 'single' quotes that also handles checking for an odd number of backslashes \\\", like this...\\\\\\" + +short_string = "Hi" " there." + +func_call(short_string=("Hi" " there.")) + +raw_strings = r"Don't" " get" r" merged" " unless they are all raw." + + +def foo(): + yield "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." + + +x = f"This is a {{really}} long string that needs to be split without a doubt (i.e. most definitely). In short, this {string} that can't possibly be {{expected}} to fit all together on one line. In {fact} it may even take up three or more lines... like four or five... but probably just four." + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # type: ignore + " of it." +) + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # noqa + " of it." +) + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # pylint: disable=some-pylint-check + " of it." +) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/missing_final_newline.py b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/missing_final_newline.py new file mode 100644 index 0000000000..763909fe59 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/missing_final_newline.py @@ -0,0 +1,3 @@ +# A comment-only file, with no final EOL character +# This triggers https://bugs.python.org/issue2142 +# This is the line without the EOL character diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/missing_final_newline.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/missing_final_newline.py.expect new file mode 100644 index 0000000000..763909fe59 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/missing_final_newline.py.expect @@ -0,0 +1,3 @@ +# A comment-only file, with no final EOL character +# This triggers https://bugs.python.org/issue2142 +# This is the line without the EOL character diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/power_op_newline.py b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/power_op_newline.py new file mode 100644 index 0000000000..930e29ab56 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/power_op_newline.py @@ -0,0 +1 @@ +importA;()<<0**0# diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/power_op_newline.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/power_op_newline.py.expect new file mode 100644 index 0000000000..32e89db2df --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/power_op_newline.py.expect @@ -0,0 +1,6 @@ +importA +( + () + << 0 + ** 0 +) # diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/string_quotes.py b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/string_quotes.py new file mode 100644 index 0000000000..86c68e531a --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/string_quotes.py @@ -0,0 +1,57 @@ +'''''' +'\'' +'"' +"'" +"\"" +"Hello" +"Don't do that" +'Here is a "' +'What\'s the deal here?' +"What's the deal \"here\"?" +"And \"here\"?" +"""Strings with "" in them""" +'''Strings with "" in them''' +'''Here's a "''' +'''Here's a " ''' +'''Just a normal triple +quote''' +f"just a normal {f} string" +f'''This is a triple-quoted {f}-string''' +f'MOAR {" ".join([])}' +f"MOAR {' '.join([])}" +r"raw string ftw" +r'Date d\'expiration:(.*)' +r'Tricky "quote' +r'Not-so-tricky \"quote' +rf'{yay}' +'\n\ +The \"quick\"\n\ +brown fox\n\ +jumps over\n\ +the \'lazy\' dog.\n\ +' +re.compile(r'[\\"]') +"x = ''; y = \"\"" +"x = '''; y = \"\"" +"x = ''''; y = \"\"" +"x = '' ''; y = \"\"" +"x = ''; y = \"\"\"" +"x = '''; y = \"\"\"\"" +"x = ''''; y = \"\"\"\"\"" +"x = '' ''; y = \"\"\"\"\"" +'unnecessary \"\"escaping' +"unnecessary \'\'escaping" +'\\""' +"\\''" +'Lots of \\\\\\\\\'quotes\'' +f'{y * " "} \'{z}\'' +f'{{y * " "}} \'{z}\'' +f'\'{z}\' {y * " "}' +f'{y * x} \'{z}\'' +'\'{z}\' {y * " "}' +'{y * x} \'{z}\'' + +# We must bail out if changing the quotes would introduce backslashes in f-string +# expressions. xref: https://github.com/psf/black/issues/2348 +f"\"{b}\"{' ' * (long-len(b)+1)}: \"{sts}\",\n" +f"\"{a}\"{'hello' * b}\"{c}\"" diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/string_quotes.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/string_quotes.py.expect new file mode 100644 index 0000000000..dce6105acf --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/string_quotes.py.expect @@ -0,0 +1,52 @@ +"""""" +"'" +'"' +"'" +'"' +"Hello" +"Don't do that" +'Here is a "' +"What's the deal here?" +'What\'s the deal "here"?' +'And "here"?' +"""Strings with "" in them""" +"""Strings with "" in them""" +'''Here's a "''' +"""Here's a " """ +"""Just a normal triple +quote""" +f"just a normal {f} string" +f"""This is a triple-quoted {f}-string""" +f'MOAR {" ".join([])}' +f"MOAR {' '.join([])}" +r"raw string ftw" +r"Date d\'expiration:(.*)" +r'Tricky "quote' +r"Not-so-tricky \"quote" +rf"{yay}" +"\nThe \"quick\"\nbrown fox\njumps over\nthe 'lazy' dog.\n" +re.compile(r'[\\"]') +"x = ''; y = \"\"" +"x = '''; y = \"\"" +"x = ''''; y = \"\"" +"x = '' ''; y = \"\"" +'x = \'\'; y = """' +'x = \'\'\'; y = """"' +'x = \'\'\'\'; y = """""' +'x = \'\' \'\'; y = """""' +'unnecessary ""escaping' +"unnecessary ''escaping" +'\\""' +"\\''" +"Lots of \\\\\\\\'quotes'" +f'{y * " "} \'{z}\'' +f"{{y * \" \"}} '{z}'" +f'\'{z}\' {y * " "}' +f"{y * x} '{z}'" +"'{z}' {y * \" \"}" +"{y * x} '{z}'" + +# We must bail out if changing the quotes would introduce backslashes in f-string +# expressions. xref: https://github.com/psf/black/issues/2348 +f"\"{b}\"{' ' * (long-len(b)+1)}: \"{sts}\",\n" +f"\"{a}\"{'hello' * b}\"{c}\"" diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/parenthesized_context_managers.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/parenthesized_context_managers.py new file mode 100644 index 0000000000..ccf1f94883 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/parenthesized_context_managers.py @@ -0,0 +1,21 @@ +with (CtxManager() as example): + ... + +with (CtxManager1(), CtxManager2()): + ... + +with (CtxManager1() as example, CtxManager2()): + ... + +with (CtxManager1(), CtxManager2() as example): + ... + +with (CtxManager1() as example1, CtxManager2() as example2): + ... + +with ( + CtxManager1() as example1, + CtxManager2() as example2, + CtxManager3() as example3, +): + ... diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/parenthesized_context_managers.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/parenthesized_context_managers.py.expect new file mode 100644 index 0000000000..dfae92c596 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/parenthesized_context_managers.py.expect @@ -0,0 +1,21 @@ +with CtxManager() as example: + ... + +with CtxManager1(), CtxManager2(): + ... + +with CtxManager1() as example, CtxManager2(): + ... + +with CtxManager1(), CtxManager2() as example: + ... + +with CtxManager1() as example1, CtxManager2() as example2: + ... + +with ( + CtxManager1() as example1, + CtxManager2() as example2, + CtxManager3() as example3, +): + ... diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_complex.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_complex.py new file mode 100644 index 0000000000..97ee194fd3 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_complex.py @@ -0,0 +1,144 @@ +# Cases sampled from Lib/test/test_patma.py + +# case black_test_patma_098 +match x: + case -0j: + y = 0 +# case black_test_patma_142 +match x: + case bytes(z): + y = 0 +# case black_test_patma_073 +match x: + case 0 if 0: + y = 0 + case 0 if 1: + y = 1 +# case black_test_patma_006 +match 3: + case 0 | 1 | 2 | 3: + x = True +# case black_test_patma_049 +match x: + case [0, 1] | [1, 0]: + y = 0 +# case black_check_sequence_then_mapping +match x: + case [*_]: + return "seq" + case {}: + return "map" +# case black_test_patma_035 +match x: + case {0: [1, 2, {}]}: + y = 0 + case {0: [1, 2, {}] | True} | {1: [[]]} | {0: [1, 2, {}]} | [] | "X" | {}: + y = 1 + case []: + y = 2 +# case black_test_patma_107 +match x: + case 0.25 + 1.75j: + y = 0 +# case black_test_patma_097 +match x: + case -0j: + y = 0 +# case black_test_patma_007 +match 4: + case 0 | 1 | 2 | 3: + x = True +# case black_test_patma_154 +match x: + case 0 if x: + y = 0 +# case black_test_patma_134 +match x: + case {1: 0}: + y = 0 + case {0: 0}: + y = 1 + case {**z}: + y = 2 +# case black_test_patma_185 +match Seq(): + case [*_]: + y = 0 +# case black_test_patma_063 +match x: + case 1: + y = 0 + case 1: + y = 1 +# case black_test_patma_248 +match x: + case {"foo": bar}: + y = bar +# case black_test_patma_019 +match (0, 1, 2): + case [0, 1, *x, 2]: + y = 0 +# case black_test_patma_052 +match x: + case [0]: + y = 0 + case [1, 0] if (x := x[:0]): + y = 1 + case [1, 0]: + y = 2 +# case black_test_patma_191 +match w: + case [x, y, *_]: + z = 0 +# case black_test_patma_110 +match x: + case -0.25 - 1.75j: + y = 0 +# case black_test_patma_151 +match (x,): + case [y]: + z = 0 +# case black_test_patma_114 +match x: + case A.B.C.D: + y = 0 +# case black_test_patma_232 +match x: + case None: + y = 0 +# case black_test_patma_058 +match x: + case 0: + y = 0 +# case black_test_patma_233 +match x: + case False: + y = 0 +# case black_test_patma_078 +match x: + case []: + y = 0 + case [""]: + y = 1 + case "": + y = 2 +# case black_test_patma_156 +match x: + case z: + y = 0 +# case black_test_patma_189 +match w: + case [x, y, *rest]: + z = 0 +# case black_test_patma_042 +match x: + case (0 as z) | (1 as z) | (2 as z) if z == x % 2: + y = 0 +# case black_test_patma_034 +match x: + case {0: [1, 2, {}]}: + y = 0 + case {0: [1, 2, {}] | False} | {1: [[]]} | {0: [1, 2, {}]} | [] | "X" | {}: + y = 1 + case []: + y = 2 diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_complex.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_complex.py.expect new file mode 100644 index 0000000000..97ee194fd3 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_complex.py.expect @@ -0,0 +1,144 @@ +# Cases sampled from Lib/test/test_patma.py + +# case black_test_patma_098 +match x: + case -0j: + y = 0 +# case black_test_patma_142 +match x: + case bytes(z): + y = 0 +# case black_test_patma_073 +match x: + case 0 if 0: + y = 0 + case 0 if 1: + y = 1 +# case black_test_patma_006 +match 3: + case 0 | 1 | 2 | 3: + x = True +# case black_test_patma_049 +match x: + case [0, 1] | [1, 0]: + y = 0 +# case black_check_sequence_then_mapping +match x: + case [*_]: + return "seq" + case {}: + return "map" +# case black_test_patma_035 +match x: + case {0: [1, 2, {}]}: + y = 0 + case {0: [1, 2, {}] | True} | {1: [[]]} | {0: [1, 2, {}]} | [] | "X" | {}: + y = 1 + case []: + y = 2 +# case black_test_patma_107 +match x: + case 0.25 + 1.75j: + y = 0 +# case black_test_patma_097 +match x: + case -0j: + y = 0 +# case black_test_patma_007 +match 4: + case 0 | 1 | 2 | 3: + x = True +# case black_test_patma_154 +match x: + case 0 if x: + y = 0 +# case black_test_patma_134 +match x: + case {1: 0}: + y = 0 + case {0: 0}: + y = 1 + case {**z}: + y = 2 +# case black_test_patma_185 +match Seq(): + case [*_]: + y = 0 +# case black_test_patma_063 +match x: + case 1: + y = 0 + case 1: + y = 1 +# case black_test_patma_248 +match x: + case {"foo": bar}: + y = bar +# case black_test_patma_019 +match (0, 1, 2): + case [0, 1, *x, 2]: + y = 0 +# case black_test_patma_052 +match x: + case [0]: + y = 0 + case [1, 0] if (x := x[:0]): + y = 1 + case [1, 0]: + y = 2 +# case black_test_patma_191 +match w: + case [x, y, *_]: + z = 0 +# case black_test_patma_110 +match x: + case -0.25 - 1.75j: + y = 0 +# case black_test_patma_151 +match (x,): + case [y]: + z = 0 +# case black_test_patma_114 +match x: + case A.B.C.D: + y = 0 +# case black_test_patma_232 +match x: + case None: + y = 0 +# case black_test_patma_058 +match x: + case 0: + y = 0 +# case black_test_patma_233 +match x: + case False: + y = 0 +# case black_test_patma_078 +match x: + case []: + y = 0 + case [""]: + y = 1 + case "": + y = 2 +# case black_test_patma_156 +match x: + case z: + y = 0 +# case black_test_patma_189 +match w: + case [x, y, *rest]: + z = 0 +# case black_test_patma_042 +match x: + case (0 as z) | (1 as z) | (2 as z) if z == x % 2: + y = 0 +# case black_test_patma_034 +match x: + case {0: [1, 2, {}]}: + y = 0 + case {0: [1, 2, {}] | False} | {1: [[]]} | {0: [1, 2, {}]} | [] | "X" | {}: + y = 1 + case []: + y = 2 diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_extras.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_extras.py new file mode 100644 index 0000000000..0242d264e5 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_extras.py @@ -0,0 +1,119 @@ +import match + +match something: + case [a as b]: + print(b) + case [a as b, c, d, e as f]: + print(f) + case Point(a as b): + print(b) + case Point(int() as x, int() as y): + print(x, y) + + +match = 1 +case: int = re.match(something) + +match re.match(case): + case type("match", match): + pass + case match: + pass + + +def func(match: case, case: match) -> case: + match Something(): + case func(match, case): + ... + case another: + ... + + +match maybe, multiple: + case perhaps, 5: + pass + case perhaps, 6,: + pass + + +match more := (than, one), indeed,: + case _, (5, 6): + pass + case [[5], (6)], [7],: + pass + case _: + pass + + +match a, *b, c: + case [*_]: + assert "seq" == _ + case {}: + assert "map" == b + + +match match( + case, + match( + match, case, match, looooooooooooooooooooooooooooooooooooong, match, case, match + ), + case, +): + case case( + match=case, + case=re.match( + loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong + ), + ): + pass + + case [a as match]: + pass + + case case: + pass + + +match match: + case case: + pass + + +match a, *b(), c: + case d, *f, g: + pass + + +match something: + case { + "key": key as key_1, + "password": PASS.ONE | PASS.TWO | PASS.THREE as password, + }: + pass + case {"maybe": something(complicated as this) as that}: + pass + + +match something: + case 1 as a: + pass + + case 2 as b, 3 as c: + pass + + case 4 as d, (5 as e), (6 | 7 as g), *h: + pass + + +match bar1: + case Foo(aa=Callable() as aa, bb=int()): + print(bar1.aa, bar1.bb) + case _: + print("no match", "\n") + + +match bar1: + case Foo( + normal=x, perhaps=[list, {"x": d, "y": 1.0}] as y, otherwise=something, q=t as u + ): + pass diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_extras.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_extras.py.expect new file mode 100644 index 0000000000..0242d264e5 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_extras.py.expect @@ -0,0 +1,119 @@ +import match + +match something: + case [a as b]: + print(b) + case [a as b, c, d, e as f]: + print(f) + case Point(a as b): + print(b) + case Point(int() as x, int() as y): + print(x, y) + + +match = 1 +case: int = re.match(something) + +match re.match(case): + case type("match", match): + pass + case match: + pass + + +def func(match: case, case: match) -> case: + match Something(): + case func(match, case): + ... + case another: + ... + + +match maybe, multiple: + case perhaps, 5: + pass + case perhaps, 6,: + pass + + +match more := (than, one), indeed,: + case _, (5, 6): + pass + case [[5], (6)], [7],: + pass + case _: + pass + + +match a, *b, c: + case [*_]: + assert "seq" == _ + case {}: + assert "map" == b + + +match match( + case, + match( + match, case, match, looooooooooooooooooooooooooooooooooooong, match, case, match + ), + case, +): + case case( + match=case, + case=re.match( + loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong + ), + ): + pass + + case [a as match]: + pass + + case case: + pass + + +match match: + case case: + pass + + +match a, *b(), c: + case d, *f, g: + pass + + +match something: + case { + "key": key as key_1, + "password": PASS.ONE | PASS.TWO | PASS.THREE as password, + }: + pass + case {"maybe": something(complicated as this) as that}: + pass + + +match something: + case 1 as a: + pass + + case 2 as b, 3 as c: + pass + + case 4 as d, (5 as e), (6 | 7 as g), *h: + pass + + +match bar1: + case Foo(aa=Callable() as aa, bb=int()): + print(bar1.aa, bar1.bb) + case _: + print("no match", "\n") + + +match bar1: + case Foo( + normal=x, perhaps=[list, {"x": d, "y": 1.0}] as y, otherwise=something, q=t as u + ): + pass diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_generic.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_generic.py new file mode 100644 index 0000000000..00a0e4a677 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_generic.py @@ -0,0 +1,107 @@ +re.match() +match = a +with match() as match: + match = f"{match}" + +re.match() +match = a +with match() as match: + match = f"{match}" + + +def get_grammars(target_versions: Set[TargetVersion]) -> List[Grammar]: + if not target_versions: + # No target_version specified, so try all grammars. + return [ + # Python 3.7+ + pygram.python_grammar_no_print_statement_no_exec_statement_async_keywords, + # Python 3.0-3.6 + pygram.python_grammar_no_print_statement_no_exec_statement, + # Python 2.7 with future print_function import + pygram.python_grammar_no_print_statement, + # Python 2.7 + pygram.python_grammar, + ] + + match match: + case case: + match match: + case case: + pass + + if all(version.is_python2() for version in target_versions): + # Python 2-only code, so try Python 2 grammars. + return [ + # Python 2.7 with future print_function import + pygram.python_grammar_no_print_statement, + # Python 2.7 + pygram.python_grammar, + ] + + re.match() + match = a + with match() as match: + match = f"{match}" + + def test_patma_139(self): + x = False + match x: + case bool(z): + y = 0 + self.assertIs(x, False) + self.assertEqual(y, 0) + self.assertIs(z, x) + + # Python 3-compatible code, so only try Python 3 grammar. + grammars = [] + if supports_feature(target_versions, Feature.PATTERN_MATCHING): + # Python 3.10+ + grammars.append(pygram.python_grammar_soft_keywords) + # If we have to parse both, try to parse async as a keyword first + if not supports_feature( + target_versions, Feature.ASYNC_IDENTIFIERS + ) and not supports_feature(target_versions, Feature.PATTERN_MATCHING): + # Python 3.7-3.9 + grammars.append( + pygram.python_grammar_no_print_statement_no_exec_statement_async_keywords + ) + if not supports_feature(target_versions, Feature.ASYNC_KEYWORDS): + # Python 3.0-3.6 + grammars.append(pygram.python_grammar_no_print_statement_no_exec_statement) + + def test_patma_155(self): + x = 0 + y = None + match x: + case 1e1000: + y = 0 + self.assertEqual(x, 0) + self.assertIs(y, None) + + x = range(3) + match x: + case [y, case as x, z]: + w = 0 + + # At least one of the above branches must have been taken, because every Python + # version has exactly one of the two 'ASYNC_*' flags + return grammars + + +def lib2to3_parse(src_txt: str, target_versions: Iterable[TargetVersion] = ()) -> Node: + """Given a string with source, return the lib2to3 Node.""" + if not src_txt.endswith("\n"): + src_txt += "\n" + + grammars = get_grammars(set(target_versions)) + + +re.match() +match = a +with match() as match: + match = f"{match}" + +re.match() +match = a +with match() as match: + match = f"{match}" diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_generic.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_generic.py.expect new file mode 100644 index 0000000000..00a0e4a677 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_generic.py.expect @@ -0,0 +1,107 @@ +re.match() +match = a +with match() as match: + match = f"{match}" + +re.match() +match = a +with match() as match: + match = f"{match}" + + +def get_grammars(target_versions: Set[TargetVersion]) -> List[Grammar]: + if not target_versions: + # No target_version specified, so try all grammars. + return [ + # Python 3.7+ + pygram.python_grammar_no_print_statement_no_exec_statement_async_keywords, + # Python 3.0-3.6 + pygram.python_grammar_no_print_statement_no_exec_statement, + # Python 2.7 with future print_function import + pygram.python_grammar_no_print_statement, + # Python 2.7 + pygram.python_grammar, + ] + + match match: + case case: + match match: + case case: + pass + + if all(version.is_python2() for version in target_versions): + # Python 2-only code, so try Python 2 grammars. + return [ + # Python 2.7 with future print_function import + pygram.python_grammar_no_print_statement, + # Python 2.7 + pygram.python_grammar, + ] + + re.match() + match = a + with match() as match: + match = f"{match}" + + def test_patma_139(self): + x = False + match x: + case bool(z): + y = 0 + self.assertIs(x, False) + self.assertEqual(y, 0) + self.assertIs(z, x) + + # Python 3-compatible code, so only try Python 3 grammar. + grammars = [] + if supports_feature(target_versions, Feature.PATTERN_MATCHING): + # Python 3.10+ + grammars.append(pygram.python_grammar_soft_keywords) + # If we have to parse both, try to parse async as a keyword first + if not supports_feature( + target_versions, Feature.ASYNC_IDENTIFIERS + ) and not supports_feature(target_versions, Feature.PATTERN_MATCHING): + # Python 3.7-3.9 + grammars.append( + pygram.python_grammar_no_print_statement_no_exec_statement_async_keywords + ) + if not supports_feature(target_versions, Feature.ASYNC_KEYWORDS): + # Python 3.0-3.6 + grammars.append(pygram.python_grammar_no_print_statement_no_exec_statement) + + def test_patma_155(self): + x = 0 + y = None + match x: + case 1e1000: + y = 0 + self.assertEqual(x, 0) + self.assertIs(y, None) + + x = range(3) + match x: + case [y, case as x, z]: + w = 0 + + # At least one of the above branches must have been taken, because every Python + # version has exactly one of the two 'ASYNC_*' flags + return grammars + + +def lib2to3_parse(src_txt: str, target_versions: Iterable[TargetVersion] = ()) -> Node: + """Given a string with source, return the lib2to3 Node.""" + if not src_txt.endswith("\n"): + src_txt += "\n" + + grammars = get_grammars(set(target_versions)) + + +re.match() +match = a +with match() as match: + match = f"{match}" + +re.match() +match = a +with match() as match: + match = f"{match}" diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_simple.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_simple.py new file mode 100644 index 0000000000..5ed62415a4 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_simple.py @@ -0,0 +1,92 @@ +# Cases sampled from PEP 636 examples + +match command.split(): + case [action, obj]: + ... # interpret action, obj + +match command.split(): + case [action]: + ... # interpret single-verb action + case [action, obj]: + ... # interpret action, obj + +match command.split(): + case ["quit"]: + print("Goodbye!") + quit_game() + case ["look"]: + current_room.describe() + case ["get", obj]: + character.get(obj, current_room) + case ["go", direction]: + current_room = current_room.neighbor(direction) + # The rest of your commands go here + +match command.split(): + case ["drop", *objects]: + for obj in objects: + character.drop(obj, current_room) + # The rest of your commands go here + +match command.split(): + case ["quit"]: + pass + case ["go", direction]: + print("Going:", direction) + case ["drop", *objects]: + print("Dropping: ", *objects) + case _: + print(f"Sorry, I couldn't understand {command!r}") + +match command.split(): + case ["north"] | ["go", "north"]: + current_room = current_room.neighbor("north") + case ["get", obj] | ["pick", "up", obj] | ["pick", obj, "up"]: + ... # Code for picking up the given object + +match command.split(): + case ["go", ("north" | "south" | "east" | "west")]: + current_room = current_room.neighbor(...) + # how do I know which direction to go? + +match command.split(): + case ["go", ("north" | "south" | "east" | "west") as direction]: + current_room = current_room.neighbor(direction) + +match command.split(): + case ["go", direction] if direction in current_room.exits: + current_room = current_room.neighbor(direction) + case ["go", _]: + print("Sorry, you can't go that way") + +match event.get(): + case Click(position=(x, y)): + handle_click_at(x, y) + case KeyPress(key_name="Q") | Quit(): + game.quit() + case KeyPress(key_name="up arrow"): + game.go_north() + case KeyPress(): + pass # Ignore other keystrokes + case other_event: + raise ValueError(f"Unrecognized event: {other_event}") + +match event.get(): + case Click((x, y), button=Button.LEFT): # This is a left click + handle_click_at(x, y) + case Click(): + pass # ignore other clicks + + +def where_is(point): + match point: + case Point(x=0, y=0): + print("Origin") + case Point(x=0, y=y): + print(f"Y={y}") + case Point(x=x, y=0): + print(f"X={x}") + case Point(): + print("Somewhere else") + case _: + print("Not a point") diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_simple.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_simple.py.expect new file mode 100644 index 0000000000..5ed62415a4 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_simple.py.expect @@ -0,0 +1,92 @@ +# Cases sampled from PEP 636 examples + +match command.split(): + case [action, obj]: + ... # interpret action, obj + +match command.split(): + case [action]: + ... # interpret single-verb action + case [action, obj]: + ... # interpret action, obj + +match command.split(): + case ["quit"]: + print("Goodbye!") + quit_game() + case ["look"]: + current_room.describe() + case ["get", obj]: + character.get(obj, current_room) + case ["go", direction]: + current_room = current_room.neighbor(direction) + # The rest of your commands go here + +match command.split(): + case ["drop", *objects]: + for obj in objects: + character.drop(obj, current_room) + # The rest of your commands go here + +match command.split(): + case ["quit"]: + pass + case ["go", direction]: + print("Going:", direction) + case ["drop", *objects]: + print("Dropping: ", *objects) + case _: + print(f"Sorry, I couldn't understand {command!r}") + +match command.split(): + case ["north"] | ["go", "north"]: + current_room = current_room.neighbor("north") + case ["get", obj] | ["pick", "up", obj] | ["pick", obj, "up"]: + ... # Code for picking up the given object + +match command.split(): + case ["go", ("north" | "south" | "east" | "west")]: + current_room = current_room.neighbor(...) + # how do I know which direction to go? + +match command.split(): + case ["go", ("north" | "south" | "east" | "west") as direction]: + current_room = current_room.neighbor(direction) + +match command.split(): + case ["go", direction] if direction in current_room.exits: + current_room = current_room.neighbor(direction) + case ["go", _]: + print("Sorry, you can't go that way") + +match event.get(): + case Click(position=(x, y)): + handle_click_at(x, y) + case KeyPress(key_name="Q") | Quit(): + game.quit() + case KeyPress(key_name="up arrow"): + game.go_north() + case KeyPress(): + pass # Ignore other keystrokes + case other_event: + raise ValueError(f"Unrecognized event: {other_event}") + +match event.get(): + case Click((x, y), button=Button.LEFT): # This is a left click + handle_click_at(x, y) + case Click(): + pass # ignore other clicks + + +def where_is(point): + match point: + case Point(x=0, y=0): + print("Origin") + case Point(x=0, y=y): + print(f"Y={y}") + case Point(x=x, y=0): + print(f"X={x}") + case Point(): + print("Somewhere else") + case _: + print("Not a point") diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_style.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_style.py new file mode 100644 index 0000000000..e17f1cdbf6 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_style.py @@ -0,0 +1,53 @@ +match something: + case b(): print(1+1) + case c( + very_complex=True, + perhaps_even_loooooooooooooooooooooooooooooooooooooong=- 1 + ): print(1) + case c( + very_complex=True, + perhaps_even_loooooooooooooooooooooooooooooooooooooong=-1, + ): print(2) + case a: pass + +match( + arg # comment +) + +match( +) + +match( + + +) + +case( + arg # comment +) + +case( +) + +case( + + +) + + +re.match( + something # fast +) +re.match( + + + +) +match match( + + +): + case case( + arg, # comment + ): + pass diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_style.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_style.py.expect new file mode 100644 index 0000000000..d81fa59e33 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_style.py.expect @@ -0,0 +1,35 @@ +match something: + case b(): + print(1 + 1) + case c( + very_complex=True, perhaps_even_loooooooooooooooooooooooooooooooooooooong=-1 + ): + print(1) + case c( + very_complex=True, + perhaps_even_loooooooooooooooooooooooooooooooooooooong=-1, + ): + print(2) + case a: + pass + +match(arg) # comment + +match() + +match() + +case(arg) # comment + +case() + +case() + + +re.match(something) # fast +re.match() +match match(): + case case( + arg, # comment + ): + pass diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pep_572_py310.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pep_572_py310.py new file mode 100644 index 0000000000..cb82b2d23f --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pep_572_py310.py @@ -0,0 +1,15 @@ +# Unparenthesized walruses are now allowed in indices since Python 3.10. +x[a:=0] +x[a:=0, b:=1] +x[5, b:=0] + +# Walruses are allowed inside generator expressions on function calls since 3.10. +if any(match := pattern_error.match(s) for s in buffer): + if match.group(2) == data_not_available: + # Error OK to ignore. + pass + +f(a := b + c for c in range(10)) +f((a := b + c for c in range(10)), x) +f(y=(a := b + c for c in range(10))) +f(x, (a := b + c for c in range(10)), y=z, **q) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pep_572_py310.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pep_572_py310.py.expect new file mode 100644 index 0000000000..cb82b2d23f --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pep_572_py310.py.expect @@ -0,0 +1,15 @@ +# Unparenthesized walruses are now allowed in indices since Python 3.10. +x[a:=0] +x[a:=0, b:=1] +x[5, b:=0] + +# Walruses are allowed inside generator expressions on function calls since 3.10. +if any(match := pattern_error.match(s) for s in buffer): + if match.group(2) == data_not_available: + # Error OK to ignore. + pass + +f(a := b + c for c in range(10)) +f((a := b + c for c in range(10)), x) +f(y=(a := b + c for c in range(10))) +f(x, (a := b + c for c in range(10)), y=z, **q) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/remove_newline_after_match.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/remove_newline_after_match.py new file mode 100644 index 0000000000..629b645fce --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/remove_newline_after_match.py @@ -0,0 +1,19 @@ +def http_status(status): + + match status: + + case 400: + + return "Bad request" + + case 401: + + return "Unauthorized" + + case 403: + + return "Forbidden" + + case 404: + + return "Not found" diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/remove_newline_after_match.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/remove_newline_after_match.py.expect new file mode 100644 index 0000000000..735169ef52 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/remove_newline_after_match.py.expect @@ -0,0 +1,13 @@ +def http_status(status): + match status: + case 400: + return "Bad request" + + case 401: + return "Unauthorized" + + case 403: + return "Forbidden" + + case 404: + return "Not found" diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/starred_for_target.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/starred_for_target.py new file mode 100644 index 0000000000..8fc8e059ed --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/starred_for_target.py @@ -0,0 +1,27 @@ +for x in *a, *b: + print(x) + +for x in a, b, *c: + print(x) + +for x in *a, b, c: + print(x) + +for x in *a, b, *c: + print(x) + +async for x in *a, *b: + print(x) + +async for x in *a, b, *c: + print(x) + +async for x in a, b, *c: + print(x) + +async for x in ( + *loooooooooooooooooooooong, + very, + *loooooooooooooooooooooooooooooooooooooooooooooooong, +): + print(x) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/starred_for_target.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/starred_for_target.py.expect new file mode 100644 index 0000000000..8fc8e059ed --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_310/starred_for_target.py.expect @@ -0,0 +1,27 @@ +for x in *a, *b: + print(x) + +for x in a, b, *c: + print(x) + +for x in *a, b, c: + print(x) + +for x in *a, b, *c: + print(x) + +async for x in *a, *b: + print(x) + +async for x in *a, b, *c: + print(x) + +async for x in a, b, *c: + print(x) + +async for x in ( + *loooooooooooooooooooooong, + very, + *loooooooooooooooooooooooooooooooooooooooooooooooong, +): + print(x) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_311/pep_654.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_311/pep_654.py new file mode 100644 index 0000000000..387c0816f4 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_311/pep_654.py @@ -0,0 +1,53 @@ +try: + raise OSError("blah") +except* ExceptionGroup as e: + pass + + +try: + async with trio.open_nursery() as nursery: + # Make two concurrent calls to child() + nursery.start_soon(child) + nursery.start_soon(child) +except* ValueError: + pass + +try: + try: + raise ValueError(42) + except: + try: + raise TypeError(int) + except* Exception: + pass + 1 / 0 +except Exception as e: + exc = e + +try: + try: + raise FalsyEG("eg", [TypeError(1), ValueError(2)]) + except* TypeError as e: + tes = e + raise + except* ValueError as e: + ves = e + pass +except Exception as e: + exc = e + +try: + try: + raise orig + except* (TypeError, ValueError) as e: + raise SyntaxError(3) from e +except BaseException as e: + exc = e + +try: + try: + raise orig + except* OSError as e: + raise TypeError(3) from e +except ExceptionGroup as e: + exc = e diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_311/pep_654.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_311/pep_654.py.expect new file mode 100644 index 0000000000..387c0816f4 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_311/pep_654.py.expect @@ -0,0 +1,53 @@ +try: + raise OSError("blah") +except* ExceptionGroup as e: + pass + + +try: + async with trio.open_nursery() as nursery: + # Make two concurrent calls to child() + nursery.start_soon(child) + nursery.start_soon(child) +except* ValueError: + pass + +try: + try: + raise ValueError(42) + except: + try: + raise TypeError(int) + except* Exception: + pass + 1 / 0 +except Exception as e: + exc = e + +try: + try: + raise FalsyEG("eg", [TypeError(1), ValueError(2)]) + except* TypeError as e: + tes = e + raise + except* ValueError as e: + ves = e + pass +except Exception as e: + exc = e + +try: + try: + raise orig + except* (TypeError, ValueError) as e: + raise SyntaxError(3) from e +except BaseException as e: + exc = e + +try: + try: + raise orig + except* OSError as e: + raise TypeError(3) from e +except ExceptionGroup as e: + exc = e diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_311/pep_654_style.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_311/pep_654_style.py new file mode 100644 index 0000000000..1c5918d17d --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_311/pep_654_style.py @@ -0,0 +1,55 @@ +try: + raise OSError("blah") +except * ExceptionGroup as e: + pass + + +try: + async with trio.open_nursery() as nursery: + # Make two concurrent calls to child() + nursery.start_soon(child) + nursery.start_soon(child) +except *ValueError: + pass + +try: + try: + raise ValueError(42) + except: + try: + raise TypeError(int) + except *(Exception): + pass + 1 / 0 +except Exception as e: + exc = e + +try: + try: + raise FalsyEG("eg", [TypeError(1), ValueError(2)]) + except \ + *TypeError as e: + tes = e + raise + except * ValueError as e: + ves = e + pass +except Exception as e: + exc = e + +try: + try: + raise orig + except *(TypeError, ValueError, *OTHER_EXCEPTIONS) as e: + raise SyntaxError(3) from e +except BaseException as e: + exc = e + +try: + try: + raise orig + except\ + * OSError as e: + raise TypeError(3) from e +except ExceptionGroup as e: + exc = e diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_311/pep_654_style.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_311/pep_654_style.py.expect new file mode 100644 index 0000000000..c0d06dbfe1 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_311/pep_654_style.py.expect @@ -0,0 +1,53 @@ +try: + raise OSError("blah") +except* ExceptionGroup as e: + pass + + +try: + async with trio.open_nursery() as nursery: + # Make two concurrent calls to child() + nursery.start_soon(child) + nursery.start_soon(child) +except* ValueError: + pass + +try: + try: + raise ValueError(42) + except: + try: + raise TypeError(int) + except* Exception: + pass + 1 / 0 +except Exception as e: + exc = e + +try: + try: + raise FalsyEG("eg", [TypeError(1), ValueError(2)]) + except* TypeError as e: + tes = e + raise + except* ValueError as e: + ves = e + pass +except Exception as e: + exc = e + +try: + try: + raise orig + except* (TypeError, ValueError, *OTHER_EXCEPTIONS) as e: + raise SyntaxError(3) from e +except BaseException as e: + exc = e + +try: + try: + raise orig + except* OSError as e: + raise TypeError(3) from e +except ExceptionGroup as e: + exc = e diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_36/numeric_literals.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_36/numeric_literals.py new file mode 100644 index 0000000000..6da4ba68d6 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_36/numeric_literals.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3.6 + +x = 123456789 +x = 123456 +x = .1 +x = 1. +x = 1E+1 +x = 1E-1 +x = 1.000_000_01 +x = 123456789.123456789 +x = 123456789.123456789E123456789 +x = 123456789E123456789 +x = 123456789J +x = 123456789.123456789J +x = 0XB1ACC +x = 0B1011 +x = 0O777 +x = 0.000000006 +x = 10000 +x = 133333 diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_36/numeric_literals.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_36/numeric_literals.py.expect new file mode 100644 index 0000000000..e263924b4e --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_36/numeric_literals.py.expect @@ -0,0 +1,20 @@ +#!/usr/bin/env python3.6 + +x = 123456789 +x = 123456 +x = 0.1 +x = 1.0 +x = 1e1 +x = 1e-1 +x = 1.000_000_01 +x = 123456789.123456789 +x = 123456789.123456789e123456789 +x = 123456789e123456789 +x = 123456789j +x = 123456789.123456789j +x = 0xB1ACC +x = 0b1011 +x = 0o777 +x = 0.000000006 +x = 10000 +x = 133333 diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_36/numeric_literals_skip_underscores.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_36/numeric_literals_skip_underscores.py new file mode 100644 index 0000000000..d77116a832 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_36/numeric_literals_skip_underscores.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python3.6 + +x = 123456789 +x = 1_2_3_4_5_6_7 +x = 1E+1 +x = 0xb1acc +x = 0.00_00_006 +x = 12_34_567J +x = .1_2 +x = 1_2. diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_36/numeric_literals_skip_underscores.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_36/numeric_literals_skip_underscores.py.expect new file mode 100644 index 0000000000..a81ada11e5 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_36/numeric_literals_skip_underscores.py.expect @@ -0,0 +1,10 @@ +#!/usr/bin/env python3.6 + +x = 123456789 +x = 1_2_3_4_5_6_7 +x = 1e1 +x = 0xB1ACC +x = 0.00_00_006 +x = 12_34_567j +x = 0.1_2 +x = 1_2.0 diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_37/python37.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_37/python37.py new file mode 100644 index 0000000000..01fd7eede3 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_37/python37.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3.7 + + +def f(): + return (i * 2 async for i in arange(42)) + + +def g(): + return ( + something_long * something_long + async for something_long in async_generator(with_an_argument) + ) + + +async def func(): + if test: + out_batched = [ + i + async for i in aitertools._async_map( + self.async_inc, arange(8), batch_size=3 + ) + ] + + +def awaited_generator_value(n): + return (await awaitable for awaitable in awaitable_list) + + +def make_arange(n): + return (i * 2 for i in range(n) if await wrap(i)) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_37/python37.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_37/python37.py.expect new file mode 100644 index 0000000000..01fd7eede3 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_37/python37.py.expect @@ -0,0 +1,30 @@ +#!/usr/bin/env python3.7 + + +def f(): + return (i * 2 async for i in arange(42)) + + +def g(): + return ( + something_long * something_long + async for something_long in async_generator(with_an_argument) + ) + + +async def func(): + if test: + out_batched = [ + i + async for i in aitertools._async_map( + self.async_inc, arange(8), batch_size=3 + ) + ] + + +def awaited_generator_value(n): + return (await awaitable for awaitable in awaitable_list) + + +def make_arange(n): + return (i * 2 for i in range(n) if await wrap(i)) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/pep_570.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/pep_570.py new file mode 100644 index 0000000000..ca8f7ab1d9 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/pep_570.py @@ -0,0 +1,44 @@ +def positional_only_arg(a, /): + pass + + +def all_markers(a, b, /, c, d, *, e, f): + pass + + +def all_markers_with_args_and_kwargs( + a_long_one, + b_long_one, + /, + c_long_one, + d_long_one, + *args, + e_long_one, + f_long_one, + **kwargs, +): + pass + + +def all_markers_with_defaults(a, b=1, /, c=2, d=3, *, e=4, f=5): + pass + + +def long_one_with_long_parameter_names( + but_all_of_them, + are_positional_only, + arguments_mmmmkay, + so_this_is_only_valid_after, + three_point_eight, + /, +): + pass + + +lambda a, /: a + +lambda a, b, /, c, d, *, e, f: a + +lambda a, b, /, c, d, *args, e, f, **kwargs: args + +lambda a, b=1, /, c=2, d=3, *, e=4, f=5: 1 diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/pep_570.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/pep_570.py.expect new file mode 100644 index 0000000000..ca8f7ab1d9 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/pep_570.py.expect @@ -0,0 +1,44 @@ +def positional_only_arg(a, /): + pass + + +def all_markers(a, b, /, c, d, *, e, f): + pass + + +def all_markers_with_args_and_kwargs( + a_long_one, + b_long_one, + /, + c_long_one, + d_long_one, + *args, + e_long_one, + f_long_one, + **kwargs, +): + pass + + +def all_markers_with_defaults(a, b=1, /, c=2, d=3, *, e=4, f=5): + pass + + +def long_one_with_long_parameter_names( + but_all_of_them, + are_positional_only, + arguments_mmmmkay, + so_this_is_only_valid_after, + three_point_eight, + /, +): + pass + + +lambda a, /: a + +lambda a, b, /, c, d, *, e, f: a + +lambda a, b, /, c, d, *args, e, f, **kwargs: args + +lambda a, b=1, /, c=2, d=3, *, e=4, f=5: 1 diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/pep_572.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/pep_572.py new file mode 100644 index 0000000000..d41805f1cb --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/pep_572.py @@ -0,0 +1,47 @@ +(a := 1) +(a := a) +if (match := pattern.search(data)) is None: + pass +if match := pattern.search(data): + pass +[y := f(x), y**2, y**3] +filtered_data = [y for x in data if (y := f(x)) is None] +(y := f(x)) +y0 = (y1 := f(x)) +foo(x=(y := f(x))) + + +def foo(answer=(p := 42)): + pass + + +def foo(answer: (p := 42) = 5): + pass + + +lambda: (x := 1) +(x := lambda: 1) +(x := lambda: (y := 1)) +lambda line: (m := re.match(pattern, line)) and m.group(1) +x = (y := 0) +(z := (y := (x := 0))) +(info := (name, phone, *rest)) +(x := 1, 2) +(total := total + tax) +len(lines := f.readlines()) +foo(x := 3, cat="vector") +foo(cat=(category := "vector")) +if any(len(longline := l) >= 100 for l in lines): + print(longline) +if env_base := os.environ.get("PYTHONUSERBASE", None): + return env_base +if self._is_special and (ans := self._check_nans(context=context)): + return ans +foo(b := 2, a=1) +foo((b := 2), a=1) +foo(c=(b := 2), a=1) + +while x := f(x): + pass +while x := f(x): + pass diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/pep_572.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/pep_572.py.expect new file mode 100644 index 0000000000..d41805f1cb --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/pep_572.py.expect @@ -0,0 +1,47 @@ +(a := 1) +(a := a) +if (match := pattern.search(data)) is None: + pass +if match := pattern.search(data): + pass +[y := f(x), y**2, y**3] +filtered_data = [y for x in data if (y := f(x)) is None] +(y := f(x)) +y0 = (y1 := f(x)) +foo(x=(y := f(x))) + + +def foo(answer=(p := 42)): + pass + + +def foo(answer: (p := 42) = 5): + pass + + +lambda: (x := 1) +(x := lambda: 1) +(x := lambda: (y := 1)) +lambda line: (m := re.match(pattern, line)) and m.group(1) +x = (y := 0) +(z := (y := (x := 0))) +(info := (name, phone, *rest)) +(x := 1, 2) +(total := total + tax) +len(lines := f.readlines()) +foo(x := 3, cat="vector") +foo(cat=(category := "vector")) +if any(len(longline := l) >= 100 for l in lines): + print(longline) +if env_base := os.environ.get("PYTHONUSERBASE", None): + return env_base +if self._is_special and (ans := self._check_nans(context=context)): + return ans +foo(b := 2, a=1) +foo((b := 2), a=1) +foo(c=(b := 2), a=1) + +while x := f(x): + pass +while x := f(x): + pass diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/python38.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/python38.py new file mode 100644 index 0000000000..391b52f8ce --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/python38.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3.8 + + +def starred_return(): + my_list = ["value2", "value3"] + return "value1", *my_list + + +def starred_yield(): + my_list = ["value2", "value3"] + yield "value1", *my_list + + +# all right hand side expressions allowed in regular assignments are now also allowed in +# annotated assignments +a : Tuple[ str, int] = "1", 2 +a: Tuple[int , ... ] = b, *c, d +def t(): + a : str = yield "a" diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/python38.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/python38.py.expect new file mode 100644 index 0000000000..5df012410a --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_38/python38.py.expect @@ -0,0 +1,21 @@ +#!/usr/bin/env python3.8 + + +def starred_return(): + my_list = ["value2", "value3"] + return "value1", *my_list + + +def starred_yield(): + my_list = ["value2", "value3"] + yield "value1", *my_list + + +# all right hand side expressions allowed in regular assignments are now also allowed in +# annotated assignments +a: Tuple[str, int] = "1", 2 +a: Tuple[int, ...] = b, *c, d + + +def t(): + a: str = yield "a" diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/pep_572_py39.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/pep_572_py39.py new file mode 100644 index 0000000000..b8b081b8c4 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/pep_572_py39.py @@ -0,0 +1,7 @@ +# Unparenthesized walruses are now allowed in set literals & set comprehensions +# since Python 3.9 +{x := 1, 2, 3} +{x4 := x**5 for x in range(7)} +# We better not remove the parentheses here (since it's a 3.10 feature) +x[(a := 1)] +x[(a := 1), (b := 3)] diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/pep_572_py39.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/pep_572_py39.py.expect new file mode 100644 index 0000000000..b8b081b8c4 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/pep_572_py39.py.expect @@ -0,0 +1,7 @@ +# Unparenthesized walruses are now allowed in set literals & set comprehensions +# since Python 3.9 +{x := 1, 2, 3} +{x4 := x**5 for x in range(7)} +# We better not remove the parentheses here (since it's a 3.10 feature) +x[(a := 1)] +x[(a := 1), (b := 3)] diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/python39.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/python39.py new file mode 100644 index 0000000000..227faca09a --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/python39.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3.9 + +@relaxed_decorator[0] +def f(): + ... + +@relaxed_decorator[extremely_long_name_that_definitely_will_not_fit_on_one_line_of_standard_length] +def f(): + ... + +@extremely_long_variable_name_that_doesnt_fit := complex.expression(with_long="arguments_value_that_wont_fit_at_the_end_of_the_line") +def f(): + ... diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/python39.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/python39.py.expect new file mode 100644 index 0000000000..4af4beebb2 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/python39.py.expect @@ -0,0 +1,20 @@ +#!/usr/bin/env python3.9 + + +@relaxed_decorator[0] +def f(): + ... + + +@relaxed_decorator[ + extremely_long_name_that_definitely_will_not_fit_on_one_line_of_standard_length +] +def f(): + ... + + +@extremely_long_variable_name_that_doesnt_fit := complex.expression( + with_long="arguments_value_that_wont_fit_at_the_end_of_the_line" +) +def f(): + ... diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/remove_with_brackets.py b/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/remove_with_brackets.py new file mode 100644 index 0000000000..9634bab444 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/remove_with_brackets.py @@ -0,0 +1,54 @@ +with (open("bla.txt")): + pass + +with (open("bla.txt")), (open("bla.txt")): + pass + +with (open("bla.txt") as f): + pass + +# Remove brackets within alias expression +with (open("bla.txt")) as f: + pass + +# Remove brackets around one-line context managers +with (open("bla.txt") as f, (open("x"))): + pass + +with ((open("bla.txt")) as f, open("x")): + pass + +with (CtxManager1() as example1, CtxManager2() as example2): + ... + +# Brackets remain when using magic comma +with (CtxManager1() as example1, CtxManager2() as example2,): + ... + +# Brackets remain for multi-line context managers +with (CtxManager1() as example1, CtxManager2() as example2, CtxManager2() as example2, CtxManager2() as example2, CtxManager2() as example2): + ... + +# Don't touch assignment expressions +with (y := open("./test.py")) as f: + pass + +# Deeply nested examples +# N.B. Multiple brackets are only possible +# around the context manager itself. +# Only one brackets is allowed around the +# alias expression or comma-delimited context managers. +with (((open("bla.txt")))): + pass + +with (((open("bla.txt")))), (((open("bla.txt")))): + pass + +with (((open("bla.txt")))) as f: + pass + +with ((((open("bla.txt")))) as f): + pass + +with ((((CtxManager1()))) as example1, (((CtxManager2()))) as example2): + ... diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/remove_with_brackets.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/remove_with_brackets.py.expect new file mode 100644 index 0000000000..e70d01b18d --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/py_39/remove_with_brackets.py.expect @@ -0,0 +1,63 @@ +with open("bla.txt"): + pass + +with open("bla.txt"), open("bla.txt"): + pass + +with open("bla.txt") as f: + pass + +# Remove brackets within alias expression +with open("bla.txt") as f: + pass + +# Remove brackets around one-line context managers +with open("bla.txt") as f, open("x"): + pass + +with open("bla.txt") as f, open("x"): + pass + +with CtxManager1() as example1, CtxManager2() as example2: + ... + +# Brackets remain when using magic comma +with ( + CtxManager1() as example1, + CtxManager2() as example2, +): + ... + +# Brackets remain for multi-line context managers +with ( + CtxManager1() as example1, + CtxManager2() as example2, + CtxManager2() as example2, + CtxManager2() as example2, + CtxManager2() as example2, +): + ... + +# Don't touch assignment expressions +with (y := open("./test.py")) as f: + pass + +# Deeply nested examples +# N.B. Multiple brackets are only possible +# around the context manager itself. +# Only one brackets is allowed around the +# alias expression or comma-delimited context managers. +with open("bla.txt"): + pass + +with open("bla.txt"), open("bla.txt"): + pass + +with open("bla.txt") as f: + pass + +with open("bla.txt") as f: + pass + +with CtxManager1() as example1, CtxManager2() as example2: + ... diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments.py.expect index 4a94e7ad93..c34daaf6f0 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments.py.expect +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments.py.expect @@ -93,4 +93,4 @@ async def wat(): # Some closing comments. # Maybe Vim or Emacs directives for formatting. -# Who knows. \ No newline at end of file +# Who knows. diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/expression.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/expression.py.expect index 08be5ea501..16160f98f3 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/expression.py.expect +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/expression.py.expect @@ -1,3 +1,4 @@ +... "some_string" b"\\xa3" Name @@ -114,7 +115,7 @@ call( arg, another, kwarg="hey", - **kwargs, + **kwargs ) # note: no trailing comma pre-3.6 call(*gidgets[:2]) call(a, *gidgets[:2]) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtonoff.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtonoff.py.expect index 8631f8eaaa..2534e5cc1d 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtonoff.py.expect +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtonoff.py.expect @@ -5,7 +5,6 @@ import sys from third_party import X, Y, Z from library import some_connection, some_decorator - # fmt: off from third_party import (X, Y, Z) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtpass_imports.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtpass_imports.py new file mode 100644 index 0000000000..8b3c0bc662 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtpass_imports.py @@ -0,0 +1,19 @@ +# Regression test for https://github.com/psf/black/issues/3438 + +import ast +import collections # fmt: skip +import dataclasses +# fmt: off +import os +# fmt: on +import pathlib + +import re # fmt: skip +import secrets + +# fmt: off +import sys +# fmt: on + +import tempfile +import zoneinfo diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtpass_imports.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtpass_imports.py.expect new file mode 100644 index 0000000000..8b3c0bc662 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtpass_imports.py.expect @@ -0,0 +1,19 @@ +# Regression test for https://github.com/psf/black/issues/3438 + +import ast +import collections # fmt: skip +import dataclasses +# fmt: off +import os +# fmt: on +import pathlib + +import re # fmt: skip +import secrets + +# fmt: off +import sys +# fmt: on + +import tempfile +import zoneinfo diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fstring.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fstring.py index b778ec2879..190cd6294a 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fstring.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fstring.py @@ -7,3 +7,5 @@ f"{f'''{'nested'} inner'''} outer" f"\"{f'{nested} inner'}\" outer" f"space between opening braces: { {a for a in (1, 2, 3)}}" f'Hello \'{tricky + "example"}\'' +f"Tried directories {str(rootdirs)} \ +but none started with prefix {parentdir_prefix}" diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fstring.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fstring.py.expect index 0c6538c5bf..488d786e39 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fstring.py.expect +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fstring.py.expect @@ -7,3 +7,5 @@ f"{f'''{'nested'} inner'''} outer" f"\"{f'{nested} inner'}\" outer" f"space between opening braces: { {a for a in (1, 2, 3)}}" f'Hello \'{tricky + "example"}\'' +f"Tried directories {str(rootdirs)} \ +but none started with prefix {parentdir_prefix}" diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/function.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/function.py index bc41e08a16..e94c5c5ace 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/function.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/function.py @@ -34,7 +34,7 @@ def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r'' def spaces_types(a: int = 1, b: tuple = (), c: list = [], d: dict = {}, e: bool = True, f: int = -1, g: int = 1 if False else 2, h: str = "", i: str = r''): ... def spaces2(result= _core.Value(None)): assert fut is self._read_fut, (fut, self._read_fut) - + # EMPTY LINE WITH WHITESPACE (this comment will be removed) def example(session): result = session.query(models.Customer.id).filter( models.Customer.account_id == account_id, diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/pep_604.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/pep_604.py new file mode 100644 index 0000000000..2ff5ca4829 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/pep_604.py @@ -0,0 +1,6 @@ +def some_very_long_name_function() -> my_module.Asdf | my_module.AnotherType | my_module.YetAnotherType | None: + pass + + +def some_very_long_name_function() -> my_module.Asdf | my_module.AnotherType | my_module.YetAnotherType | my_module.EvenMoreType | None: + pass diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/pep_604.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/pep_604.py.expect new file mode 100644 index 0000000000..1629cc693b --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/pep_604.py.expect @@ -0,0 +1,14 @@ +def some_very_long_name_function() -> ( + my_module.Asdf | my_module.AnotherType | my_module.YetAnotherType | None +): + pass + + +def some_very_long_name_function() -> ( + my_module.Asdf + | my_module.AnotherType + | my_module.YetAnotherType + | my_module.EvenMoreType + | None +): + pass diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/skip_magic_trailing_comma.options.json b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/skip_magic_trailing_comma.options.json new file mode 100644 index 0000000000..e01e786cb6 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/skip_magic_trailing_comma.options.json @@ -0,0 +1,3 @@ +{ + "magic_trailing_comma": "ignore" +} \ No newline at end of file diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/slices.py b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/slices.py index 63885bb872..165117cdcb 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/slices.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/slices.py @@ -29,35 +29,3 @@ ham[lower:upper], ham[lower:upper:], ham[lower::step] # ham[lower+offset : upper+offset] ham[: upper_fn(x) : step_fn(x)], ham[:: step_fn(x)] ham[lower + offset : upper + offset] - -slice[::, ::] -slice[ - # A - : - # B - : - # C -] -slice[ - # A - 1: - # B - 2: - # C - 3 -] - -slice[ - # A - 1 - + 2 : - # B - 3 : - # C - 4 -] -x[ - 1: # A - 2: # B - 3 # C -] diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/slices.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/slices.py.expect index 61218829e4..165117cdcb 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/slices.py.expect +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/slices.py.expect @@ -29,31 +29,3 @@ ham[lower:upper], ham[lower:upper:], ham[lower::step] # ham[lower+offset : upper+offset] ham[: upper_fn(x) : step_fn(x)], ham[:: step_fn(x)] ham[lower + offset : upper + offset] - -slice[::, ::] -slice[ - # A - : - # B - : - # C -] -slice[ - # A - 1: - # B - 2: - # C - 3 -] - -slice[ - # A - 1 - + 2 : - # B - 3 : - # C - 4 -] -x[1:2:3] # A # B # C diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/whitespace.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/whitespace.py.expect index e69de29bb2..8b13789179 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/whitespace.py.expect +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/whitespace.py.expect @@ -0,0 +1 @@ + diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/type_comments/type_comment_syntax_error.py b/crates/ruff_python_formatter/resources/test/fixtures/black/type_comments/type_comment_syntax_error.py new file mode 100644 index 0000000000..70e2fbe328 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/type_comments/type_comment_syntax_error.py @@ -0,0 +1,3 @@ +def foo( + # type: Foo + x): pass diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/type_comments/type_comment_syntax_error.py.expect b/crates/ruff_python_formatter/resources/test/fixtures/black/type_comments/type_comment_syntax_error.py.expect new file mode 100644 index 0000000000..764f7cedd8 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/type_comments/type_comment_syntax_error.py.expect @@ -0,0 +1,5 @@ +def foo( + # type: Foo + x, +): + pass diff --git a/crates/ruff_python_formatter/resources/test/fixtures/import_black_tests.py b/crates/ruff_python_formatter/resources/test/fixtures/import_black_tests.py new file mode 100755 index 0000000000..6cad807da4 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/import_black_tests.py @@ -0,0 +1,113 @@ +#!/usr/bin/python3 + +from __future__ import annotations + +import argparse +from pathlib import Path + + +def import_fixture(fixture: Path, fixture_set: str): + """ + Imports a single fixture by writing the input and expected output to the black directory. + """ + + output_directory = Path(__file__).parent.joinpath("black").joinpath(fixture_set) + output_directory.mkdir(parents=True, exist_ok=True) + + fixture_path = output_directory.joinpath(fixture.name) + expect_path = fixture_path.with_suffix(".py.expect") + + with ( + fixture.open("r") as black_file, + fixture_path.open("w") as fixture_file, + expect_path.open("w") as expect_file + ): + lines = iter(black_file) + expected = [] + input = [] + + for line in lines: + if line.rstrip() == "# output": + expected = list(lines) + break + else: + input.append(line) + + if not expected: + # If there's no output marker, tread the whole file as already pre-formatted + expected = input + + fixture_file.write("".join(input).strip() + "\n") + expect_file.write("".join(expected).strip() + "\n") + + +# The name of the folders in the `data` for which the tests should be imported +FIXTURE_SETS = [ + "py_36", + "py_37", + "py_38", + "py_39", + "py_310", + "py_311", + "simple_cases", + "miscellaneous", + ".", + "type_comments" +] + +# Tests that ruff doesn't fully support yet and, therefore, should not be imported +IGNORE_LIST = [ + "pep_572_remove_parens.py", # Reformatting bugs + "pep_646.py", # Rust Python parser bug + + # Contain syntax errors + "async_as_identifier.py", + "invalid_header.py", + "pattern_matching_invalid.py", + + # Python 2 + "python2_detection.py" +] + + +def import_fixtures(black_dir: str): + """Imports all the black fixtures""" + + test_directory = Path(black_dir, "tests/data") + + if not test_directory.exists(): + print( + "Black directory does not contain a 'tests/data' directory. Does the directory point to a full black " + "checkout (git clone https://github.com/psf/black.git)?") + return + + for fixture_set in FIXTURE_SETS: + fixture_directory = test_directory.joinpath(fixture_set) + fixtures = fixture_directory.glob("*.py") + + if not fixtures: + print(f"Fixture set '{fixture_set}' contains no python files") + return + + for fixture in fixtures: + if fixture.name in IGNORE_LIST: + print(f"Ignoring fixture '{fixture}") + continue + + print(f"Importing fixture '{fixture}") + import_fixture(fixture, fixture_set) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description="Imports the test suite from black.", + epilog="import_black_tests.py " + ) + + parser.add_argument("black_dir", type=Path) + + args = parser.parse_args() + + black_dir = args.black_dir + + import_fixtures(black_dir) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/carriage_return/.editorconfig b/crates/ruff_python_formatter/resources/test/fixtures/ruff/carriage_return/.editorconfig new file mode 100644 index 0000000000..cafa748cf3 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/carriage_return/.editorconfig @@ -0,0 +1,2 @@ +[*.py] +end_of_line = crlf diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/carriage_return/.gitattributes b/crates/ruff_python_formatter/resources/test/fixtures/ruff/carriage_return/.gitattributes new file mode 100644 index 0000000000..0c42f3cc29 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/carriage_return/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=crlf diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/carriage_return/string.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/carriage_return/string.py new file mode 100644 index 0000000000..45f9dacc38 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/carriage_return/string.py @@ -0,0 +1,6 @@ +'This string will not include \ +backslashes or newline characters.' + +"""Multiline +String \" +""" diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/annotated_assign.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/annotated_assign.py new file mode 100644 index 0000000000..0809374ffc --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/annotated_assign.py @@ -0,0 +1,13 @@ +a: string + +b: string = "test" + +b: list[ + string, + int +] = [1, 2] + +b: list[ + string, + int, +] = [1, 2] diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/attribute.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/attribute.py new file mode 100644 index 0000000000..24bea5ca42 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/attribute.py @@ -0,0 +1,103 @@ +from argparse import Namespace + +a = Namespace() + +( + a + # comment + .b # trailing comment +) + +( + a + # comment + .b # trailing dot comment # trailing identifier comment +) + +( + a + # comment + .b # trailing identifier comment +) + + +( + a + # comment + . # trailing dot comment + # in between + b # trailing identifier comment +) + + +a.aaaaaaaaaaaaaaaaaaaaa.lllllllllllllllllllllllllllloooooooooong.chaaaaaaaaaaaaaaaaaaaaaaiiiiiiiiiiiiiiiiiiiiiiinnnnnnnn.ooooooooooooooooooooooooofffffffff.aaaaaaaaaattr + + +# Test that we add parentheses around the outermost attribute access in an attribute +# chain if and only if we need them, that is if there are own line comments inside +# the chain. +x1 = ( + a + .b + # comment 1 + . # comment 2 + # comment 3 + c + .d +) + +x20 = ( + a + .b +) +x21 = ( + # leading name own line + a # trailing name end-of-line + .b +) +x22 = ( + a + # outermost leading own line + .b # outermost trailing end-of-line +) + +x31 = ( + a + # own line between nodes 1 + .b +) +x321 = ( + a + . # end-of-line dot comment + b +) +x322 = ( + a + . # end-of-line dot comment 2 + b + .c +) +x331 = ( + a. + # own line between nodes 3 + b +) +x332 = ( + "" + # own line between nodes + .find +) + +x8 = ( + (a + a) + .b +) + +x51 = ( + a.b.c +) +x52 = a.askjdfahdlskjflsajfadhsaf.akjdsf.aksjdlfadhaljsashdfljaf.askjdflhasfdlashdlfaskjfd.asdkjfksahdfkjafs +x53 = ( + a.askjdfahdlskjflsajfadhsaf.akjdsf.aksjdlfadhaljsashdfljaf.askjdflhasfdlashdlfaskjfd.asdkjfksahdfkjafs +) + diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary.py new file mode 100644 index 0000000000..30cf4c4465 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary.py @@ -0,0 +1,213 @@ +(aaaaaaaa + + # trailing operator comment + b # trailing right comment +) + + +(aaaaaaaa # trailing left comment + + # trailing operator comment + # leading right comment + b +) + +( + # leading left most comment + aaaaaaaa + + # trailing operator comment + # leading b comment + b # trailing b comment + # trailing b ownline comment + + # trailing second operator comment + # leading c comment + c # trailing c comment + # trailing own line comment + ) + + +# Black breaks the right side first for the following expressions: +aaaaaaaaaaaaaa + caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaal(argument1, argument2, argument3) +aaaaaaaaaaaaaa + [bbbbbbbbbbbbbbbbbbbbbb, ccccccccccccccccccccc, dddddddddddddddd, eeeeeee] +aaaaaaaaaaaaaa + (bbbbbbbbbbbbbbbbbbbbbb, ccccccccccccccccccccc, dddddddddddddddd, eeeeeee) +aaaaaaaaaaaaaa + { key1:bbbbbbbbbbbbbbbbbbbbbb, key2: ccccccccccccccccccccc, key3: dddddddddddddddd, key4: eeeeeee } +aaaaaaaaaaaaaa + { bbbbbbbbbbbbbbbbbbbbbb, ccccccccccccccccccccc, dddddddddddddddd, eeeeeee } +aaaaaaaaaaaaaa + [a for x in bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb ] +aaaaaaaaaaaaaa + (a for x in bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb ) +aaaaaaaaaaaaaa + {a for x in bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb} + +# Wraps it in parentheses if it needs to break both left and right +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + [ + bbbbbbbbbbbbbbbbbbbbbb, + ccccccccccccccccccccc, + dddddddddddddddd, + eee +] # comment + + + +# But only for expressions that have a statement parent. +not (aaaaaaaaaaaaaa + {a for x in bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb}) +[a + [bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb] in c ] + + +# leading comment +( + # comment + content + b +) + + +if ( + aaaaaaaaaaaaaaaaaa + + # has the child process finished? + bbbbbbbbbbbbbbb + + # the child process has finished, but the + # transport hasn't been notified yet? + ccccccccccc +): + pass + + +# Left only breaks +if [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +] & aaaaaaaaaaaaaaaaaaaaaaaaaa: + ... + +if [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +] & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: + ... + +# Right only can break +if aaaaaaaaaaaaaaaaaaaaaaaaaa & [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +]: + ... + +if aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa & [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +]: + ... + + +# Left or right can break +if [2222, 333] & [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +]: + ... + +if [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +] & [2222, 333]: + ... + +if [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +] & [fffffffffffffffff, gggggggggggggggggggg, hhhhhhhhhhhhhhhhhhhhh, iiiiiiiiiiiiiiii, jjjjjjjjjjjjj]: + ... + +if ( + # comment + [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, + ] +) & [ + fffffffffffffffff, + gggggggggggggggggggg, + hhhhhhhhhhhhhhhhhhhhh, + iiiiiiiiiiiiiiii, + jjjjjjjjjjjjj, +]: + pass + + ... + +# Nesting +if (aaaa + b) & [ + fffffffffffffffff, + gggggggggggggggggggg, + hhhhhhhhhhhhhhhhhhhhh, + iiiiiiiiiiiiiiii, + jjjjjjjjjjjjj, +]: + ... + +if [ + fffffffffffffffff, + gggggggggggggggggggg, + hhhhhhhhhhhhhhhhhhhhh, + iiiiiiiiiiiiiiii, + jjjjjjjjjjjjj, +] & (a + b): + ... + + +if [ + fffffffffffffffff, + gggggggggggggggggggg, + hhhhhhhhhhhhhhhhhhhhh, + iiiiiiiiiiiiiiii, + jjjjjjjjjjjjj, +] & ( + # comment + a + + b +): + ... + +if ( + [ + fffffffffffffffff, + gggggggggggggggggggg, + hhhhhhhhhhhhhhhhhhhhh, + iiiiiiiiiiiiiiii, + jjjjjjjjjjjjj, + ] + & + # comment + a + b +): + ... + + +# Unstable formatting in https://github.com/realtyem/synapse-unraid/blob/unraid_develop/synapse/handlers/presence.py +for user_id in set(target_user_ids) - {u.user_id for u in updates}: + updates.append(UserPresenceState.default(user_id)) + +# Keeps parenthesized left hand sides +( + log(self.price / self.strike) + + (self.risk_free - self.div_cont + 0.5 * (self.sigma**2)) * self.exp_time +) / self.sigmaT diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary_expression.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary_expression.py deleted file mode 100644 index c27141d22b..0000000000 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary_expression.py +++ /dev/null @@ -1,54 +0,0 @@ -(aaaaaaaa - + # trailing operator comment - b # trailing right comment -) - - -(aaaaaaaa # trailing left comment - + # trailing operator comment - # leading right comment - b -) - - -# Black breaks the right side first for the following expressions: -aaaaaaaaaaaaaa + caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaal(argument1, argument2, argument3) -aaaaaaaaaaaaaa + [bbbbbbbbbbbbbbbbbbbbbb, ccccccccccccccccccccc, dddddddddddddddd, eeeeeee] -aaaaaaaaaaaaaa + (bbbbbbbbbbbbbbbbbbbbbb, ccccccccccccccccccccc, dddddddddddddddd, eeeeeee) -aaaaaaaaaaaaaa + { key1:bbbbbbbbbbbbbbbbbbbbbb, key2: ccccccccccccccccccccc, key3: dddddddddddddddd, key4: eeeeeee } -aaaaaaaaaaaaaa + { bbbbbbbbbbbbbbbbbbbbbb, ccccccccccccccccccccc, dddddddddddddddd, eeeeeee } -aaaaaaaaaaaaaa + [a for x in bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb ] -aaaaaaaaaaaaaa + (a for x in bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb ) -aaaaaaaaaaaaaa + {a for x in bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb} - -# Wraps it in parentheses if it needs to break both left and right -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + [ - bbbbbbbbbbbbbbbbbbbbbb, - ccccccccccccccccccccc, - dddddddddddddddd, - eee -] # comment - - - -# But only for expressions that have a statement parent. -not (aaaaaaaaaaaaaa + {a for x in bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb}) -[a + [bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb] in c ] - - -# leading comment -( - # comment - content + b -) - - -if ( - aaaaaaaaaaaaaaaaaa + - # has the child process finished? - bbbbbbbbbbbbbbb + - # the child process has finished, but the - # transport hasn't been notified yet? - ccccccccccc -): - pass diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/boolean_operation.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/boolean_operation.py new file mode 100644 index 0000000000..f976491cd1 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/boolean_operation.py @@ -0,0 +1,64 @@ +if ( + self._proc + # has the child process finished? + and self._returncode + # the child process has finished, but the + # transport hasn't been notified yet? + and self._proc.poll() +): + pass + +if ( + self._proc + and self._returncode + and self._proc.poll() + and self._proc + and self._returncode + and self._proc.poll() +): + ... + +if ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + and aaaaaaaaaaaaaaaaa + and aaaaaaaaaaaaaaaaaaaaaa + and aaaaaaaaaaaaaaaaaaaaaaaa + and aaaaaaaaaaaaaaaaaaaaaaaaaa + and aaaaaaaaaaaaaaaaaaaaaaaaaaaa +): + ... + + +if ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaas + and aaaaaaaaaaaaaaaaa +): + ... + + +if [2222, 333] and [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +]: + ... + +if [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +] and [2222, 333]: + pass + +# Break right only applies for boolean operations with a left and right side +if ( + aaaaaaaaaaaaaaaaaaaaaaaaaa + and bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + and ccccccccccccccccc + and [dddddddddddddd, eeeeeeeeee, fffffffffffffff] +): + pass diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/call.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/call.py new file mode 100644 index 0000000000..8c372180ce --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/call.py @@ -0,0 +1,88 @@ +from unittest.mock import MagicMock + + +def f(*args, **kwargs): + pass + +this_is_a_very_long_argument_asökdhflakjslaslhfdlaffahflahsöfdhasägporejfäalkdsjäfalisjäfdlkasjd = 1 +session = MagicMock() +models = MagicMock() + +f() + +f(1) + +f(x=2) + +f(1, x=2) + +f( + this_is_a_very_long_argument_asökdhflakjslaslhfdlaffahflahsöfdhasägporejfäalkdsjäfalisjäfdlkasjd +) +f( + this_is_a_very_long_keyword_argument_asökdhflakjslaslhfdlaffahflahsöfdhasägporejfäalkdsjäfalisjäfdlkasjd=1 +) + +f( + 1, + mixed_very_long_arguments=1, +) + +f( + this_is_a_very_long_argument_asökdhflakjslaslhfdlaffahflahsöfdhasägporejfäalkdsjäfalisjäfdlkasjd, + these_arguments_have_values_that_need_to_break_because_they_are_too_long1=(100000 - 100000000000), + these_arguments_have_values_that_need_to_break_because_they_are_too_long2="akshfdlakjsdfad" + "asdfasdfa", + these_arguments_have_values_that_need_to_break_because_they_are_too_long3=session, +) + +f( + # dangling comment +) + + +f( + only=1, short=1, arguments=1 +) + +f( + hey_this_is_a_long_call, it_has_funny_attributes_that_breaks_into_three_lines=1 +) + +f( + hey_this_is_a_very_long_call=1, it_has_funny_attributes_asdf_asdf=1, too_long_for_the_line=1, really=True +) + +# TODO(konstin): Call chains/fluent interface (https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#call-chains) +result = ( + session.query(models.Customer.id) + .filter( + models.Customer.account_id == 10000, + models.Customer.email == "user@example.org", + ) + .order_by(models.Customer.id.asc()) + .all() +) +# TODO(konstin): Black has this special case for comment placement where everything stays in one line +f( + "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa" +) + +f( + session, + b=1, + ** # oddly placed end-of-line comment + dict() +) +f( + session, + b=1, + ** + # oddly placed own line comment + dict() +) + +# Don't add a magic trailing comma when there is only one entry +# Minimized from https://github.com/django/django/blob/7eeadc82c2f7d7a778e3bb43c34d642e6275dacf/django/contrib/admin/checks.py#L674-L681 +f( + a.very_long_function_function_that_is_so_long_that_it_expands_the_parent_but_its_only_a_single_argument() +) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/compare.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/compare.py new file mode 100644 index 0000000000..906d5710aa --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/compare.py @@ -0,0 +1,61 @@ +a == b +a != b +a < b +a <= b +a > b +a >= b +a is b +a is not b +a in b +a not in b + +(a == + # comment + b +) + +(a == # comment + b + ) + +a < b > c == d + +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa < bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb > ccccccccccccccccccccccccccccc == ddddddddddddddddddddd + +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa < [ + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, + ff, +] < [ccccccccccccccccccccccccccccc, dddd] < ddddddddddddddddddddddddddddddddddddddddddd + +return 1 == 2 and ( + name, + description, + self_default, + self_selected, + self_auto_generated, + self_parameters, + self_meta_data, + self_schedule, +) == ( + name, + description, + othr_default, + othr_selected, + othr_auto_generated, + othr_parameters, + othr_meta_data, + othr_schedule, +) + +(name, description, self_default, self_selected, self_auto_generated, self_parameters, self_meta_data, self_schedule) == (name, description, other_default, othr_selected, othr_auto_generated, othr_parameters, othr_meta_data, othr_schedule) +((name, description, self_default, self_selected, self_auto_generated, self_parameters, self_meta_data, self_schedule) == (name, description, other_default, othr_selected, othr_auto_generated, othr_parameters, othr_meta_data, othr_schedule)) + +[ + ( + a + + [ + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + ] + >= c + ) +] diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/dict.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/dict.py new file mode 100644 index 0000000000..b53ff5c6f3 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/dict.py @@ -0,0 +1,58 @@ +# before +{ # open + key# key + : # colon + value# value +} # close +# after + + +{**d} + +{**a, # leading +** # middle +b # trailing +} + +{ +** # middle with single item +b +} + +{ + # before + ** # between + b, +} + +{ + **a # comment before preceding node's comma + , + # before + ** # between + b, +} + +{} + +{1:2,} + +{1:2, + 3:4,} + +{asdfsadfalsdkjfhalsdkjfhalskdjfhlaksjdfhlaskjdfhlaskjdfhlaksdjfh: 1, adsfadsflasdflasdfasdfasdasdf: 2} + +mapping = { + A: 0.25 * (10.0 / 12), + B: 0.1 * (10.0 / 12), + C: 0.1 * (10.0 / 12), + D: 0.1 * (10.0 / 12), +} + +# Regression test for formatter panic with comment after parenthesized dict value +# Originally found in https://github.com/bolucat/Firefox/blob/636a717ef025c16434997dc89e42351ef740ee6b/testing/marionette/client/marionette_driver/geckoinstance.py#L109 +a = { + 1: (2), + # comment + 3: True, +} diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/if.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/if.py new file mode 100644 index 0000000000..df4c488603 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/if.py @@ -0,0 +1,41 @@ +a1 = 1 if True else 2 + +a2 = "this is a very long text that will make the group break to check that parentheses are added" if True else 2 + +# These comment should be kept in place +b1 = ( + # We return "a" ... + "a" # that's our True value + # ... if this condition matches ... + if True # that's our test + # ... otherwise we return "b" + else "b" # that's our False value +) + +# These only need to be stable, bonus is we also keep the order +c1 = ( + "a" # 1 + if # 2 + True # 3 + else # 4 + "b" # 5 +) +c2 = ( + "a" # 1 + # 2 + if # 3 + # 4 + True # 5 + # 6 + else # 7 + # 8 + "b" # 9 +) + +# regression test: parentheses outside the expression ranges interfering with finding +# the `if` and `else` token finding +d1 = [ + ("a") if # 1 + ("b") else # 2 + ("c") +] diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/list.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/list.py new file mode 100644 index 0000000000..f0fedc6957 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/list.py @@ -0,0 +1,23 @@ +# Dangling comment placement in empty lists +# Regression test for https://github.com/python/cpython/blob/03160630319ca26dcbbad65225da4248e54c45ec/Tools/c-analyzer/c_analyzer/datafiles.py#L14-L16 +a1 = [ # a +] +a2 = [ # a + # b +] +a3 = [ + # b +] + +# Add magic trailing comma only if there is more than one entry, but respect it if it's +# already there +b1 = [ + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa +] +b2 = [ + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, +] +b3 = [ + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa +] diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/list_comp.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/list_comp.py new file mode 100644 index 0000000000..6f8a4dbd31 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/list_comp.py @@ -0,0 +1,45 @@ +[i for i in []] + +[i for i in [1,]] + +[ + a # a + for # for + c # c + in # in + e # e +] + +[ + # above a + a # a + # above for + for # for + # above c + c # c + # above in + in # in + # above e + e # e + # above if + if # if + # above f + f # f + # above if2 + if # if2 + # above g + g # g +] + +[ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + [dddddddddddddddddd, eeeeeeeeeeeeeeeeeee] + for + ccccccccccccccccccccccccccccccccccccccc, + ddddddddddddddddddd, [eeeeeeeeeeeeeeeeeeeeee, fffffffffffffffffffffffff] + in + eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeffffffffffffffffffffffffggggggggggggggggggggghhhhhhhhhhhhhhothermoreeand_even_moreddddddddddddddddddddd + if + fffffffffffffffffffffffffffffffffffffffffff < gggggggggggggggggggggggggggggggggggggggggggggg < hhhhhhhhhhhhhhhhhhhhhhhhhh + if + gggggggggggggggggggggggggggggggggggggggggggg +] diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/named_expr.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/named_expr.py new file mode 100644 index 0000000000..9377e9704e --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/named_expr.py @@ -0,0 +1,13 @@ +y = 1 + +if ( + # 1 + x # 2 + := # 3 + y # 4 +): + pass + +y0 = (y1 := f(x)) + +f(x:=y, z=True) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/slice.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/slice.py new file mode 100644 index 0000000000..7406108226 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/slice.py @@ -0,0 +1,82 @@ +# Handle comments both when lower and upper exist and when they don't +a1 = "a"[ + # a + 1 # b + : # c + 2 # d +] +a2 = "a"[ + # a + # b + : # c + # d +] + +# Check all places where comments can exist +b1 = "b"[ # a + # b + 1 # c + # d + : # e + # f + 2 # g + # h + : # i + # j + 3 # k + # l +] + +# Handle the spacing from the colon correctly with upper leading comments +c1 = "c"[ + 1 + : # e + # f + 2 +] +c2 = "c"[ + 1 + : # e + 2 +] +c3 = "c"[ + 1 + : + # f + 2 +] +c4 = "c"[ + 1 + : # f + 2 +] + +# End of line comments +d1 = "d"[ # comment + : +] +d2 = "d"[ # comment + 1: +] +d3 = "d"[ + 1 # comment + : +] + +# Spacing around the colon(s) +def a(): + ... + +e00 = "e"[:] +e01 = "e"[:1] +e02 = "e"[: a()] +e10 = "e"[1:] +e11 = "e"[1:1] +e12 = "e"[1 : a()] +e20 = "e"[a() :] +e21 = "e"[a() : 1] +e22 = "e"[a() : a()] +e200 = "e"[a() :: ] +e201 = "e"[a() :: 1] +e202 = "e"[a() :: a()] +e210 = "e"[a() : 1 :] diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/starred.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/starred.py new file mode 100644 index 0000000000..76483d7fa1 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/starred.py @@ -0,0 +1,15 @@ +call( + # Leading starred comment + * # Trailing star comment + [ + # Leading value commnt + [What, i, this, s, very, long, aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa] + ] # trailing value comment +) + +call( + # Leading starred comment + * ( # Leading value commnt + [What, i, this, s, very, long, aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa] + ) # trailing value comment +) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/string.options.json b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/string.options.json new file mode 100644 index 0000000000..7d6d0512c2 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/string.options.json @@ -0,0 +1,8 @@ +[ + { + "quote_style": "double" + }, + { + "quote_style": "single" + } +] diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/string.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/string.py new file mode 100644 index 0000000000..6767a2463e --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/string.py @@ -0,0 +1,120 @@ +"' test" +'" test' + +"\" test" +'\' test' + +# Prefer single quotes for string with more double quotes +"' \" \" '' \" \" '" + +# Prefer double quotes for string with more single quotes +'\' " " \'\' " " \'' + +# Prefer double quotes for string with equal amount of single and double quotes +'" \' " " \'\'' +"' \" '' \" \"" + +"\\' \"\"" +'\\\' ""' + + +u"Test" +U"Test" + +r"Test" +R"Test" + +'This string will not include \ +backslashes or newline characters.' + +if True: + 'This string will not include \ + backslashes or newline characters.' + +"""Multiline +String \" +""" + +'''Multiline +String \' +''' + +'''Multiline +String "" +''' + +'''Multiline +String """ +''' + +'''Multiline +String "''' + +"""Multiline +String ''' +""" + +"""Multiline +String '""" + +'''Multiline +String \"\"\" +''' + +# String continuation + +"Let's" "start" "with" "a" "simple" "example" + +"Let's" "start" "with" "a" "simple" "example" "now repeat after me:" "I am confident" "I am confident" "I am confident" "I am confident" "I am confident" + +( + "Let's" "start" "with" "a" "simple" "example" "now repeat after me:" "I am confident" "I am confident" "I am confident" "I am confident" "I am confident" +) + +if ( + a + "Let's" + "start" + "with" + "a" + "simple" + "example" + "now repeat after me:" + "I am confident" + "I am confident" + "I am confident" + "I am confident" + "I am confident" +): + pass + +if "Let's" "start" "with" "a" "simple" "example" "now repeat after me:" "I am confident" "I am confident" "I am confident" "I am confident" "I am confident": + pass + +( + # leading + "a" # trailing part comment + + # leading part comment + + "b" # trailing second part comment + # trailing +) + +test_particular = [ + # squares + '1.00000000100000000025', + '1.0000000000000000000000000100000000000000000000000' #... + '00025', + '1.0000000000000000000000000000000000000000000010000' #... + '0000000000000000000000000000000000000000025', +] + +# Parenthesized string continuation with messed up indentation +{ + "key": ( + [], + 'a' + 'b' + 'c' + ) +} diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tuple.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tuple.py new file mode 100644 index 0000000000..13859fb27d --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tuple.py @@ -0,0 +1,66 @@ +# Non-wrapping parentheses checks +a1 = 1, 2 +a2 = (1, 2) +a3 = (1, 2), 3 +a4 = ((1, 2), 3) + +# Wrapping parentheses checks +b1 = (("Michael", "Ende"), ("Der", "satanarchäolügenialkohöllische", "Wunschpunsch"), ("Beelzebub", "Irrwitzer"), ("Tyrannja", "Vamperl"),) +b2 = ("akjdshflkjahdslkfjlasfdahjlfds", "ljklsadhflakfdajflahfdlajfhafldkjalfj", "ljklsadhflakfdajflahfdlajfhafldkjalf2",) +b3 = ("The", "Night", "of", "Wishes:", "Or", "the", "Satanarchaeolidealcohellish", "Notion", "Potion",) + +# Nested wrapping parentheses check +c1 = (("cicero"), ("Qui", "autem,", "si", "maxime", "hoc", "placeat,", "moderatius", "tamen", "id", "uolunt", "fieri,", "difficilem", "quandam", "temperantiam", "postulant", "in", "eo,", "quod", "semel", "admissum", "coerceri", "reprimique", "non", "potest,", "ut", "propemodum", "iustioribus", "utamur", "illis,", "qui", "omnino", "auocent", "a", "philosophia,", "quam", "his,", "qui", "rebus", "infinitis", "modum", "constituant", "in", "reque", "eo", "meliore,", "quo", "maior", "sit,", "mediocritatem", "desiderent."), ("de", "finibus", "bonorum", "et", "malorum")) + +# Deeply nested parentheses +d1 = ((("3D",),),) +d2 = (((((((((((((((((((((((((((("¯\_(ツ)_/¯",),),),),),),),),),),),),),),),),),),),),),),),),),),),) + +# Join and magic trailing comma +e1 = ( + 1, + 2 +) +e2 = ( + 1, + 2, +) +e3 = ( + 1, +) +e4 = ( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor", + "incididunt" +) + +# Empty tuples and comments +f1 = ( + # empty +) +f2 = () +f3 = ( # end-of-line + # own-line +) # trailing +f4 = ( # end-of-line + # own-line + # own-line 2 +) # trailing + +# Comments in other tuples +g1 = ( # a + # b + 1, # c + # d +) # e +g2 = ( # a + # b + 1, # c + # d + 2, # e + # f +) # g + +# Ensure the correct number of parentheses +h1 = ((((1, 2)))) +h2 = ((((1, "qweiurpoiqwurepqiurpqirpuqoiwrupqoirupqoirupqoiurpqiorupwqiourpqurpqurpqurpqurpqurpqurüqurqpuriq")))) +h3 = 1, "qweiurpoiqwurepqiurpqirpuqoiwrupqoirupqoirupqoiurpqiorupwqiourpqurpqurpqurpqurpqurpqurüqurqpuriq" diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/unary.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/unary.py new file mode 100644 index 0000000000..11106d7d23 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/unary.py @@ -0,0 +1,142 @@ +if not aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb: + pass + +a = True +not a + +b = 10 +-b ++b + +## Leading operand comments + +if not ( + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + + +if ~( + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb): + pass + +if -( + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb): + pass + + +if +( + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb): + pass + +if ( + not + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + + +if ( + ~ + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb): + pass + +if ( + - + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb): + pass + + +if ( + + + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb): + pass + +## Parentheses + +if ( + # unary comment + not + # operand comment + ( + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + ) +): + pass + +if (not ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +) +): + pass + +if aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa & (not ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +) +): + pass + +if ( + not ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + ) + & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +): + pass + + +## Trailing operator comments + +if ( + not # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + + +if ( + ~ # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + +if ( + - # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + + +if ( + + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + + +## Varia + +if not \ + a: + pass + +# Regression: https://github.com/astral-sh/ruff/issues/5338 +if a and not aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: + ... diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/skip_magic_trailing_comma.options.json b/crates/ruff_python_formatter/resources/test/fixtures/ruff/skip_magic_trailing_comma.options.json new file mode 100644 index 0000000000..f842b9e3ff --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/skip_magic_trailing_comma.options.json @@ -0,0 +1,8 @@ +[ + { + "magic_trailing_comma": "respect" + }, + { + "magic_trailing_comma": "ignore" + } +] diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/skip_magic_trailing_comma.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/skip_magic_trailing_comma.py new file mode 100644 index 0000000000..1c138f163d --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/skip_magic_trailing_comma.py @@ -0,0 +1,22 @@ +( + "First entry", + "Second entry", + "last with trailing comma", +) + +( + "First entry", + "Second entry", + "last without trailing comma" +) + +( + "First entry", + "Second entry", + "third entry", + "fourth entry", + "fifth entry", + "sixt entry", + "seventh entry", + "eighth entry", +) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/ann_assign.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/ann_assign.py new file mode 100644 index 0000000000..b965199c98 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/ann_assign.py @@ -0,0 +1,7 @@ +# Regression test: Don't forget the parentheses in the value when breaking +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: int = a + 1 * a + + +# Regression test: Don't forget the parentheses in the annotation when breaking +class DefaultRunner: + task_runner_cls: TaskRunnerProtocol | typing.Callable[[], typing.Any] = DefaultTaskRunner diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/stmt_assign.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/assign.py similarity index 100% rename from crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/stmt_assign.py rename to crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/assign.py diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/aug_assign.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/aug_assign.py new file mode 100644 index 0000000000..6c9d3155f6 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/aug_assign.py @@ -0,0 +1,5 @@ +tree_depth += 1 + +greeting += "This is very long, formal greeting for whomever is name here. Dear %s, it will break the line" % len( + name +) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/break.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/break.py new file mode 100644 index 0000000000..c171873a02 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/break.py @@ -0,0 +1,5 @@ +# leading comment +while True: # block comment + # inside comment + break # break comment + # post comment diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/class_definition.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/class_definition.py new file mode 100644 index 0000000000..08b8e40e25 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/class_definition.py @@ -0,0 +1,39 @@ +class Test( + Aaaaaaaaaaaaaaaaa, + Bbbbbbbbbbbbbbbb, + DDDDDDDDDDDDDDDD, + EEEEEEEEEEEEEE, + metaclass=meta, +): + pass + + +class Test((Aaaaaaaaaaaaaaaaa), Bbbbbbbbbbbbbbbb, metaclass=meta): + pass + +class Test( # trailing class comment + Aaaaaaaaaaaaaaaaa, # trailing comment + + # in between comment + + Bbbbbbbbbbbbbbbb, + # another leading comment + DDDDDDDDDDDDDDDD, + EEEEEEEEEEEEEE, + # meta comment + metaclass=meta, # trailing meta comment +): + pass + +class Test((Aaaa)): + ... + + +class Test(aaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbb + cccccccccccccccccccccccc + dddddddddddddddddddddd + eeeeeeeee, ffffffffffffffffff, gggggggggggggggggg): + pass + +class Test(aaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbb * cccccccccccccccccccccccc + dddddddddddddddddddddd + eeeeeeeee, ffffffffffffffffff, gggggggggggggggggg): + pass + +class Test(Aaaa): # trailing comment + pass diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/delete.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/delete.py new file mode 100644 index 0000000000..fa5efd652a --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/delete.py @@ -0,0 +1,72 @@ +x = 1 +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa = 1 +a, b, c, d = 1, 2, 3, 4 + +del a, b, c, d +del a, b, c, d # Trailing + +del a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a +del a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a # Trailing + +del ( + a, + a +) + +del ( + # Dangling comment +) + +# Delete something +del x # Deleted something +# Done deleting + +# Delete something +del ( + # Deleting something + x # Deleted something + # Finishing deletes +) # Completed +# Done deleting + +# Delete something +del ( + # Deleting something + x # Deleted something + # Finishing deletes +) # Completed +# Done deleting + +# Delete something +del ( + # Deleting something + x, # Deleted something + # Finishing deletes +) # Completed +# Done deleting + +# Delete something +del ( + # Deleting something + x # Deleted something + # Finishing deletes + # Dangling comment +) # Completed +# Done deleting + +# NOTE: This shouldn't format. See https://github.com/astral-sh/ruff/issues/5630. +# Delete something +del x, aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, b, c, d # Delete these +# Ready to delete + +# Delete something +del ( + x, + # Deleting this + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, + b, + c, + d, + # Deleted +) # Completed +# Done diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/for.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/for.py new file mode 100644 index 0000000000..3a96b5390f --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/for.py @@ -0,0 +1,34 @@ +for x in y: # trailing test comment + pass # trailing last statement comment + + # trailing for body comment + +# leading else comment + +else: # trailing else comment + pass + + # trailing else body comment + + +for aVeryLongNameThatSpillsOverToTheNextLineBecauseItIsExtremelyLongAndGoesOnAndOnAndOnAndOnAndOnAndOnAndOnAndOnAndOn in anotherVeryLongNameThatSpillsOverToTheNextLineBecauseItIsExtremelyLongAndGoesOnAndOnAndOnAndOnAndOnAndOnAndOnAndOnAndOn: # trailing comment + pass + +else: + ... + +for ( + x, + y, + ) in z: # comment + ... + + +# remove brackets around x,y but keep them around z,w +for (x, y) in (z, w): + ... + + +# type comment +for x in (): # type: int + ... diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/function.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/function.py index 7e4ddaa48b..2c9450a926 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/function.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/function.py @@ -68,3 +68,166 @@ def test(): ... # Comment def with_leading_comment(): ... + +# Comment that could be mistaken for a trailing comment of the function declaration when +# looking from the position of the if +# Regression test for https://github.com/python/cpython/blob/ad56340b665c5d8ac1f318964f71697bba41acb7/Lib/logging/__init__.py#L253-L260 +if True: + def f1(): + pass # a +else: + pass + +# Here it's actually a trailing comment +if True: + def f2(): + pass + # a +else: + pass + +# Make sure the star is printed +# Regression test for https://github.com/python/cpython/blob/7199584ac8632eab57612f595a7162ab8d2ebbc0/Lib/warnings.py#L513 +def f(arg1=1, *, kwonlyarg1, kwonlyarg2=2): + pass + + +# Regression test for https://github.com/astral-sh/ruff/issues/5176#issuecomment-1598171989 +def foo( + b=3 + 2 # comment +): + ... + + +# Comments on the slash or the star, both of which don't have a node +def f11( + a, + # positional only comment, leading + /, # positional only comment, trailing + b, +): + pass + +def f12( + a=1, + # positional only comment, leading + /, # positional only comment, trailing + b=2, +): + pass + +def f13( + a, + # positional only comment, leading + /, # positional only comment, trailing +): + pass + +def f21( + a=1, + # keyword only comment, leading + *, # keyword only comment, trailing + b=2, +): + pass + +def f22( + a, + # keyword only comment, leading + *, # keyword only comment, trailing + b, +): + pass + +def f23( + a, + # keyword only comment, leading + *args, # keyword only comment, trailing + b, +): + pass + +def f24( + # keyword only comment, leading + *, # keyword only comment, trailing + a +): + pass + + +def f31( + a=1, + # positional only comment, leading + /, # positional only comment, trailing + b=2, + # keyword only comment, leading + *, # keyword only comment, trailing + c=3, +): + pass + +def f32( + a, + # positional only comment, leading + /, # positional only comment, trailing + b, + # keyword only comment, leading + *, # keyword only comment, trailing + c, +): + pass + +def f33( + a, + # positional only comment, leading + /, # positional only comment, trailing + # keyword only comment, leading + *args, # keyword only comment, trailing + c, +): + pass + + +def f34( + a, + # positional only comment, leading + /, # positional only comment, trailing + # keyword only comment, leading + *, # keyword only comment, trailing + c, +): + pass + +def f35( + # keyword only comment, leading + *, # keyword only comment, trailing + c, +): + pass + +# Multiple trailing comments +def f41( + a, + / # 1 + , # 2 + # 3 + * # 4 + , # 5 + c, +): + pass + +# Multiple trailing comments strangely places. The goal here is only stable formatting, +# the comments are placed to strangely to keep their relative position intact +def f42( + a, + / # 1 + # 2 + , # 3 + # 4 + * # 5 + # 6 + , # 7 + c, +): + pass diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/if.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/if.py new file mode 100644 index 0000000000..e737ebbab1 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/if.py @@ -0,0 +1,73 @@ +if x == y: # trailing if condition + pass # trailing `pass` comment + # Root `if` trailing comment + +# Leading elif comment +elif x < y: # trailing elif condition + pass + # `elif` trailing comment + +# Leading else comment +else: # trailing else condition + pass + # `else` trailing comment + + +if x == y: + if y == z: + ... + + if a == b: + ... + else: # trailing comment + ... + + # trailing else comment + +# leading else if comment +elif aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + [ + 11111111111111111111111111, + 2222222222222222222222, + 3333333333 + ]: + ... + + +else: + ... + +# Regression test: Don't drop the trailing comment by associating it with the elif +# instead of the else. +# Originally found in https://github.com/python/cpython/blob/ab3823a97bdeefb0266b3c8d493f7f6223ce3686/Lib/dataclasses.py#L539 + +if "if 1": + pass +elif "elif 1": + pass +# Don't drop this comment 1 +x = 1 + +if "if 2": + pass +elif "elif 2": + pass +else: + pass +# Don't drop this comment 2 +x = 2 + +if "if 3": + pass +else: + pass +# Don't drop this comment 3 +x = 3 + +# Regression test for a following if that could get confused for an elif +# Originally found in https://github.com/gradio-app/gradio/blob/1570b94a02d23d051ae137e0063974fd8a48b34e/gradio/external.py#L478 +if True: + pass +else: # Comment + if False: + pass + pass diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/if_statement.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/if_statement.py deleted file mode 100644 index 65b21e30e9..0000000000 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/if_statement.py +++ /dev/null @@ -1,37 +0,0 @@ -if x == y: # trailing if condition - pass # trailing `pass` comment - # Root `if` trailing comment - -# Leading elif comment -elif x < y: # trailing elif condition - pass - # `elif` trailing comment - -# Leading else comment -else: # trailing else condition - pass - # `else` trailing comment - - -if x == y: - if y == z: - ... - - if a == b: - ... - else: # trailing comment - ... - - # trailing else comment - -# leading else if comment -elif aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + [ - 11111111111111111111111111, - 2222222222222222222222, - 3333333333 - ]: - ... - - -else: - ... diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/import.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/import.py new file mode 100644 index 0000000000..1677400431 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/import.py @@ -0,0 +1,3 @@ +from a import aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa +from a import aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa +from a import aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa as dfgsdfgsd, aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa as sdkjflsdjlahlfd diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/import_from.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/import_from.py new file mode 100644 index 0000000000..335e91036a --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/import_from.py @@ -0,0 +1,16 @@ +from a import aksjdhflsakhdflkjsadlfajkslhf +from a import ( + aksjdhflsakhdflkjsadlfajkslhf, +) +from a import ( + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, +) +from a import ( + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, +) +from a import ( + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa as dfgsdfgsd, + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa as sdkjflsdjlahlfd, +) +from aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa import * diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/raise.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/raise.py new file mode 100644 index 0000000000..db15be22e8 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/raise.py @@ -0,0 +1,85 @@ +raise a from aksjdhflsakhdflkjsadlfajkslhf +raise a from (aksjdhflsakhdflkjsadlfajkslhf,) +raise (aaaaa.aaa(a).a) from (aksjdhflsakhdflkjsadlfajkslhf) + +raise a from (aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa,) +raise a from OsError("aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa") + +# some comment +raise a from aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa # some comment +# some comment + +raise OsError("aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa") from e + + +raise OsError( + # should i stay + "aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa" # mhhh very long + # or should i go +) from e # here is e + +raise OsError("aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa") from OsError("aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa") + +raise OsError( + "aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa" +) from a.aaaaa( + aaa +).a(aaaa) + +raise OsError( + "aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa" +) from a.aaaaa(aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa).a(aaaa) + +raise aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbb + cccccccccccccccccccccc + ddddddddddddddddddddddddd +raise aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbb + (cccccccccccccccccccccc + ddddddddddddddddddddddddd) +raise (aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbb + cccccccccccccccccccccc + ddddddddddddddddddddddddd) + + +raise ( # hey + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + # Holala + + bbbbbbbbbbbbbbbbbbbbbbbbbb # stay + + cccccccccccccccccccccc + ddddddddddddddddddddddddd # where I'm going + # I don't know +) # whaaaaat +# the end + +raise ( # hey 2 + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + # Holala + "bbbbbbbbbbbbbbbbbbbbbbbb" # stay + "ccccccccccccccccccccccc" "dddddddddddddddddddddddd" # where I'm going + # I don't know +) # whaaaaat + +# some comment +raise aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa[aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:bbbbbbbbbbbbbbbbbbbbbbbbbb] + +raise aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa < aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa (content: &T) -> ParenthesizeIfExpands<'_, 'ast> +where + T: Format>, +{ + ParenthesizeIfExpands { + inner: Argument::new(content), + } +} + +pub(crate) struct ParenthesizeIfExpands<'a, 'ast> { + inner: Argument<'a, PyFormatContext<'ast>>, +} + +impl<'ast> Format> for ParenthesizeIfExpands<'_, 'ast> { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + let saved_level = f.context().node_level(); + + f.context_mut() + .set_node_level(NodeLevel::ParenthesizedExpression); + + let result = group(&format_args![ + if_group_breaks(&text("(")), + soft_block_indent(&Arguments::from(&self.inner)), + if_group_breaks(&text(")")), + ]) + .fmt(f); + + f.context_mut().set_node_level(saved_level); + + result + } +} /// Provides Python specific extensions to [`Formatter`]. pub(crate) trait PyFormatterExtensions<'ast, 'buf> { @@ -15,12 +52,27 @@ pub(crate) trait PyFormatterExtensions<'ast, 'buf> { /// * [`NodeLevel::CompoundStatement`]: Up to one empty line /// * [`NodeLevel::Expression`]: No empty lines fn join_nodes<'fmt>(&'fmt mut self, level: NodeLevel) -> JoinNodesBuilder<'fmt, 'ast, 'buf>; + + /// A builder that separates each element by a `,` and a [`soft_line_break_or_space`]. + /// It emits a trailing `,` that is only shown if the enclosing group expands. It forces the enclosing + /// group to expand if the last item has a trailing `comma` and the magical comma option is enabled. + fn join_comma_separated<'fmt>( + &'fmt mut self, + sequence_end: TextSize, + ) -> JoinCommaSeparatedBuilder<'fmt, 'ast, 'buf>; } impl<'buf, 'ast> PyFormatterExtensions<'ast, 'buf> for PyFormatter<'ast, 'buf> { fn join_nodes<'fmt>(&'fmt mut self, level: NodeLevel) -> JoinNodesBuilder<'fmt, 'ast, 'buf> { JoinNodesBuilder::new(self, level) } + + fn join_comma_separated<'fmt>( + &'fmt mut self, + sequence_end: TextSize, + ) -> JoinCommaSeparatedBuilder<'fmt, 'ast, 'buf> { + JoinCommaSeparatedBuilder::new(self, sequence_end) + } } #[must_use = "must eventually call `finish()` on the builder."] @@ -51,7 +103,7 @@ impl<'fmt, 'ast, 'buf> JoinNodesBuilder<'fmt, 'ast, 'buf> { self.result = self.result.and_then(|_| { if let Some(last_end) = self.last_end.replace(node.end()) { - let source = self.fmt.context().contents(); + let source = self.fmt.context().source(); let count_lines = |offset| { // It's necessary to skip any trailing line comment because RustPython doesn't include trailing comments // in the node's range @@ -79,7 +131,9 @@ impl<'fmt, 'ast, 'buf> JoinNodesBuilder<'fmt, 'ast, 'buf> { 0 | 1 => hard_line_break().fmt(self.fmt), _ => empty_line().fmt(self.fmt), }, - NodeLevel::Expression => hard_line_break().fmt(self.fmt), + NodeLevel::Expression(_) | NodeLevel::ParenthesizedExpression => { + hard_line_break().fmt(self.fmt) + } }?; } @@ -145,15 +199,126 @@ impl<'fmt, 'ast, 'buf> JoinNodesBuilder<'fmt, 'ast, 'buf> { } } +pub(crate) struct JoinCommaSeparatedBuilder<'fmt, 'ast, 'buf> { + result: FormatResult<()>, + fmt: &'fmt mut PyFormatter<'ast, 'buf>, + end_of_last_entry: Option, + sequence_end: TextSize, + /// We need to track whether we have more than one entry since a sole entry doesn't get a + /// magic trailing comma even when expanded + len: usize, +} + +impl<'fmt, 'ast, 'buf> JoinCommaSeparatedBuilder<'fmt, 'ast, 'buf> { + fn new(f: &'fmt mut PyFormatter<'ast, 'buf>, sequence_end: TextSize) -> Self { + Self { + fmt: f, + result: Ok(()), + end_of_last_entry: None, + len: 0, + sequence_end, + } + } + + pub(crate) fn entry( + &mut self, + node: &T, + content: &dyn Format>, + ) -> &mut Self + where + T: Ranged, + { + self.result = self.result.and_then(|_| { + if self.end_of_last_entry.is_some() { + write!(self.fmt, [text(","), soft_line_break_or_space()])?; + } + + self.end_of_last_entry = Some(node.end()); + self.len += 1; + + content.fmt(self.fmt) + }); + + self + } + + #[allow(unused)] + pub(crate) fn entries(&mut self, entries: I) -> &mut Self + where + T: Ranged, + F: Format>, + I: IntoIterator, + { + for (node, content) in entries { + self.entry(&node, &content); + } + + self + } + + pub(crate) fn nodes<'a, T, I>(&mut self, entries: I) -> &mut Self + where + T: Ranged + AsFormat> + 'a, + I: IntoIterator, + { + for node in entries { + self.entry(node, &node.format()); + } + + self + } + + pub(crate) fn finish(&mut self) -> FormatResult<()> { + self.result.and_then(|_| { + if let Some(last_end) = self.end_of_last_entry.take() { + let magic_trailing_comma = match self.fmt.options().magic_trailing_comma() { + MagicTrailingComma::Respect => { + let first_token = SimpleTokenizer::new( + self.fmt.context().source(), + TextRange::new(last_end, self.sequence_end), + ) + .skip_trivia() + // Skip over any closing parentheses belonging to the expression + .find(|token| token.kind() != TokenKind::RParen); + + matches!( + first_token, + Some(Token { + kind: TokenKind::Comma, + .. + }) + ) + } + MagicTrailingComma::Ignore => false, + }; + + // If there is a single entry, only keep the magic trailing comma, don't add it if + // it wasn't there. If there is more than one entry, always add it. + if magic_trailing_comma || self.len > 1 { + if_group_breaks(&text(",")).fmt(self.fmt)?; + } + + if magic_trailing_comma { + expand_parent().fmt(self.fmt)?; + } + } + + Ok(()) + }) + } +} + #[cfg(test)] mod tests { + use rustpython_parser::ast::ModModule; + use rustpython_parser::Parse; + + use ruff_formatter::format; + use crate::comments::Comments; use crate::context::{NodeLevel, PyFormatContext}; use crate::prelude::*; - use ruff_formatter::format; - use ruff_formatter::SimpleFormatOptions; - use rustpython_parser::ast::ModModule; - use rustpython_parser::Parse; + use crate::PyFormatOptions; fn format_ranged(level: NodeLevel) -> String { let source = r#" @@ -172,8 +337,7 @@ no_leading_newline = 30 let module = ModModule::parse(source, "test.py").unwrap(); - let context = - PyFormatContext::new(SimpleFormatOptions::default(), source, Comments::default()); + let context = PyFormatContext::new(PyFormatOptions::default(), source, Comments::default()); let test_formatter = format_with(|f: &mut PyFormatter| f.join_nodes(level).nodes(&module.body).finish()); @@ -225,7 +389,7 @@ no_leading_newline = 30"# // Removes all empty lines #[test] fn ranged_builder_parenthesized_level() { - let printed = format_ranged(NodeLevel::Expression); + let printed = format_ranged(NodeLevel::Expression(None)); assert_eq!( &printed, diff --git a/crates/ruff_python_formatter/src/cli.rs b/crates/ruff_python_formatter/src/cli.rs index f605e66407..15d480377f 100644 --- a/crates/ruff_python_formatter/src/cli.rs +++ b/crates/ruff_python_formatter/src/cli.rs @@ -10,7 +10,7 @@ use rustpython_parser::{parse_tokens, Mode}; use ruff_formatter::SourceCode; use ruff_python_ast::source_code::CommentRangesBuilder; -use crate::format_node; +use crate::{format_node, PyFormatOptions}; #[derive(ValueEnum, Clone, Debug)] pub enum Emit { @@ -57,13 +57,18 @@ pub fn format_and_debug_print(input: &str, cli: &Cli) -> Result { let python_ast = parse_tokens(tokens, Mode::Module, "") .with_context(|| "Syntax error in input")?; - let formatted = format_node(&python_ast, &comment_ranges, input)?; + let formatted = format_node( + &python_ast, + &comment_ranges, + input, + PyFormatOptions::default(), + )?; if cli.print_ir { println!("{}", formatted.document().display(SourceCode::new(input))); } if cli.print_comments { println!( - "{:?}", + "{:#?}", formatted.context().comments().debug(SourceCode::new(input)) ); } diff --git a/crates/ruff_python_formatter/src/comments/debug.rs b/crates/ruff_python_formatter/src/comments/debug.rs index 6bb123db7d..37be2ed9c0 100644 --- a/crates/ruff_python_formatter/src/comments/debug.rs +++ b/crates/ruff_python_formatter/src/comments/debug.rs @@ -1,9 +1,12 @@ +use std::fmt::{Debug, Formatter, Write}; + +use itertools::Itertools; +use rustpython_parser::ast::Ranged; + +use ruff_formatter::SourceCode; + use crate::comments::node_key::NodeRefEqualityKey; use crate::comments::{CommentsMap, SourceComment}; -use itertools::Itertools; -use ruff_formatter::SourceCode; -use ruff_python_ast::prelude::*; -use std::fmt::{Debug, Formatter, Write}; /// Prints a debug representation of [`SourceComment`] that includes the comment's text pub(crate) struct DebugComment<'a> { @@ -26,10 +29,8 @@ impl Debug for DebugComment<'_> { strut .field("text", &self.comment.slice.text(self.source_code)) - .field("position", &self.comment.position); - - #[cfg(debug_assertions)] - strut.field("formatted", &self.comment.formatted.get()); + .field("position", &self.comment.line_position) + .field("formatted", &self.comment.formatted.get()); strut.finish() } @@ -176,14 +177,16 @@ impl Debug for DebugNodeCommentSlice<'_> { #[cfg(test)] mod tests { - use crate::comments::map::MultiMap; - use crate::comments::{CommentTextPosition, Comments, CommentsMap, SourceComment}; use insta::assert_debug_snapshot; - use ruff_formatter::SourceCode; - use ruff_python_ast::node::AnyNode; use ruff_text_size::{TextRange, TextSize}; use rustpython_parser::ast::{StmtBreak, StmtContinue}; + use ruff_formatter::SourceCode; + use ruff_python_ast::node::AnyNode; + + use crate::comments::map::MultiMap; + use crate::comments::{CommentLinePosition, Comments, CommentsMap, SourceComment}; + #[test] fn debug() { let continue_statement = AnyNode::from(StmtContinue { @@ -208,7 +211,7 @@ break; continue_statement.as_ref().into(), SourceComment::new( source_code.slice(TextRange::at(TextSize::new(0), TextSize::new(17))), - CommentTextPosition::OwnLine, + CommentLinePosition::OwnLine, ), ); @@ -216,7 +219,7 @@ break; continue_statement.as_ref().into(), SourceComment::new( source_code.slice(TextRange::at(TextSize::new(28), TextSize::new(10))), - CommentTextPosition::EndOfLine, + CommentLinePosition::EndOfLine, ), ); @@ -224,7 +227,7 @@ break; break_statement.as_ref().into(), SourceComment::new( source_code.slice(TextRange::at(TextSize::new(39), TextSize::new(15))), - CommentTextPosition::OwnLine, + CommentLinePosition::OwnLine, ), ); diff --git a/crates/ruff_python_formatter/src/comments/format.rs b/crates/ruff_python_formatter/src/comments/format.rs index 70eb87f679..84b0e3b654 100644 --- a/crates/ruff_python_formatter/src/comments/format.rs +++ b/crates/ruff_python_formatter/src/comments/format.rs @@ -1,12 +1,13 @@ +use ruff_text_size::{TextLen, TextRange, TextSize}; +use rustpython_parser::ast::Ranged; + +use ruff_formatter::{format_args, write, FormatError, SourceCode}; +use ruff_python_ast::node::{AnyNodeRef, AstNode}; + use crate::comments::SourceComment; use crate::context::NodeLevel; use crate::prelude::*; use crate::trivia::{lines_after, lines_before, skip_trailing_trivia}; -use ruff_formatter::{format_args, write, FormatError, SourceCode}; -use ruff_python_ast::node::AnyNodeRef; -use ruff_python_ast::prelude::AstNode; -use ruff_text_size::{TextLen, TextRange, TextSize}; -use rustpython_parser::ast::Ranged; /// Formats the leading comments of a node. pub(crate) fn leading_node_comments(node: &T) -> FormatLeadingComments @@ -42,7 +43,7 @@ impl Format> for FormatLeadingComments<'_> { { let slice = comment.slice(); - let lines_after_comment = lines_after(slice.end(), f.context().contents()); + let lines_after_comment = lines_after(slice.end(), f.context().source()); write!( f, [format_comment(comment), empty_lines(lines_after_comment)] @@ -69,7 +70,7 @@ where { FormatLeadingAlternateBranchComments { comments, - last_node: last_node.map(std::convert::Into::into), + last_node: last_node.map(Into::into), } } @@ -83,16 +84,16 @@ impl Format> for FormatLeadingAlternateBranchComments<'_> { if let Some(first_leading) = self.comments.first() { // Leading comments only preserves the lines after the comment but not before. // Insert the necessary lines. - if lines_before(first_leading.slice().start(), f.context().contents()) > 1 { + if lines_before(first_leading.slice().start(), f.context().source()) > 1 { write!(f, [empty_line()])?; } write!(f, [leading_comments(self.comments)])?; } else if let Some(last_preceding) = self.last_node { - let full_end = skip_trailing_trivia(last_preceding.end(), f.context().contents()); + let full_end = skip_trailing_trivia(last_preceding.end(), f.context().source()); // The leading comments formatting ensures that it preserves the right amount of lines after // We need to take care of this ourselves, if there's no leading `else` comment. - if lines_after(full_end, f.context().contents()) > 1 { + if lines_after(full_end, f.context().source()) > 1 { write!(f, [empty_line()])?; } } @@ -136,10 +137,10 @@ impl Format> for FormatTrailingComments<'_> { { let slice = trailing.slice(); - has_trailing_own_line_comment |= trailing.position().is_own_line(); + has_trailing_own_line_comment |= trailing.line_position().is_own_line(); if has_trailing_own_line_comment { - let lines_before_comment = lines_before(slice.start(), f.context().contents()); + let lines_before_comment = lines_before(slice.start(), f.context().source()); // A trailing comment at the end of a body or list // ```python @@ -208,7 +209,7 @@ impl Format> for FormatDanglingComments<'_> { .iter() .filter(|comment| comment.is_unformatted()) { - if first && comment.position().is_end_of_line() { + if first && comment.line_position().is_end_of_line() { write!(f, [space(), space()])?; } @@ -216,7 +217,7 @@ impl Format> for FormatDanglingComments<'_> { f, [ format_comment(comment), - empty_lines(lines_after(comment.slice().end(), f.context().contents())) + empty_lines(lines_after(comment.slice().end(), f.context().source())) ] )?; @@ -244,7 +245,7 @@ struct FormatComment<'a> { impl Format> for FormatComment<'_> { fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { let slice = self.comment.slice(); - let comment_text = slice.text(SourceCode::new(f.context().contents())); + let comment_text = slice.text(SourceCode::new(f.context().source())); let trimmed = comment_text.trim_end(); let trailing_whitespace_len = comment_text.text_len() - trimmed.text_len(); @@ -317,7 +318,9 @@ impl Format> for FormatEmptyLines { }, // Remove all whitespace in parenthesized expressions - NodeLevel::Expression => write!(f, [hard_line_break()]), + NodeLevel::Expression(_) | NodeLevel::ParenthesizedExpression => { + write!(f, [hard_line_break()]) + } } } } diff --git a/crates/ruff_python_formatter/src/comments/mod.rs b/crates/ruff_python_formatter/src/comments/mod.rs index 171073d565..71912c60fd 100644 --- a/crates/ruff_python_formatter/src/comments/mod.rs +++ b/crates/ruff_python_formatter/src/comments/mod.rs @@ -87,10 +87,13 @@ //! //! It is possible to add an additional optional label to [`SourceComment`] If ever the need arises to distinguish two *dangling comments* in the formatting logic, -use crate::comments::debug::{DebugComment, DebugComments}; -use crate::comments::map::MultiMap; -use crate::comments::node_key::NodeRefEqualityKey; -use crate::comments::visitor::CommentsVisitor; +use ruff_text_size::TextRange; +use std::cell::Cell; +use std::fmt::Debug; +use std::rc::Rc; + +use rustpython_parser::ast::{Mod, Ranged}; + pub(crate) use format::{ dangling_comments, dangling_node_comments, leading_alternate_branch_comments, leading_comments, leading_node_comments, trailing_comments, trailing_node_comments, @@ -98,10 +101,11 @@ pub(crate) use format::{ use ruff_formatter::{SourceCode, SourceCodeSlice}; use ruff_python_ast::node::AnyNodeRef; use ruff_python_ast::source_code::CommentRanges; -use rustpython_parser::ast::Mod; -use std::cell::Cell; -use std::fmt::Debug; -use std::rc::Rc; + +use crate::comments::debug::{DebugComment, DebugComments}; +use crate::comments::map::MultiMap; +use crate::comments::node_key::NodeRefEqualityKey; +use crate::comments::visitor::CommentsVisitor; mod debug; mod format; @@ -111,20 +115,20 @@ mod placement; mod visitor; /// A comment in the source document. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Eq, PartialEq)] pub(crate) struct SourceComment { /// The location of the comment in the source document. slice: SourceCodeSlice, /// Whether the comment has been formatted or not. formatted: Cell, - position: CommentTextPosition, + line_position: CommentLinePosition, } impl SourceComment { - fn new(slice: SourceCodeSlice, position: CommentTextPosition) -> Self { + fn new(slice: SourceCodeSlice, position: CommentLinePosition) -> Self { Self { slice, - position, + line_position: position, formatted: Cell::new(false), } } @@ -135,8 +139,8 @@ impl SourceComment { &self.slice } - pub(crate) const fn position(&self) -> CommentTextPosition { - self.position + pub(crate) const fn line_position(&self) -> CommentLinePosition { + self.line_position } /// Marks the comment as formatted @@ -152,18 +156,23 @@ impl SourceComment { pub(crate) fn is_unformatted(&self) -> bool { !self.is_formatted() } -} -impl SourceComment { /// Returns a nice debug representation that prints the source code for every comment (and not just the range). pub(crate) fn debug<'a>(&'a self, source_code: SourceCode<'a>) -> DebugComment<'a> { DebugComment::new(self, source_code) } } +impl Ranged for SourceComment { + #[inline] + fn range(&self) -> TextRange { + self.slice.range() + } +} + /// The position of a comment in the source text. #[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub(crate) enum CommentTextPosition { +pub(crate) enum CommentLinePosition { /// A comment that is on the same line as the preceding token and is separated by at least one line break from the following token. /// /// # Examples @@ -176,7 +185,7 @@ pub(crate) enum CommentTextPosition { /// ``` /// /// `# comment` is an end of line comments because it is separated by at least one line break from the following token `b`. - /// Comments that not only end, but also start on a new line are [`OwnLine`](CommentTextPosition::OwnLine) comments. + /// Comments that not only end, but also start on a new line are [`OwnLine`](CommentLinePosition::OwnLine) comments. EndOfLine, /// A Comment that is separated by at least one line break from the preceding token. @@ -193,13 +202,13 @@ pub(crate) enum CommentTextPosition { OwnLine, } -impl CommentTextPosition { +impl CommentLinePosition { pub(crate) const fn is_own_line(self) -> bool { - matches!(self, CommentTextPosition::OwnLine) + matches!(self, CommentLinePosition::OwnLine) } pub(crate) const fn is_end_of_line(self) -> bool { - matches!(self, CommentTextPosition::EndOfLine) + matches!(self, CommentLinePosition::EndOfLine) } } @@ -208,7 +217,7 @@ type CommentsMap<'a> = MultiMap, SourceComment>; /// The comments of a syntax tree stored by node. /// /// Cloning `comments` is cheap as it only involves bumping a reference counter. -#[derive(Clone, Default)] +#[derive(Debug, Clone, Default)] pub(crate) struct Comments<'a> { /// The implementation uses an [Rc] so that [Comments] has a lifetime independent from the [crate::Formatter]. /// Independent lifetimes are necessary to support the use case where a (formattable object)[crate::Format] @@ -260,76 +269,109 @@ impl<'a> Comments<'a> { } #[inline] - pub(crate) fn has_comments(&self, node: AnyNodeRef) -> bool { - self.data.comments.has(&NodeRefEqualityKey::from_ref(node)) + pub(crate) fn has_comments(&self, node: T) -> bool + where + T: Into>, + { + self.data + .comments + .has(&NodeRefEqualityKey::from_ref(node.into())) } /// Returns `true` if the given `node` has any [leading comments](self#leading-comments). #[inline] - pub(crate) fn has_leading_comments(&self, node: AnyNodeRef) -> bool { + pub(crate) fn has_leading_comments(&self, node: T) -> bool + where + T: Into>, + { !self.leading_comments(node).is_empty() } /// Returns the `node`'s [leading comments](self#leading-comments). #[inline] - pub(crate) fn leading_comments(&self, node: AnyNodeRef<'a>) -> &[SourceComment] { + pub(crate) fn leading_comments(&self, node: T) -> &[SourceComment] + where + T: Into>, + { self.data .comments - .leading(&NodeRefEqualityKey::from_ref(node)) + .leading(&NodeRefEqualityKey::from_ref(node.into())) } /// Returns `true` if node has any [dangling comments](self#dangling-comments). - pub(crate) fn has_dangling_comments(&self, node: AnyNodeRef<'a>) -> bool { + pub(crate) fn has_dangling_comments(&self, node: T) -> bool + where + T: Into>, + { !self.dangling_comments(node).is_empty() } /// Returns the [dangling comments](self#dangling-comments) of `node` - pub(crate) fn dangling_comments(&self, node: AnyNodeRef<'a>) -> &[SourceComment] { + pub(crate) fn dangling_comments(&self, node: T) -> &[SourceComment] + where + T: Into>, + { self.data .comments - .dangling(&NodeRefEqualityKey::from_ref(node)) + .dangling(&NodeRefEqualityKey::from_ref(node.into())) } /// Returns the `node`'s [trailing comments](self#trailing-comments). #[inline] - pub(crate) fn trailing_comments(&self, node: AnyNodeRef<'a>) -> &[SourceComment] { + pub(crate) fn trailing_comments(&self, node: T) -> &[SourceComment] + where + T: Into>, + { self.data .comments - .trailing(&NodeRefEqualityKey::from_ref(node)) + .trailing(&NodeRefEqualityKey::from_ref(node.into())) } /// Returns `true` if the given `node` has any [trailing comments](self#trailing-comments). #[inline] - pub(crate) fn has_trailing_comments(&self, node: AnyNodeRef) -> bool { + pub(crate) fn has_trailing_comments(&self, node: T) -> bool + where + T: Into>, + { !self.trailing_comments(node).is_empty() } /// Returns `true` if the given `node` has any [trailing own line comments](self#trailing-comments). #[inline] - pub(crate) fn has_trailing_own_line_comments(&self, node: AnyNodeRef) -> bool { + pub(crate) fn has_trailing_own_line_comments(&self, node: T) -> bool + where + T: Into>, + { self.trailing_comments(node) .iter() - .any(|comment| comment.position().is_own_line()) + .any(|comment| comment.line_position().is_own_line()) } /// Returns an iterator over the [leading](self#leading-comments) and [trailing comments](self#trailing-comments) of `node`. - pub(crate) fn leading_trailing_comments( + pub(crate) fn leading_trailing_comments( &self, - node: AnyNodeRef<'a>, - ) -> impl Iterator { + node: T, + ) -> impl Iterator + where + T: Into>, + { + let node = node.into(); self.leading_comments(node) .iter() .chain(self.trailing_comments(node).iter()) } /// Returns an iterator over the [leading](self#leading-comments), [dangling](self#dangling-comments), and [trailing](self#trailing) comments of `node`. - pub(crate) fn leading_dangling_trailing_comments( + pub(crate) fn leading_dangling_trailing_comments( &self, - node: AnyNodeRef<'a>, - ) -> impl Iterator { + node: T, + ) -> impl Iterator + where + T: Into>, + { self.data .comments - .parts(&NodeRefEqualityKey::from_ref(node)) + .parts(&NodeRefEqualityKey::from_ref(node.into())) } #[inline(always)] @@ -364,21 +406,23 @@ impl<'a> Comments<'a> { } } -#[derive(Default)] +#[derive(Debug, Default)] struct CommentsData<'a> { comments: CommentsMap<'a>, } #[cfg(test)] mod tests { - use crate::comments::Comments; use insta::assert_debug_snapshot; - use ruff_formatter::SourceCode; - use ruff_python_ast::prelude::*; - use ruff_python_ast::source_code::{CommentRanges, CommentRangesBuilder}; + use rustpython_parser::ast::Mod; use rustpython_parser::lexer::lex; use rustpython_parser::{parse_tokens, Mode}; + use ruff_formatter::SourceCode; + use ruff_python_ast::source_code::{CommentRanges, CommentRangesBuilder}; + + use crate::comments::Comments; + struct CommentsTestCase<'a> { module: Mod, comment_ranges: CommentRanges, @@ -398,7 +442,7 @@ mod tests { let comment_ranges = comment_ranges.finish(); - let parsed = parse_tokens(tokens.into_iter(), Mode::Module, "test.py") + let parsed = parse_tokens(tokens, Mode::Module, "test.py") .expect("Expect source to be valid Python"); CommentsTestCase { diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index c1c2b8df1f..b82d4947c4 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -1,34 +1,53 @@ -use crate::comments::visitor::{CommentPlacement, DecoratedComment}; -use crate::comments::CommentTextPosition; -use crate::trivia::{SimpleTokenizer, TokenKind}; -use ruff_python_ast::node::AnyNodeRef; +use std::cmp::Ordering; + +use ruff_text_size::TextRange; +use rustpython_parser::ast; +use rustpython_parser::ast::{Expr, ExprIfExp, ExprSlice, Ranged}; + +use ruff_python_ast::node::{AnyNodeRef, AstNode}; use ruff_python_ast::source_code::Locator; use ruff_python_ast::whitespace; use ruff_python_whitespace::{PythonWhitespace, UniversalNewlines}; -use ruff_text_size::{TextRange, TextSize}; -use rustpython_parser::ast::Ranged; -use std::cmp::Ordering; + +use crate::comments::visitor::{CommentPlacement, DecoratedComment}; +use crate::expression::expr_slice::{assign_comment_in_slice, ExprSliceCommentSection}; +use crate::other::arguments::{ + assign_argument_separator_comment_placement, find_argument_separators, +}; +use crate::trivia::{first_non_trivia_token_rev, SimpleTokenizer, Token, TokenKind}; /// Implements the custom comment placement logic. pub(super) fn place_comment<'a>( - comment: DecoratedComment<'a>, + mut comment: DecoratedComment<'a>, locator: &Locator, ) -> CommentPlacement<'a> { - handle_in_between_excepthandlers_or_except_handler_and_else_or_finally_comment(comment, locator) - .or_else(|comment| handle_match_comment(comment, locator)) - .or_else(|comment| handle_in_between_bodies_own_line_comment(comment, locator)) - .or_else(|comment| handle_in_between_bodies_end_of_line_comment(comment, locator)) - .or_else(|comment| handle_trailing_body_comment(comment, locator)) - .or_else(handle_trailing_end_of_line_body_comment) - .or_else(|comment| handle_trailing_end_of_line_condition_comment(comment, locator)) - .or_else(|comment| { - handle_module_level_own_line_comment_before_class_or_function_comment(comment, locator) - }) - .or_else(|comment| handle_positional_only_arguments_separator_comment(comment, locator)) - .or_else(|comment| { - handle_trailing_binary_expression_left_or_operator_comment(comment, locator) - }) - .or_else(handle_leading_function_with_decorators_comment) + static HANDLERS: &[for<'a> fn(DecoratedComment<'a>, &Locator) -> CommentPlacement<'a>] = &[ + handle_in_between_except_handlers_or_except_handler_and_else_or_finally_comment, + handle_match_comment, + handle_in_between_bodies_own_line_comment, + handle_in_between_bodies_end_of_line_comment, + handle_trailing_body_comment, + handle_trailing_end_of_line_body_comment, + handle_trailing_end_of_line_condition_comment, + handle_trailing_end_of_line_except_comment, + handle_module_level_own_line_comment_before_class_or_function_comment, + handle_arguments_separator_comment, + handle_trailing_binary_expression_left_or_operator_comment, + handle_leading_function_with_decorators_comment, + handle_dict_unpacking_comment, + handle_slice_comments, + handle_attribute_comment, + handle_expr_if_comment, + handle_comprehension_comment, + handle_trailing_expression_starred_star_end_of_line_comment, + ]; + for handler in HANDLERS { + comment = match handler(comment, locator) { + CommentPlacement::Default(comment) => comment, + placement => return placement, + }; + } + CommentPlacement::Default(comment) } /// Handles leading comments in front of a match case or a trailing comment of the `match` statement. @@ -45,20 +64,18 @@ fn handle_match_comment<'a>( locator: &Locator, ) -> CommentPlacement<'a> { // Must be an own line comment after the last statement in a match case - if comment.text_position().is_end_of_line() || comment.following_node().is_some() { + if comment.line_position().is_end_of_line() || comment.following_node().is_some() { return CommentPlacement::Default(comment); } // Get the enclosing match case let Some(match_case) = comment.enclosing_node().match_case() else { - return CommentPlacement::Default(comment) + return CommentPlacement::Default(comment); }; // And its parent match statement. - let Some(match_stmt) = comment - .enclosing_parent() - .and_then(AnyNodeRef::stmt_match) else { - return CommentPlacement::Default(comment) + let Some(match_stmt) = comment.enclosing_parent().and_then(AnyNodeRef::stmt_match) else { + return CommentPlacement::Default(comment); }; // Get the next sibling (sibling traversal would be really nice) @@ -138,49 +155,59 @@ fn handle_match_comment<'a>( } } -/// Handles comments between excepthandlers and between the last except handler and any following `else` or `finally` block. -fn handle_in_between_excepthandlers_or_except_handler_and_else_or_finally_comment<'a>( +/// Handles comments between except handlers and between the last except handler and any following `else` or `finally` block. +fn handle_in_between_except_handlers_or_except_handler_and_else_or_finally_comment<'a>( comment: DecoratedComment<'a>, locator: &Locator, ) -> CommentPlacement<'a> { - if comment.text_position().is_end_of_line() || comment.following_node().is_none() { + if comment.line_position().is_end_of_line() { return CommentPlacement::Default(comment); } - if let Some(AnyNodeRef::ExcepthandlerExceptHandler(except_handler)) = comment.preceding_node() { - // it now depends on the indentation level of the comment if it is a leading comment for e.g. - // the following `elif` or indeed a trailing comment of the previous body's last statement. - let comment_indentation = - whitespace::indentation_at_offset(locator, comment.slice().range().start()) - .map(str::len) - .unwrap_or_default(); + let (Some(AnyNodeRef::ExceptHandlerExceptHandler(preceding_except_handler)), Some(following)) = + (comment.preceding_node(), comment.following_node()) + else { + return CommentPlacement::Default(comment); + }; - if let Some(except_indentation) = - whitespace::indentation(locator, except_handler).map(str::len) - { - return if comment_indentation <= except_indentation { - // It has equal, or less indent than the `except` handler. It must be a comment - // of the following `finally` or `else` block - // - // ```python - // try: - // pass - // except Exception: - // print("noop") - // # leading - // finally: - // pass - // ``` - // Attach it to the `try` statement. - CommentPlacement::dangling(comment.enclosing_node(), comment) - } else { - // Delegate to `handle_trailing_body_comment` - CommentPlacement::Default(comment) - }; - } + // it now depends on the indentation level of the comment if it is a leading comment for e.g. + // the following `finally` or indeed a trailing comment of the previous body's last statement. + let comment_indentation = + whitespace::indentation_at_offset(locator, comment.slice().range().start()) + .map(str::len) + .unwrap_or_default(); + + let Some(except_indentation) = + whitespace::indentation(locator, preceding_except_handler).map(str::len) + else { + return CommentPlacement::Default(comment); + }; + + if comment_indentation > except_indentation { + // Delegate to `handle_trailing_body_comment` + return CommentPlacement::Default(comment); } - CommentPlacement::Default(comment) + // It has equal, or less indent than the `except` handler. It must be a comment of a subsequent + // except handler or of the following `finally` or `else` block + // + // ```python + // try: + // pass + // except Exception: + // print("noop") + // # leading + // finally: + // pass + // ``` + + if following.is_except_handler() { + // Attach it to the following except handler (which has a node) as leading + CommentPlacement::leading(following, comment) + } else { + // No following except handler; attach it to the `try` statement.as dangling + CommentPlacement::dangling(comment.enclosing_node(), comment) + } } /// Handles own line comments between the last statement and the first statement of two bodies. @@ -197,7 +224,7 @@ fn handle_in_between_bodies_own_line_comment<'a>( comment: DecoratedComment<'a>, locator: &Locator, ) -> CommentPlacement<'a> { - if !comment.text_position().is_own_line() { + if !comment.line_position().is_own_line() { return CommentPlacement::Default(comment); } @@ -306,60 +333,94 @@ fn handle_in_between_bodies_end_of_line_comment<'a>( comment: DecoratedComment<'a>, locator: &Locator, ) -> CommentPlacement<'a> { - if !comment.text_position().is_end_of_line() { + if !comment.line_position().is_end_of_line() { return CommentPlacement::Default(comment); } // The comment must be between two statements... - if let (Some(preceding), Some(following)) = (comment.preceding_node(), comment.following_node()) - { - // ...and the following statement must be the first statement in an alternate body of the parent... - if !is_first_statement_in_enclosing_alternate_body(following, comment.enclosing_node()) { - // ```python - // if test: - // a - // # comment - // b - // ``` - return CommentPlacement::Default(comment); - } + let (Some(preceding), Some(following)) = (comment.preceding_node(), comment.following_node()) + else { + return CommentPlacement::Default(comment); + }; - if !locator.contains_line_break(TextRange::new(preceding.end(), comment.slice().start())) { - // Trailing comment of the preceding statement + // ...and the following statement must be the first statement in an alternate body of the parent... + if !is_first_statement_in_enclosing_alternate_body(following, comment.enclosing_node()) { + // ```python + // if test: + // a + // # comment + // b + // ``` + return CommentPlacement::Default(comment); + } + + if locator.contains_line_break(TextRange::new(preceding.end(), comment.slice().start())) { + // The `elif` or except handlers have their own body to which we can attach the trailing comment + // ```python + // if test: + // a + // elif c: # comment + // b + // ``` + if following.is_except_handler() { + return CommentPlacement::trailing(following, comment); + } else if following.is_stmt_if() { + // We have to exclude for following if statements that are not elif by checking the + // indentation // ```python - // while test: - // a # comment - // else: - // b - // ``` - CommentPlacement::trailing(preceding, comment) - } else if following.is_stmt_if() || following.is_except_handler() { - // The `elif` or except handlers have their own body to which we can attach the trailing comment - // ```python - // if test: - // a - // elif c: # comment - // b - // ``` - CommentPlacement::trailing(following, comment) - } else { - // There are no bodies for the "else" branch and other bodies that are represented as a `Vec`. - // This means, there's no good place to attach the comments to. - // Make this a dangling comments and manually format the comment in - // in the enclosing node's formatting logic. For `try`, it's the formatters responsibility - // to correctly identify the comments for the `finally` and `orelse` block by looking - // at the comment's range. - // - // ```python - // while x == y: + // if True: + // pass + // else: # Comment + // if False: + // pass // pass - // else: # trailing - // print("nooop") // ``` - CommentPlacement::dangling(comment.enclosing_node(), comment) + let base_if_indent = + whitespace::indentation_at_offset(locator, following.range().start()); + let maybe_elif_indent = whitespace::indentation_at_offset( + locator, + comment.enclosing_node().range().start(), + ); + if base_if_indent == maybe_elif_indent { + return CommentPlacement::trailing(following, comment); + } } + // There are no bodies for the "else" branch and other bodies that are represented as a `Vec`. + // This means, there's no good place to attach the comments to. + // Make this a dangling comments and manually format the comment in + // in the enclosing node's formatting logic. For `try`, it's the formatters responsibility + // to correctly identify the comments for the `finally` and `orelse` block by looking + // at the comment's range. + // + // ```python + // while x == y: + // pass + // else: # trailing + // print("nooop") + // ``` + CommentPlacement::dangling(comment.enclosing_node(), comment) } else { - CommentPlacement::Default(comment) + // Trailing comment of the preceding statement + // ```python + // while test: + // a # comment + // else: + // b + // ``` + if preceding.is_node_with_body() { + // We can't set this as a trailing comment of the function declaration because it + // will then move behind the function block instead of sticking with the pass + // ```python + // if True: + // def f(): + // pass # a + // else: + // pass + // ``` + CommentPlacement::Default(comment) + } else { + CommentPlacement::trailing(preceding, comment) + } } } @@ -376,16 +437,22 @@ fn handle_trailing_body_comment<'a>( comment: DecoratedComment<'a>, locator: &Locator, ) -> CommentPlacement<'a> { - if comment.text_position().is_end_of_line() { + if comment.line_position().is_end_of_line() { return CommentPlacement::Default(comment); } // Only do something if the preceding node has a body (has indented statements). - let Some(last_child) = comment.preceding_node().and_then(last_child_in_body) else { + let Some(preceding_node) = comment.preceding_node() else { return CommentPlacement::Default(comment); }; - let Some(comment_indentation) = whitespace::indentation_at_offset(locator, comment.slice().range().start()) else { + let Some(last_child) = last_child_in_body(preceding_node) else { + return CommentPlacement::Default(comment); + }; + + let Some(comment_indentation) = + whitespace::indentation_at_offset(locator, comment.slice().range().start()) + else { // The comment can't be a comment for the previous block if it isn't indented.. return CommentPlacement::Default(comment); }; @@ -394,8 +461,26 @@ fn handle_trailing_body_comment<'a>( // the indent-level doesn't depend on the tab width (the indent level must be the same if the tab width is 1 or 8). let comment_indentation_len = comment_indentation.len(); + // Keep the comment on the entire statement in case it's a trailing comment + // ```python + // if "first if": + // pass + // elif "first elif": + // pass + // # Trailing if comment + // ``` + // Here we keep the comment a trailing comment of the `if` + let Some(preceding_node_indentation) = + whitespace::indentation_at_offset(locator, preceding_node.start()) + else { + return CommentPlacement::Default(comment); + }; + if comment_indentation_len == preceding_node_indentation.len() { + return CommentPlacement::Default(comment); + } + let mut current_child = last_child; - let mut parent_body = comment.preceding_node(); + let mut parent_body = Some(preceding_node); let mut grand_parent_body = None; loop { @@ -466,9 +551,12 @@ fn handle_trailing_body_comment<'a>( /// if something.changed: /// do.stuff() # trailing comment /// ``` -fn handle_trailing_end_of_line_body_comment(comment: DecoratedComment<'_>) -> CommentPlacement<'_> { +fn handle_trailing_end_of_line_body_comment<'a>( + comment: DecoratedComment<'a>, + _locator: &Locator, +) -> CommentPlacement<'a> { // Must be an end of line comment - if comment.text_position().is_own_line() { + if comment.line_position().is_own_line() { return CommentPlacement::Default(comment); } @@ -506,35 +594,42 @@ fn handle_trailing_end_of_line_condition_comment<'a>( comment: DecoratedComment<'a>, locator: &Locator, ) -> CommentPlacement<'a> { - use ruff_python_ast::prelude::*; - // Must be an end of line comment - if comment.text_position().is_own_line() { + if comment.line_position().is_own_line() { return CommentPlacement::Default(comment); } // Must be between the condition expression and the first body element - let (Some(preceding), Some(following)) = (comment.preceding_node(), comment.following_node()) else { + let (Some(preceding), Some(following)) = (comment.preceding_node(), comment.following_node()) + else { return CommentPlacement::Default(comment); }; let expression_before_colon = match comment.enclosing_node() { - AnyNodeRef::StmtIf(StmtIf { test: expr, .. }) - | AnyNodeRef::StmtWhile(StmtWhile { test: expr, .. }) - | AnyNodeRef::StmtFor(StmtFor { iter: expr, .. }) - | AnyNodeRef::StmtAsyncFor(StmtAsyncFor { iter: expr, .. }) => { + AnyNodeRef::StmtIf(ast::StmtIf { test: expr, .. }) + | AnyNodeRef::StmtWhile(ast::StmtWhile { test: expr, .. }) + | AnyNodeRef::StmtFor(ast::StmtFor { iter: expr, .. }) + | AnyNodeRef::StmtAsyncFor(ast::StmtAsyncFor { iter: expr, .. }) => { Some(AnyNodeRef::from(expr.as_ref())) } - AnyNodeRef::StmtWith(StmtWith { items, .. }) - | AnyNodeRef::StmtAsyncWith(StmtAsyncWith { items, .. }) => { + AnyNodeRef::StmtWith(ast::StmtWith { items, .. }) + | AnyNodeRef::StmtAsyncWith(ast::StmtAsyncWith { items, .. }) => { items.last().map(AnyNodeRef::from) } - AnyNodeRef::StmtFunctionDef(StmtFunctionDef { returns, args, .. }) - | AnyNodeRef::StmtAsyncFunctionDef(StmtAsyncFunctionDef { returns, args, .. }) => returns - .as_deref() + AnyNodeRef::StmtFunctionDef(ast::StmtFunctionDef { returns, args, .. }) + | AnyNodeRef::StmtAsyncFunctionDef(ast::StmtAsyncFunctionDef { returns, args, .. }) => { + returns + .as_deref() + .map(AnyNodeRef::from) + .or_else(|| Some(AnyNodeRef::from(args.as_ref()))) + } + AnyNodeRef::StmtClassDef(ast::StmtClassDef { + bases, keywords, .. + }) => keywords + .last() .map(AnyNodeRef::from) - .or_else(|| Some(AnyNodeRef::from(args.as_ref()))), + .or_else(|| bases.last().map(AnyNodeRef::from)), _ => None, }; @@ -575,8 +670,13 @@ fn handle_trailing_end_of_line_condition_comment<'a>( TokenKind::RParen => { // Skip over any closing parentheses } - _ => { - unreachable!("Only ')' or ':' should follow the condition") + TokenKind::Comma => { + // Skip over any trailing comma + } + kind => { + unreachable!( + "Only ')' or ':' should follow the condition but encountered {kind:?}" + ) } } } @@ -585,18 +685,45 @@ fn handle_trailing_end_of_line_condition_comment<'a>( CommentPlacement::Default(comment) } -/// Attaches comments for the positional-only arguments separator `/` as trailing comments to the -/// enclosing [`Arguments`] node. +/// Handles end of line comments after the `:` of an except clause /// /// ```python -/// def test( -/// a, -/// # Positional arguments only after here -/// /, # trailing positional argument comment. -/// b, -/// ): pass +/// try: +/// ... +/// except: # comment +/// pass /// ``` -fn handle_positional_only_arguments_separator_comment<'a>( +/// +/// It attaches the comment as dangling comment to the enclosing except handler. +fn handle_trailing_end_of_line_except_comment<'a>( + comment: DecoratedComment<'a>, + _locator: &Locator, +) -> CommentPlacement<'a> { + let AnyNodeRef::ExceptHandlerExceptHandler(handler) = comment.enclosing_node() else { + return CommentPlacement::Default(comment); + }; + + // Must be an end of line comment + if comment.line_position().is_own_line() { + return CommentPlacement::Default(comment); + } + + let Some(first_body_statement) = handler.body.first() else { + return CommentPlacement::Default(comment); + }; + + if comment.slice().start() < first_body_statement.range().start() { + CommentPlacement::dangling(comment.enclosing_node(), comment) + } else { + CommentPlacement::Default(comment) + } +} + +/// Attaches comments for the positional only arguments separator `/` or the keywords only arguments +/// separator `*` as dangling comments to the enclosing [`Arguments`] node. +/// +/// See [`assign_argument_separator_comment_placement`] +fn handle_arguments_separator_comment<'a>( comment: DecoratedComment<'a>, locator: &Locator, ) -> CommentPlacement<'a> { @@ -604,59 +731,25 @@ fn handle_positional_only_arguments_separator_comment<'a>( return CommentPlacement::Default(comment); }; - // Using the `/` without any leading arguments is a syntax error. - let Some(last_argument_or_default) = comment.preceding_node() else { - return CommentPlacement::Default(comment); - }; - - let is_last_positional_argument = are_same_optional(last_argument_or_default, arguments.posonlyargs.last()) - // If the preceding node is the default for the last positional argument - // ```python - // def test(a=10, /, b): pass - // ``` - || arguments - .defaults - .iter() - .position(|default| AnyNodeRef::from(default).ptr_eq(last_argument_or_default)) - == Some(arguments.posonlyargs.len().saturating_sub(1)); - - if !is_last_positional_argument { - return CommentPlacement::Default(comment); + let (slash, star) = find_argument_separators(locator.contents(), arguments); + let comment_range = comment.slice().range(); + let placement = assign_argument_separator_comment_placement( + slash.as_ref(), + star.as_ref(), + comment_range, + comment.line_position(), + ); + if placement.is_some() { + return CommentPlacement::dangling(comment.enclosing_node(), comment); } - let trivia_end = comment - .following_node() - .map_or(arguments.end(), |following| following.start()); - let trivia_range = TextRange::new(last_argument_or_default.end(), trivia_end); - - if let Some(slash_offset) = find_pos_only_slash_offset(trivia_range, locator) { - let comment_start = comment.slice().range().start(); - let is_slash_comment = match comment.text_position() { - CommentTextPosition::EndOfLine => { - let preceding_end_line = locator.line_end(last_argument_or_default.end()); - let slash_comments_start = preceding_end_line.min(slash_offset); - - comment_start >= slash_comments_start - && locator.line_end(slash_offset) > comment_start - } - CommentTextPosition::OwnLine => comment_start < slash_offset, - }; - - if is_slash_comment { - CommentPlacement::dangling(comment.enclosing_node(), comment) - } else { - CommentPlacement::Default(comment) - } - } else { - // Should not happen, but let's go with it - CommentPlacement::Default(comment) - } + CommentPlacement::Default(comment) } /// Handles comments between the left side and the operator of a binary expression (trailing comments of the left), /// and trailing end-of-line comments that are on the same line as the operator. /// -///```python +/// ```python /// a = ( /// 5 # trailing left comment /// + # trailing operator comment @@ -702,7 +795,7 @@ fn handle_trailing_binary_expression_left_or_operator_comment<'a>( // ) // ``` CommentPlacement::trailing(AnyNodeRef::from(binary_expression.left.as_ref()), comment) - } else if comment.text_position().is_end_of_line() { + } else if comment.line_position().is_end_of_line() { // Is the operator on its own line. if locator.contains_line_break(TextRange::new( binary_expression.left.end(), @@ -791,13 +884,14 @@ fn handle_module_level_own_line_comment_before_class_or_function_comment<'a>( locator: &Locator, ) -> CommentPlacement<'a> { // Only applies for own line comments on the module level... - if !comment.text_position().is_own_line() || !comment.enclosing_node().is_module() { + if !comment.line_position().is_own_line() || !comment.enclosing_node().is_module() { return CommentPlacement::Default(comment); } // ... for comments with a preceding and following node, - let (Some(preceding), Some(following)) = (comment.preceding_node(), comment.following_node()) else { - return CommentPlacement::Default(comment) + let (Some(preceding), Some(following)) = (comment.preceding_node(), comment.following_node()) + else { + return CommentPlacement::Default(comment); }; // ... where the following is a function or class statement. @@ -820,28 +914,85 @@ fn handle_module_level_own_line_comment_before_class_or_function_comment<'a>( } } -/// Finds the offset of the `/` that separates the positional only and arguments from the other arguments. -/// Returns `None` if the positional only separator `/` isn't present in the specified range. -fn find_pos_only_slash_offset( - between_arguments_range: TextRange, +/// Handles the attaching comments left or right of the colon in a slice as trailing comment of the +/// preceding node or leading comment of the following node respectively. +/// ```python +/// a = "input"[ +/// 1 # c +/// # d +/// :2 +/// ] +/// ``` +fn handle_slice_comments<'a>( + comment: DecoratedComment<'a>, locator: &Locator, -) -> Option { - let mut tokens = - SimpleTokenizer::new(locator.contents(), between_arguments_range).skip_trivia(); - - if let Some(comma) = tokens.next() { - debug_assert_eq!(comma.kind(), TokenKind::Comma); - - if let Some(maybe_slash) = tokens.next() { - if maybe_slash.kind() == TokenKind::Slash { - return Some(maybe_slash.start()); +) -> CommentPlacement<'a> { + let expr_slice = match comment.enclosing_node() { + AnyNodeRef::ExprSlice(expr_slice) => expr_slice, + AnyNodeRef::ExprSubscript(expr_subscript) => { + if expr_subscript.value.end() < expr_subscript.slice.start() { + if let Expr::Slice(expr_slice) = expr_subscript.slice.as_ref() { + expr_slice + } else { + return CommentPlacement::Default(comment); + } + } else { + return CommentPlacement::Default(comment); } - - debug_assert_eq!(maybe_slash.kind(), TokenKind::RParen); } + _ => return CommentPlacement::Default(comment), + }; + + let ExprSlice { + range: _, + lower, + upper, + step, + } = expr_slice; + + // Check for `foo[ # comment`, but only if they are on the same line + let after_lbracket = matches!( + first_non_trivia_token_rev(comment.slice().start(), locator.contents()), + Some(Token { + kind: TokenKind::LBracket, + .. + }) + ); + if comment.line_position().is_end_of_line() && after_lbracket { + // Keep comments after the opening bracket there by formatting them outside the + // soft block indent + // ```python + // "a"[ # comment + // 1: + // ] + // ``` + debug_assert!( + matches!(comment.enclosing_node(), AnyNodeRef::ExprSubscript(_)), + "{:?}", + comment.enclosing_node() + ); + return CommentPlacement::dangling(comment.enclosing_node(), comment); } - None + let assignment = + assign_comment_in_slice(comment.slice().range(), locator.contents(), expr_slice); + let node = match assignment { + ExprSliceCommentSection::Lower => lower, + ExprSliceCommentSection::Upper => upper, + ExprSliceCommentSection::Step => step, + }; + + if let Some(node) = node { + if comment.slice().start() < node.start() { + CommentPlacement::leading(node.as_ref().into(), comment) + } else { + // If a trailing comment is an end of line comment that's fine because we have a node + // ahead of it + CommentPlacement::trailing(node.as_ref().into(), comment) + } + } else { + CommentPlacement::dangling(expr_slice.as_any_node_ref(), comment) + } } /// Handles own line comments between the last function decorator and the *header* of the function. @@ -854,7 +1005,10 @@ fn find_pos_only_slash_offset( /// def test(): /// ... /// ``` -fn handle_leading_function_with_decorators_comment(comment: DecoratedComment) -> CommentPlacement { +fn handle_leading_function_with_decorators_comment<'a>( + comment: DecoratedComment<'a>, + _locator: &Locator, +) -> CommentPlacement<'a> { let is_preceding_decorator = comment .preceding_node() .map_or(false, |node| node.is_decorator()); @@ -863,13 +1017,365 @@ fn handle_leading_function_with_decorators_comment(comment: DecoratedComment) -> .following_node() .map_or(false, |node| node.is_arguments()); - if comment.text_position().is_own_line() && is_preceding_decorator && is_following_arguments { + if comment.line_position().is_own_line() && is_preceding_decorator && is_following_arguments { CommentPlacement::dangling(comment.enclosing_node(), comment) } else { CommentPlacement::Default(comment) } } +/// Handles comments between `**` and the variable name in dict unpacking +/// It attaches these to the appropriate value node +/// +/// ```python +/// { +/// ** # comment between `**` and the variable name +/// value +/// ... +/// } +/// ``` +fn handle_dict_unpacking_comment<'a>( + comment: DecoratedComment<'a>, + locator: &Locator, +) -> CommentPlacement<'a> { + match comment.enclosing_node() { + // TODO: can maybe also add AnyNodeRef::Arguments here, but tricky to test due to + // https://github.com/astral-sh/ruff/issues/5176 + AnyNodeRef::ExprDict(_) | AnyNodeRef::Keyword(_) => {} + _ => { + return CommentPlacement::Default(comment); + } + }; + + // no node after our comment so we can't be between `**` and the name (node) + let Some(following) = comment.following_node() else { + return CommentPlacement::Default(comment); + }; + + // we look at tokens between the previous node (or the start of the dict) + // and the comment + let preceding_end = match comment.preceding_node() { + Some(preceding) => preceding.end(), + None => comment.enclosing_node().start(), + }; + if preceding_end > comment.slice().start() { + return CommentPlacement::Default(comment); + } + let mut tokens = SimpleTokenizer::new( + locator.contents(), + TextRange::new(preceding_end, comment.slice().start()), + ) + .skip_trivia(); + + // if the remaining tokens from the previous node are exactly `**`, + // re-assign the comment to the one that follows the stars + let mut count = 0; + + // we start from the preceding node but we skip its token + for token in tokens.by_ref() { + // Skip closing parentheses that are not part of the node range + if token.kind == TokenKind::RParen { + continue; + } + // The Keyword case + if token.kind == TokenKind::Star { + count += 1; + break; + } + // The dict case + debug_assert!( + matches!( + token, + Token { + kind: TokenKind::LBrace | TokenKind::Comma | TokenKind::Colon, + .. + } + ), + "{token:?}", + ); + break; + } + + for token in tokens { + if token.kind != TokenKind::Star { + return CommentPlacement::Default(comment); + } + count += 1; + } + if count == 2 { + return CommentPlacement::trailing(following, comment); + } + + CommentPlacement::Default(comment) +} + +/// Own line comments coming after the node are always dangling comments +/// ```python +/// ( +/// a +/// # trailing a comment +/// . # dangling comment +/// # or this +/// b +/// ) +/// ``` +fn handle_attribute_comment<'a>( + comment: DecoratedComment<'a>, + _locator: &Locator, +) -> CommentPlacement<'a> { + let Some(attribute) = comment.enclosing_node().expr_attribute() else { + return CommentPlacement::Default(comment); + }; + + // It must be a comment AFTER the name + if comment.preceding_node().is_none() { + return CommentPlacement::Default(comment); + } + + if TextRange::new(attribute.value.end(), attribute.attr.start()) + .contains(comment.slice().start()) + { + // ```text + // value . attr + // ^^^^^^^ the range of dangling comments + // ``` + if comment.line_position().is_end_of_line() { + // Attach to node with b + // ```python + // x322 = ( + // a + // . # end-of-line dot comment 2 + // b + // ) + // ``` + CommentPlacement::trailing(comment.enclosing_node(), comment) + } else { + CommentPlacement::dangling(attribute.into(), comment) + } + } else { + CommentPlacement::Default(comment) + } +} + +/// Assign comments between `if` and `test` and `else` and `orelse` as leading to the respective +/// node. +/// +/// ```python +/// x = ( +/// "a" +/// if # leading comment of `True` +/// True +/// else # leading comment of `"b"` +/// "b" +/// ) +/// ``` +/// +/// This placement ensures comments remain in their previous order. This an edge case that only +/// happens if the comments are in a weird position but it also doesn't hurt handling it. +fn handle_expr_if_comment<'a>( + comment: DecoratedComment<'a>, + locator: &Locator, +) -> CommentPlacement<'a> { + let Some(expr_if) = comment.enclosing_node().expr_if_exp() else { + return CommentPlacement::Default(comment); + }; + let ExprIfExp { + range: _, + test, + body, + orelse, + } = expr_if; + + if comment.line_position().is_own_line() { + return CommentPlacement::Default(comment); + } + + // Find the if and the else + let if_token = find_only_token_in_range( + TextRange::new(body.end(), test.start()), + locator, + TokenKind::If, + ); + let else_token = find_only_token_in_range( + TextRange::new(test.end(), orelse.start()), + locator, + TokenKind::Else, + ); + + // Between `if` and `test` + if if_token.range.start() < comment.slice().start() && comment.slice().start() < test.start() { + return CommentPlacement::leading(test.as_ref().into(), comment); + } + + // Between `else` and `orelse` + if else_token.range.start() < comment.slice().start() + && comment.slice().start() < orelse.start() + { + return CommentPlacement::leading(orelse.as_ref().into(), comment); + } + + CommentPlacement::Default(comment) +} + +fn handle_trailing_expression_starred_star_end_of_line_comment<'a>( + comment: DecoratedComment<'a>, + _locator: &Locator, +) -> CommentPlacement<'a> { + if comment.line_position().is_own_line() || comment.following_node().is_none() { + return CommentPlacement::Default(comment); + } + + let AnyNodeRef::ExprStarred(starred) = comment.enclosing_node() else { + return CommentPlacement::Default(comment); + }; + + CommentPlacement::leading(starred.as_any_node_ref(), comment) +} + +/// Looks for a token in the range that contains no other tokens except for parentheses outside +/// the expression ranges +fn find_only_token_in_range(range: TextRange, locator: &Locator, token_kind: TokenKind) -> Token { + let mut tokens = SimpleTokenizer::new(locator.contents(), range) + .skip_trivia() + .skip_while(|token| token.kind == TokenKind::RParen); + let token = tokens.next().expect("Expected a token"); + debug_assert_eq!(token.kind(), token_kind); + let mut tokens = tokens.skip_while(|token| token.kind == TokenKind::LParen); + debug_assert_eq!(tokens.next(), None); + token +} + +// Handle comments inside comprehensions, e.g. +// +// ```python +// [ +// a +// for # dangling on the comprehension +// b +// # dangling on the comprehension +// in # dangling on comprehension.iter +// # leading on the iter +// c +// # dangling on comprehension.if.n +// if # dangling on comprehension.if.n +// d +// ] +// ``` +fn handle_comprehension_comment<'a>( + comment: DecoratedComment<'a>, + locator: &Locator, +) -> CommentPlacement<'a> { + let AnyNodeRef::Comprehension(comprehension) = comment.enclosing_node() else { + return CommentPlacement::Default(comment); + }; + let is_own_line = comment.line_position().is_own_line(); + + // Comments between the `for` and target + // ```python + // [ + // a + // for # attache as dangling on the comprehension + // b in c + // ] + // ``` + if comment.slice().end() < comprehension.target.range().start() { + return if is_own_line { + // own line comments are correctly assigned as leading the target + CommentPlacement::Default(comment) + } else { + // after the `for` + CommentPlacement::dangling(comment.enclosing_node(), comment) + }; + } + + let in_token = find_only_token_in_range( + TextRange::new( + comprehension.target.range().end(), + comprehension.iter.range().start(), + ), + locator, + TokenKind::In, + ); + + // Comments between the target and the `in` + // ```python + // [ + // a for b + // # attach as dangling on the target + // # (to be rendered as leading on the "in") + // in c + // ] + // ``` + if comment.slice().start() < in_token.start() { + // attach as dangling comments on the target + // (to be rendered as leading on the "in") + return if is_own_line { + CommentPlacement::dangling(comment.enclosing_node(), comment) + } else { + // correctly trailing on the target + CommentPlacement::Default(comment) + }; + } + + // Comments between the `in` and the iter + // ```python + // [ + // a for b + // in # attach as dangling on the iter + // c + // ] + // ``` + if comment.slice().start() < comprehension.iter.range().start() { + return if is_own_line { + CommentPlacement::Default(comment) + } else { + // after the `in` but same line, turn into trailing on the `in` token + CommentPlacement::dangling((&comprehension.iter).into(), comment) + }; + } + + let mut last_end = comprehension.iter.range().end(); + + for if_node in &comprehension.ifs { + // ```python + // [ + // a + // for + // c + // in + // e + // # above if <-- find these own-line between previous and `if` token + // if # if <-- find these end-of-line between `if` and if node (`f`) + // # above f <-- already correctly assigned as leading `f` + // f # f <-- already correctly assigned as trailing `f` + // # above if2 + // if # if2 + // # above g + // g # g + // ] + // ``` + let if_token = find_only_token_in_range( + TextRange::new(last_end, if_node.range().start()), + locator, + TokenKind::If, + ); + if is_own_line { + if last_end < comment.slice().start() && comment.slice().start() < if_token.start() { + return CommentPlacement::dangling((if_node).into(), comment); + } + } else { + if if_token.start() < comment.slice().start() + && comment.slice().start() < if_node.range().start() + { + return CommentPlacement::dangling((if_node).into(), comment); + } + } + last_end = if_node.range().end(); + } + + CommentPlacement::Default(comment) +} + /// Returns `true` if `right` is `Some` and `left` and `right` are referentially equal. fn are_same_optional<'a, T>(left: AnyNodeRef, right: Option) -> bool where @@ -879,21 +1385,21 @@ where } fn last_child_in_body(node: AnyNodeRef) -> Option { - use ruff_python_ast::prelude::*; - let body = match node { - AnyNodeRef::StmtFunctionDef(StmtFunctionDef { body, .. }) - | AnyNodeRef::StmtAsyncFunctionDef(StmtAsyncFunctionDef { body, .. }) - | AnyNodeRef::StmtClassDef(StmtClassDef { body, .. }) - | AnyNodeRef::StmtWith(StmtWith { body, .. }) - | AnyNodeRef::StmtAsyncWith(StmtAsyncWith { body, .. }) - | AnyNodeRef::MatchCase(MatchCase { body, .. }) - | AnyNodeRef::ExcepthandlerExceptHandler(ExcepthandlerExceptHandler { body, .. }) => body, + AnyNodeRef::StmtFunctionDef(ast::StmtFunctionDef { body, .. }) + | AnyNodeRef::StmtAsyncFunctionDef(ast::StmtAsyncFunctionDef { body, .. }) + | AnyNodeRef::StmtClassDef(ast::StmtClassDef { body, .. }) + | AnyNodeRef::StmtWith(ast::StmtWith { body, .. }) + | AnyNodeRef::StmtAsyncWith(ast::StmtAsyncWith { body, .. }) + | AnyNodeRef::MatchCase(ast::MatchCase { body, .. }) + | AnyNodeRef::ExceptHandlerExceptHandler(ast::ExceptHandlerExceptHandler { + body, .. + }) => body, - AnyNodeRef::StmtIf(StmtIf { body, orelse, .. }) - | AnyNodeRef::StmtFor(StmtFor { body, orelse, .. }) - | AnyNodeRef::StmtAsyncFor(StmtAsyncFor { body, orelse, .. }) - | AnyNodeRef::StmtWhile(StmtWhile { body, orelse, .. }) => { + AnyNodeRef::StmtIf(ast::StmtIf { body, orelse, .. }) + | AnyNodeRef::StmtFor(ast::StmtFor { body, orelse, .. }) + | AnyNodeRef::StmtAsyncFor(ast::StmtAsyncFor { body, orelse, .. }) + | AnyNodeRef::StmtWhile(ast::StmtWhile { body, orelse, .. }) => { if orelse.is_empty() { body } else { @@ -901,18 +1407,18 @@ fn last_child_in_body(node: AnyNodeRef) -> Option { } } - AnyNodeRef::StmtMatch(StmtMatch { cases, .. }) => { + AnyNodeRef::StmtMatch(ast::StmtMatch { cases, .. }) => { return cases.last().map(AnyNodeRef::from) } - AnyNodeRef::StmtTry(StmtTry { + AnyNodeRef::StmtTry(ast::StmtTry { body, handlers, orelse, finalbody, .. }) - | AnyNodeRef::StmtTryStar(StmtTryStar { + | AnyNodeRef::StmtTryStar(ast::StmtTryStar { body, handlers, orelse, @@ -946,23 +1452,21 @@ fn is_first_statement_in_enclosing_alternate_body( following: AnyNodeRef, enclosing: AnyNodeRef, ) -> bool { - use ruff_python_ast::prelude::*; - match enclosing { - AnyNodeRef::StmtIf(StmtIf { orelse, .. }) - | AnyNodeRef::StmtFor(StmtFor { orelse, .. }) - | AnyNodeRef::StmtAsyncFor(StmtAsyncFor { orelse, .. }) - | AnyNodeRef::StmtWhile(StmtWhile { orelse, .. }) => { + AnyNodeRef::StmtIf(ast::StmtIf { orelse, .. }) + | AnyNodeRef::StmtFor(ast::StmtFor { orelse, .. }) + | AnyNodeRef::StmtAsyncFor(ast::StmtAsyncFor { orelse, .. }) + | AnyNodeRef::StmtWhile(ast::StmtWhile { orelse, .. }) => { are_same_optional(following, orelse.first()) } - AnyNodeRef::StmtTry(StmtTry { + AnyNodeRef::StmtTry(ast::StmtTry { handlers, orelse, finalbody, .. }) - | AnyNodeRef::StmtTryStar(StmtTryStar { + | AnyNodeRef::StmtTryStar(ast::StmtTryStar { handlers, orelse, finalbody, @@ -970,7 +1474,7 @@ fn is_first_statement_in_enclosing_alternate_body( }) => { are_same_optional(following, handlers.first()) // Comments between the handlers and the `else`, or comments between the `handlers` and the `finally` - // are already handled by `handle_in_between_excepthandlers_or_except_handler_and_else_or_finally_comment` + // are already handled by `handle_in_between_except_handlers_or_except_handler_and_else_or_finally_comment` || handlers.is_empty() && are_same_optional(following, orelse.first()) || (handlers.is_empty() || !orelse.is_empty()) && are_same_optional(following, finalbody.first()) diff --git a/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__non_positional_arguments_slash_on_same_line.snap b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__non_positional_arguments_slash_on_same_line.snap index ab41a810a3..6764fffaaa 100644 --- a/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__non_positional_arguments_slash_on_same_line.snap +++ b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__non_positional_arguments_slash_on_same_line.snap @@ -19,9 +19,9 @@ expression: comments.debug(test_case.source_code) "trailing": [], }, Node { - kind: Arg, - range: 90..91, - source: `b`, + kind: ArgWithDefault, + range: 90..94, + source: `b=20`, }: { "leading": [ SourceComment { diff --git a/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__non_positional_arguments_with_defaults.snap b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__non_positional_arguments_with_defaults.snap index a4fde5b30d..6fa4a9c9d0 100644 --- a/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__non_positional_arguments_with_defaults.snap +++ b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__non_positional_arguments_with_defaults.snap @@ -24,9 +24,9 @@ expression: comments.debug(test_case.source_code) "trailing": [], }, Node { - kind: ExprConstant, - range: 17..19, - source: `10`, + kind: ArgWithDefault, + range: 15..19, + source: `a=10`, }: { "leading": [], "dangling": [], @@ -39,9 +39,9 @@ expression: comments.debug(test_case.source_code) ], }, Node { - kind: Arg, - range: 173..174, - source: `b`, + kind: ArgWithDefault, + range: 173..177, + source: `b=20`, }: { "leading": [ SourceComment { diff --git a/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__positional_argument_only_comment.snap b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__positional_argument_only_comment.snap index ff26fa7292..aa1f34eb1f 100644 --- a/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__positional_argument_only_comment.snap +++ b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__positional_argument_only_comment.snap @@ -24,7 +24,7 @@ expression: comments.debug(test_case.source_code) "trailing": [], }, Node { - kind: Arg, + kind: ArgWithDefault, range: 15..16, source: `a`, }: { @@ -39,7 +39,7 @@ expression: comments.debug(test_case.source_code) ], }, Node { - kind: Arg, + kind: ArgWithDefault, range: 166..167, source: `b`, }: { diff --git a/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__positional_argument_only_comment_without_following_node.snap b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__positional_argument_only_comment_without_following_node.snap index 2996b7bee1..2c93c46b00 100644 --- a/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__positional_argument_only_comment_without_following_node.snap +++ b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__positional_argument_only_comment_without_following_node.snap @@ -24,7 +24,7 @@ expression: comments.debug(test_case.source_code) "trailing": [], }, Node { - kind: Arg, + kind: ArgWithDefault, range: 15..16, source: `a`, }: { diff --git a/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__positional_argument_only_leading_comma_comment.snap b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__positional_argument_only_leading_comma_comment.snap index ff26fa7292..aa1f34eb1f 100644 --- a/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__positional_argument_only_leading_comma_comment.snap +++ b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__positional_argument_only_leading_comma_comment.snap @@ -24,7 +24,7 @@ expression: comments.debug(test_case.source_code) "trailing": [], }, Node { - kind: Arg, + kind: ArgWithDefault, range: 15..16, source: `a`, }: { @@ -39,7 +39,7 @@ expression: comments.debug(test_case.source_code) ], }, Node { - kind: Arg, + kind: ArgWithDefault, range: 166..167, source: `b`, }: { diff --git a/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__trailing_after_comma.snap b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__trailing_after_comma.snap index 030b63f38b..5a2c922207 100644 --- a/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__trailing_after_comma.snap +++ b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__trailing_after_comma.snap @@ -4,7 +4,7 @@ expression: comments.debug(test_case.source_code) --- { Node { - kind: Arg, + kind: ArgWithDefault, range: 15..16, source: `a`, }: { diff --git a/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__try_except.snap b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__try_except.snap index 1bcf35e95c..f72858e288 100644 --- a/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__try_except.snap +++ b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__try_except.snap @@ -34,7 +34,7 @@ expression: comments.debug(test_case.source_code) ], }, Node { - kind: ExcepthandlerExceptHandler, + kind: ExceptHandlerExceptHandler, range: 100..136, source: `except Exception as ex:⏎`, }: { diff --git a/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__try_except_finally_else.snap b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__try_except_finally_else.snap index d5b6bbcb7f..004b7a2de2 100644 --- a/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__try_except_finally_else.snap +++ b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__try_except_finally_else.snap @@ -39,7 +39,7 @@ expression: comments.debug(test_case.source_code) ], }, Node { - kind: ExcepthandlerExceptHandler, + kind: ExceptHandlerExceptHandler, range: 68..100, source: `except Exception as ex:⏎`, }: { diff --git a/crates/ruff_python_formatter/src/comments/visitor.rs b/crates/ruff_python_formatter/src/comments/visitor.rs index a6cb1e40cc..947dc4b31b 100644 --- a/crates/ruff_python_formatter/src/comments/visitor.rs +++ b/crates/ruff_python_formatter/src/comments/visitor.rs @@ -1,17 +1,23 @@ -use crate::comments::node_key::NodeRefEqualityKey; -use crate::comments::placement::place_comment; -use crate::comments::{CommentTextPosition, CommentsMap, SourceComment}; +use std::iter::Peekable; + +use ruff_text_size::{TextRange, TextSize}; +use rustpython_parser::ast::{ + Alias, Arg, ArgWithDefault, Arguments, Comprehension, Decorator, ExceptHandler, Expr, Keyword, + MatchCase, Mod, Pattern, Ranged, Stmt, WithItem, +}; + use ruff_formatter::{SourceCode, SourceCodeSlice}; use ruff_python_ast::node::AnyNodeRef; -use ruff_python_ast::prelude::*; use ruff_python_ast::source_code::{CommentRanges, Locator}; // The interface is designed to only export the members relevant for iterating nodes in // pre-order. #[allow(clippy::wildcard_imports)] use ruff_python_ast::visitor::preorder::*; use ruff_python_whitespace::is_python_whitespace; -use ruff_text_size::TextRange; -use std::iter::Peekable; + +use crate::comments::node_key::NodeRefEqualityKey; +use crate::comments::placement::place_comment; +use crate::comments::{CommentLinePosition, CommentsMap, SourceComment}; /// Visitor extracting the comments from an AST. #[derive(Debug, Clone)] @@ -66,7 +72,7 @@ impl<'a> CommentsVisitor<'a> { preceding: self.preceding_node, following: Some(node), parent: self.parents.iter().rev().nth(1).copied(), - text_position: text_position(*comment_range, self.source_code), + line_position: text_position(*comment_range, self.source_code), slice: self.source_code.slice(*comment_range), }; @@ -125,7 +131,7 @@ impl<'a> CommentsVisitor<'a> { preceding: self.preceding_node, parent: self.parents.last().copied(), following: None, - text_position: text_position(*comment_range, self.source_code), + line_position: text_position(*comment_range, self.source_code), slice: self.source_code.slice(*comment_range), }; @@ -208,11 +214,11 @@ impl<'ast> PreorderVisitor<'ast> for CommentsVisitor<'ast> { self.finish_node(comprehension); } - fn visit_excepthandler(&mut self, excepthandler: &'ast Excepthandler) { - if self.start_node(excepthandler).is_traverse() { - walk_excepthandler(self, excepthandler); + fn visit_except_handler(&mut self, except_handler: &'ast ExceptHandler) { + if self.start_node(except_handler).is_traverse() { + walk_except_handler(self, except_handler); } - self.finish_node(excepthandler); + self.finish_node(except_handler); } fn visit_format_spec(&mut self, format_spec: &'ast Expr) { @@ -236,6 +242,13 @@ impl<'ast> PreorderVisitor<'ast> for CommentsVisitor<'ast> { self.finish_node(arg); } + fn visit_arg_with_default(&mut self, arg_with_default: &'ast ArgWithDefault) { + if self.start_node(arg_with_default).is_traverse() { + walk_arg_with_default(self, arg_with_default); + } + self.finish_node(arg_with_default); + } + fn visit_keyword(&mut self, keyword: &'ast Keyword) { if self.start_node(keyword).is_traverse() { walk_keyword(self, keyword); @@ -250,13 +263,14 @@ impl<'ast> PreorderVisitor<'ast> for CommentsVisitor<'ast> { self.finish_node(alias); } - fn visit_withitem(&mut self, withitem: &'ast Withitem) { - if self.start_node(withitem).is_traverse() { - walk_withitem(self, withitem); + fn visit_with_item(&mut self, with_item: &'ast WithItem) { + if self.start_node(with_item).is_traverse() { + walk_with_item(self, with_item); } - self.finish_node(withitem); + self.finish_node(with_item); } + fn visit_match_case(&mut self, match_case: &'ast MatchCase) { if self.start_node(match_case).is_traverse() { walk_match_case(self, match_case); @@ -272,7 +286,7 @@ impl<'ast> PreorderVisitor<'ast> for CommentsVisitor<'ast> { } } -fn text_position(comment_range: TextRange, source_code: SourceCode) -> CommentTextPosition { +fn text_position(comment_range: TextRange, source_code: SourceCode) -> CommentLinePosition { let before = &source_code.as_str()[TextRange::up_to(comment_range.start())]; for c in before.chars().rev() { @@ -281,11 +295,11 @@ fn text_position(comment_range: TextRange, source_code: SourceCode) -> CommentTe break; } c if is_python_whitespace(c) => continue, - _ => return CommentTextPosition::EndOfLine, + _ => return CommentLinePosition::EndOfLine, } } - CommentTextPosition::OwnLine + CommentLinePosition::OwnLine } /// A comment decorated with additional information about its surrounding context in the source document. @@ -297,7 +311,7 @@ pub(super) struct DecoratedComment<'a> { preceding: Option>, following: Option>, parent: Option>, - text_position: CommentTextPosition, + line_position: CommentLinePosition, slice: SourceCodeSlice, } @@ -435,14 +449,21 @@ impl<'a> DecoratedComment<'a> { } /// The position of the comment in the text. - pub(super) fn text_position(&self) -> CommentTextPosition { - self.text_position + pub(super) fn line_position(&self) -> CommentLinePosition { + self.line_position + } +} + +impl Ranged for DecoratedComment<'_> { + #[inline] + fn range(&self) -> TextRange { + self.slice.range() } } impl From> for SourceComment { fn from(decorated: DecoratedComment) -> Self { - Self::new(decorated.slice, decorated.text_position) + Self::new(decorated.slice, decorated.line_position) } } @@ -607,18 +628,6 @@ impl<'a> CommentPlacement<'a> { comment: comment.into(), } } - - /// Returns the placement if it isn't [`CommentPlacement::Default`], otherwise calls `f` and returns the result. - #[inline] - pub(super) fn or_else(self, f: F) -> Self - where - F: FnOnce(DecoratedComment<'a>) -> CommentPlacement<'a>, - { - match self { - CommentPlacement::Default(comment) => f(comment), - placement => placement, - } - } } #[derive(Copy, Clone, Eq, PartialEq, Debug)] @@ -651,8 +660,8 @@ impl<'a> CommentsBuilder<'a> { self.push_dangling_comment(node, comment); } CommentPlacement::Default(comment) => { - match comment.text_position() { - CommentTextPosition::EndOfLine => { + match comment.line_position() { + CommentLinePosition::EndOfLine => { match (comment.preceding_node(), comment.following_node()) { (Some(preceding), Some(_)) => { // Attach comments with both preceding and following node to the preceding @@ -674,7 +683,7 @@ impl<'a> CommentsBuilder<'a> { } } } - CommentTextPosition::OwnLine => { + CommentLinePosition::OwnLine => { match (comment.preceding_node(), comment.following_node()) { // Following always wins for a leading comment // ```python diff --git a/crates/ruff_python_formatter/src/context.rs b/crates/ruff_python_formatter/src/context.rs index 074d5cc08d..d0828745c8 100644 --- a/crates/ruff_python_formatter/src/context.rs +++ b/crates/ruff_python_formatter/src/context.rs @@ -1,22 +1,19 @@ use crate::comments::Comments; -use ruff_formatter::{FormatContext, SimpleFormatOptions, SourceCode}; +use crate::PyFormatOptions; +use ruff_formatter::{FormatContext, GroupId, SourceCode}; use ruff_python_ast::source_code::Locator; use std::fmt::{Debug, Formatter}; #[derive(Clone)] pub struct PyFormatContext<'a> { - options: SimpleFormatOptions, + options: PyFormatOptions, contents: &'a str, comments: Comments<'a>, node_level: NodeLevel, } impl<'a> PyFormatContext<'a> { - pub(crate) fn new( - options: SimpleFormatOptions, - contents: &'a str, - comments: Comments<'a>, - ) -> Self { + pub(crate) fn new(options: PyFormatOptions, contents: &'a str, comments: Comments<'a>) -> Self { Self { options, contents, @@ -25,7 +22,7 @@ impl<'a> PyFormatContext<'a> { } } - pub(crate) fn contents(&self) -> &'a str { + pub(crate) fn source(&self) -> &'a str { self.contents } @@ -48,7 +45,7 @@ impl<'a> PyFormatContext<'a> { } impl FormatContext for PyFormatContext<'_> { - type Options = SimpleFormatOptions; + type Options = PyFormatOptions; fn options(&self) -> &Self::Options { &self.options @@ -81,6 +78,9 @@ pub(crate) enum NodeLevel { /// (`if`, `while`, `match`, etc.). CompoundStatement, - /// Formatting nodes that are enclosed in a parenthesized expression. - Expression, + /// The root or any sub-expression. + Expression(Option), + + /// Formatting nodes that are enclosed by a parenthesized (any `[]`, `{}` or `()`) expression. + ParenthesizedExpression, } diff --git a/crates/ruff_python_formatter/src/expression/expr_attribute.rs b/crates/ruff_python_formatter/src/expression/expr_attribute.rs index 9732025e35..e5a8602caa 100644 --- a/crates/ruff_python_formatter/src/expression/expr_attribute.rs +++ b/crates/ruff_python_formatter/src/expression/expr_attribute.rs @@ -1,12 +1,13 @@ -use crate::comments::Comments; -use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, -}; -use crate::prelude::*; -use crate::{not_yet_implemented_custom_text, FormatNodeRule}; -use ruff_formatter::write; use rustpython_parser::ast::{Constant, Expr, ExprAttribute, ExprConstant}; +use ruff_formatter::write; +use ruff_python_ast::node::AnyNodeRef; + +use crate::comments::{leading_comments, trailing_comments}; +use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses, Parentheses}; +use crate::prelude::*; +use crate::FormatNodeRule; + #[derive(Default)] pub struct FormatExprAttribute; @@ -15,11 +16,11 @@ impl FormatNodeRule for FormatExprAttribute { let ExprAttribute { value, range: _, - attr: _, + attr, ctx: _, } = item; - let requires_space = matches!( + let needs_parentheses = matches!( value.as_ref(), Expr::Constant(ExprConstant { value: Constant::Int(_) | Constant::Float(_), @@ -27,25 +28,67 @@ impl FormatNodeRule for FormatExprAttribute { }) ); + let comments = f.context().comments().clone(); + let dangling_comments = comments.dangling_comments(item); + let leading_attribute_comments_start = + dangling_comments.partition_point(|comment| comment.line_position().is_end_of_line()); + let (trailing_dot_comments, leading_attribute_comments) = + dangling_comments.split_at(leading_attribute_comments_start); + + if needs_parentheses { + value.format().with_options(Parentheses::Always).fmt(f)?; + } else if let Expr::Attribute(expr_attribute) = value.as_ref() { + // We're in a attribute chain (`a.b.c`). The outermost node adds parentheses if + // required, the inner ones don't need them so we skip the `Expr` formatting that + // normally adds the parentheses. + expr_attribute.format().fmt(f)?; + } else { + value.format().fmt(f)?; + } + + if comments.has_trailing_own_line_comments(value.as_ref()) { + hard_line_break().fmt(f)?; + } + write!( f, [ - item.value.format(), - requires_space.then_some(space()), text("."), - not_yet_implemented_custom_text("NOT_IMPLEMENTED_attr") + trailing_comments(trailing_dot_comments), + (!leading_attribute_comments.is_empty()).then_some(hard_line_break()), + leading_comments(leading_attribute_comments), + attr.format() ] ) } + + fn fmt_dangling_comments( + &self, + _node: &ExprAttribute, + _f: &mut PyFormatter, + ) -> FormatResult<()> { + // handle in `fmt_fields` + Ok(()) + } } impl NeedsParentheses for ExprAttribute { fn needs_parentheses( &self, - parenthesize: Parenthesize, - source: &str, - comments: &Comments, - ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, source, comments) + parent: AnyNodeRef, + context: &PyFormatContext, + ) -> OptionalParentheses { + // Checks if there are any own line comments in an attribute chain (a.b.c). + if context + .comments() + .dangling_comments(self) + .iter() + .any(|comment| comment.line_position().is_own_line()) + || context.comments().has_trailing_own_line_comments(self) + { + OptionalParentheses::Always + } else { + self.value.needs_parentheses(parent, context) + } } } diff --git a/crates/ruff_python_formatter/src/expression/expr_await.rs b/crates/ruff_python_formatter/src/expression/expr_await.rs index 19c6bf5196..2f02c352ed 100644 --- a/crates/ruff_python_formatter/src/expression/expr_await.rs +++ b/crates/ruff_python_formatter/src/expression/expr_await.rs @@ -1,10 +1,9 @@ -use crate::comments::Comments; -use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, -}; +use crate::context::PyFormatContext; +use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses}; use crate::{AsFormat, FormatNodeRule, PyFormatter}; use ruff_formatter::prelude::{space, text}; use ruff_formatter::{write, Buffer, FormatResult}; +use ruff_python_ast::node::AnyNodeRef; use rustpython_parser::ast::ExprAwait; #[derive(Default)] @@ -20,10 +19,9 @@ impl FormatNodeRule for FormatExprAwait { impl NeedsParentheses for ExprAwait { fn needs_parentheses( &self, - parenthesize: Parenthesize, - source: &str, - comments: &Comments, - ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, source, comments) + _parent: AnyNodeRef, + _context: &PyFormatContext, + ) -> OptionalParentheses { + OptionalParentheses::Multiline } } diff --git a/crates/ruff_python_formatter/src/expression/expr_bin_op.rs b/crates/ruff_python_formatter/src/expression/expr_bin_op.rs index 0700bc888d..20d708e819 100644 --- a/crates/ruff_python_formatter/src/expression/expr_bin_op.rs +++ b/crates/ruff_python_formatter/src/expression/expr_bin_op.rs @@ -1,17 +1,17 @@ -use crate::comments::{trailing_comments, Comments}; +use crate::comments::{trailing_comments, trailing_node_comments}; use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parenthesize, + in_parentheses_only_group, is_expression_parenthesized, NeedsParentheses, OptionalParentheses, }; use crate::expression::Parentheses; use crate::prelude::*; use crate::FormatNodeRule; -use ruff_formatter::{ - format_args, write, FormatOwnedWithRule, FormatRefWithRule, FormatRuleWithOptions, -}; -use ruff_python_ast::node::AstNode; +use ruff_formatter::{write, FormatOwnedWithRule, FormatRefWithRule, FormatRuleWithOptions}; +use ruff_python_ast::node::{AnyNodeRef, AstNode}; use rustpython_parser::ast::{ - Constant, Expr, ExprAttribute, ExprBinOp, ExprConstant, ExprUnaryOp, Operator, Unaryop, + Constant, Expr, ExprAttribute, ExprBinOp, ExprConstant, ExprUnaryOp, Operator, UnaryOp, }; +use smallvec::SmallVec; +use std::iter; #[derive(Default)] pub struct FormatExprBinOp { @@ -29,76 +29,77 @@ impl FormatRuleWithOptions> for FormatExprBinOp { impl FormatNodeRule for FormatExprBinOp { fn fmt_fields(&self, item: &ExprBinOp, f: &mut PyFormatter) -> FormatResult<()> { - let ExprBinOp { - left, - right, - op, - range: _, - } = item; + let comments = f.context().comments().clone(); - let should_break_right = self.parentheses == Some(Parentheses::Custom); + let format_inner = format_with(|f: &mut PyFormatter| { + let source = f.context().source(); + let binary_chain: SmallVec<[&ExprBinOp; 4]> = iter::successors(Some(item), |parent| { + parent.left.as_bin_op_expr().and_then(|bin_expression| { + if is_expression_parenthesized(bin_expression.as_any_node_ref(), source) { + None + } else { + Some(bin_expression) + } + }) + }) + .collect(); - if should_break_right { - let left_group = f.group_id("BinaryLeft"); + // SAFETY: `binary_chain` is guaranteed not to be empty because it always contains the current expression. + let left_most = binary_chain.last().unwrap(); - write!( - f, - [ - // Wrap the left in a group and gives it an id. The printer first breaks the - // right side if `right` contains any line break because the printer breaks - // sequences of groups from right to left. - // Indents the left side if the group breaks. - group(&format_args![ - if_group_breaks(&text("(")), - indent_if_group_breaks( - &format_args![ - soft_line_break(), - left.format(), - soft_line_break_or_space(), - op.format(), - space() - ], - left_group - ) - ]) - .with_group_id(Some(left_group)), - // Wrap the right in a group and indents its content but only if the left side breaks - group(&indent_if_group_breaks(&right.format(), left_group)), - // If the left side breaks, insert a hard line break to finish the indent and close the open paren. - if_group_breaks(&format_args![hard_line_break(), text(")")]) - .with_group_id(Some(left_group)) - ] - ) - } else { - let comments = f.context().comments().clone(); - let operator_comments = comments.dangling_comments(item.as_any_node_ref()); - let needs_space = !is_simple_power_expression(item); + // Format the left most expression + in_parentheses_only_group(&left_most.left.format()).fmt(f)?; - let before_operator_space = if needs_space { - soft_line_break_or_space() - } else { - soft_line_break() - }; + // Iterate upwards in the binary expression tree and, for each level, format the operator + // and the right expression. + for current in binary_chain.into_iter().rev() { + let ExprBinOp { + range: _, + left: _, + op, + right, + } = current; - write!( - f, - [ - left.format(), - before_operator_space, - op.format(), - trailing_comments(operator_comments), - ] - )?; + let operator_comments = comments.dangling_comments(current); + let needs_space = !is_simple_power_expression(current); - // Format the operator on its own line if the right side has any leading comments. - if comments.has_leading_comments(right.as_ref().into()) { - write!(f, [hard_line_break()])?; - } else if needs_space { - write!(f, [space()])?; + let before_operator_space = if needs_space { + soft_line_break_or_space() + } else { + soft_line_break() + }; + + write!( + f, + [ + before_operator_space, + op.format(), + trailing_comments(operator_comments), + ] + )?; + + // Format the operator on its own line if the right side has any leading comments. + if comments.has_leading_comments(right.as_ref()) || !operator_comments.is_empty() { + hard_line_break().fmt(f)?; + } else if needs_space { + space().fmt(f)?; + } + + in_parentheses_only_group(&right.format()).fmt(f)?; + + // It's necessary to format the trailing comments because the code bypasses + // `FormatNodeRule::fmt` for the nested binary expressions. + // Don't call the formatting function for the most outer binary expression because + // these comments have already been formatted. + if current != item { + trailing_node_comments(current).fmt(f)?; + } } - write!(f, [group(&right.format())]) - } + Ok(()) + }); + + in_parentheses_only_group(&format_inner).fmt(f) } fn fmt_dangling_comments(&self, _node: &ExprBinOp, _f: &mut PyFormatter) -> FormatResult<()> { @@ -116,7 +117,7 @@ const fn is_simple_power_expression(expr: &ExprBinOp) -> bool { const fn is_simple_power_operand(expr: &Expr) -> bool { match expr { Expr::UnaryOp(ExprUnaryOp { - op: Unaryop::Not, .. + op: UnaryOp::Not, .. }) => false, Expr::Constant(ExprConstant { value: Constant::Complex { .. } | Constant::Float(_) | Constant::Int(_), @@ -142,6 +143,7 @@ impl<'ast> AsFormat> for Operator { impl<'ast> IntoFormat> for Operator { type Format = FormatOwnedWithRule>; + fn into_format(self) -> Self::Format { FormatOwnedWithRule::new(self, FormatOperator) } @@ -172,48 +174,9 @@ impl FormatRule> for FormatOperator { impl NeedsParentheses for ExprBinOp { fn needs_parentheses( &self, - parenthesize: Parenthesize, - source: &str, - comments: &Comments, - ) -> Parentheses { - match default_expression_needs_parentheses(self.into(), parenthesize, source, comments) { - Parentheses::Optional => { - if should_binary_break_right_side_first(self) { - Parentheses::Custom - } else { - Parentheses::Optional - } - } - parentheses => parentheses, - } - } -} - -pub(super) fn should_binary_break_right_side_first(expr: &ExprBinOp) -> bool { - use ruff_python_ast::prelude::*; - - if expr.left.is_bin_op_expr() { - false - } else { - match expr.right.as_ref() { - Expr::Tuple(ExprTuple { - elts: expressions, .. - }) - | Expr::List(ExprList { - elts: expressions, .. - }) - | Expr::Set(ExprSet { - elts: expressions, .. - }) - | Expr::Dict(ExprDict { - values: expressions, - .. - }) => !expressions.is_empty(), - Expr::Call(ExprCall { args, keywords, .. }) => !args.is_empty() && !keywords.is_empty(), - Expr::ListComp(_) | Expr::SetComp(_) | Expr::DictComp(_) | Expr::GeneratorExp(_) => { - true - } - _ => false, - } + _parent: AnyNodeRef, + _context: &PyFormatContext, + ) -> OptionalParentheses { + OptionalParentheses::Multiline } } diff --git a/crates/ruff_python_formatter/src/expression/expr_bool_op.rs b/crates/ruff_python_formatter/src/expression/expr_bool_op.rs index e975949029..f18abeb18c 100644 --- a/crates/ruff_python_formatter/src/expression/expr_bool_op.rs +++ b/crates/ruff_python_formatter/src/expression/expr_bool_op.rs @@ -1,32 +1,108 @@ -use crate::comments::Comments; +use crate::comments::leading_comments; use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, + in_parentheses_only_group, NeedsParentheses, OptionalParentheses, Parentheses, }; -use crate::{not_yet_implemented_custom_text, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; -use rustpython_parser::ast::ExprBoolOp; +use crate::prelude::*; +use ruff_formatter::{write, FormatOwnedWithRule, FormatRefWithRule, FormatRuleWithOptions}; +use ruff_python_ast::node::AnyNodeRef; +use rustpython_parser::ast::{BoolOp, ExprBoolOp}; #[derive(Default)] -pub struct FormatExprBoolOp; +pub struct FormatExprBoolOp { + parentheses: Option, +} + +impl FormatRuleWithOptions> for FormatExprBoolOp { + type Options = Option; + fn with_options(mut self, options: Self::Options) -> Self { + self.parentheses = options; + self + } +} impl FormatNodeRule for FormatExprBoolOp { - fn fmt_fields(&self, _item: &ExprBoolOp, f: &mut PyFormatter) -> FormatResult<()> { - write!( - f, - [not_yet_implemented_custom_text( - "NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2" - )] - ) + fn fmt_fields(&self, item: &ExprBoolOp, f: &mut PyFormatter) -> FormatResult<()> { + let ExprBoolOp { + range: _, + op, + values, + } = item; + + let inner = format_with(|f: &mut PyFormatter| { + let mut values = values.iter(); + let comments = f.context().comments().clone(); + + let Some(first) = values.next() else { + return Ok(()); + }; + + write!(f, [in_parentheses_only_group(&first.format())])?; + + for value in values { + let leading_value_comments = comments.leading_comments(value); + // Format the expressions leading comments **before** the operator + if leading_value_comments.is_empty() { + write!(f, [soft_line_break_or_space()])?; + } else { + write!( + f, + [hard_line_break(), leading_comments(leading_value_comments)] + )?; + } + + write!( + f, + [ + op.format(), + space(), + in_parentheses_only_group(&value.format()) + ] + )?; + } + + Ok(()) + }); + + in_parentheses_only_group(&inner).fmt(f) } } impl NeedsParentheses for ExprBoolOp { fn needs_parentheses( &self, - parenthesize: Parenthesize, - source: &str, - comments: &Comments, - ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, source, comments) + _parent: AnyNodeRef, + _context: &PyFormatContext, + ) -> OptionalParentheses { + OptionalParentheses::Multiline + } +} + +#[derive(Copy, Clone)] +pub struct FormatBoolOp; + +impl<'ast> AsFormat> for BoolOp { + type Format<'a> = FormatRefWithRule<'a, BoolOp, FormatBoolOp, PyFormatContext<'ast>>; + + fn format(&self) -> Self::Format<'_> { + FormatRefWithRule::new(self, FormatBoolOp) + } +} + +impl<'ast> IntoFormat> for BoolOp { + type Format = FormatOwnedWithRule>; + + fn into_format(self) -> Self::Format { + FormatOwnedWithRule::new(self, FormatBoolOp) + } +} + +impl FormatRule> for FormatBoolOp { + fn fmt(&self, item: &BoolOp, f: &mut Formatter>) -> FormatResult<()> { + let operator = match item { + BoolOp::And => "and", + BoolOp::Or => "or", + }; + + text(operator).fmt(f) } } diff --git a/crates/ruff_python_formatter/src/expression/expr_call.rs b/crates/ruff_python_formatter/src/expression/expr_call.rs index 3f73b6cbbd..c46aa374f7 100644 --- a/crates/ruff_python_formatter/src/expression/expr_call.rs +++ b/crates/ruff_python_formatter/src/expression/expr_call.rs @@ -1,33 +1,141 @@ -use crate::comments::Comments; +use ruff_text_size::{TextRange, TextSize}; +use rustpython_parser::ast::{Expr, ExprCall, Ranged}; + +use ruff_formatter::write; +use ruff_python_ast::node::AnyNodeRef; + +use crate::comments::dangling_comments; + use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, + parenthesized, NeedsParentheses, OptionalParentheses, Parentheses, }; -use crate::{not_yet_implemented_custom_text, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; -use rustpython_parser::ast::ExprCall; +use crate::prelude::*; +use crate::trivia::{SimpleTokenizer, TokenKind}; +use crate::FormatNodeRule; #[derive(Default)] pub struct FormatExprCall; impl FormatNodeRule for FormatExprCall { - fn fmt_fields(&self, _item: &ExprCall, f: &mut PyFormatter) -> FormatResult<()> { + fn fmt_fields(&self, item: &ExprCall, f: &mut PyFormatter) -> FormatResult<()> { + let ExprCall { + range: _, + func, + args, + keywords, + } = item; + + // We have a case with `f()` without any argument, which is a special case because we can + // have a comment with no node attachment inside: + // ```python + // f( + // # This function has a dangling comment + // ) + // ``` + if args.is_empty() && keywords.is_empty() { + let comments = f.context().comments().clone(); + let comments = comments.dangling_comments(item); + return write!( + f, + [ + func.format(), + text("("), + dangling_comments(comments), + text(")") + ] + ); + } + + let all_args = format_with(|f: &mut PyFormatter| { + let source = f.context().source(); + let mut joiner = f.join_comma_separated(item.end()); + match args.as_slice() { + [argument] if keywords.is_empty() => { + let parentheses = + if is_single_argument_parenthesized(argument, item.end(), source) { + Parentheses::Always + } else { + Parentheses::Never + }; + joiner.entry(argument, &argument.format().with_options(parentheses)); + } + arguments => { + joiner + .entries( + // We have the parentheses from the call so the arguments never need any + arguments + .iter() + .map(|arg| (arg, arg.format().with_options(Parentheses::Preserve))), + ) + .nodes(keywords.iter()); + } + } + + joiner.finish() + }); + write!( f, - [not_yet_implemented_custom_text("NOT_IMPLEMENTED_call()")] + [ + func.format(), + // The outer group is for things like + // ```python + // get_collection( + // hey_this_is_a_very_long_call, + // it_has_funny_attributes_asdf_asdf, + // too_long_for_the_line, + // really=True, + // ) + // ``` + // The inner group is for things like: + // ```python + // get_collection( + // hey_this_is_a_very_long_call, it_has_funny_attributes_asdf_asdf, really=True + // ) + // ``` + // TODO(konstin): Doesn't work see wrongly formatted test + parenthesized("(", &group(&all_args), ")") + ] ) } + + fn fmt_dangling_comments(&self, _node: &ExprCall, _f: &mut PyFormatter) -> FormatResult<()> { + // Handled in `fmt_fields` + Ok(()) + } } impl NeedsParentheses for ExprCall { fn needs_parentheses( &self, - parenthesize: Parenthesize, - source: &str, - comments: &Comments, - ) -> Parentheses { - match default_expression_needs_parentheses(self.into(), parenthesize, source, comments) { - Parentheses::Optional => Parentheses::Never, - parentheses => parentheses, - } + parent: AnyNodeRef, + context: &PyFormatContext, + ) -> OptionalParentheses { + self.func.needs_parentheses(parent, context) } } + +fn is_single_argument_parenthesized(argument: &Expr, call_end: TextSize, source: &str) -> bool { + let mut has_seen_r_paren = false; + + for token in + SimpleTokenizer::new(source, TextRange::new(argument.end(), call_end)).skip_trivia() + { + match token.kind() { + TokenKind::RParen => { + if has_seen_r_paren { + return true; + } + has_seen_r_paren = true; + } + // Skip over any trailing comma + TokenKind::Comma => continue, + _ => { + // Passed the arguments + break; + } + } + } + + false +} diff --git a/crates/ruff_python_formatter/src/expression/expr_compare.rs b/crates/ruff_python_formatter/src/expression/expr_compare.rs index 54c5301e85..770c750162 100644 --- a/crates/ruff_python_formatter/src/expression/expr_compare.rs +++ b/crates/ruff_python_formatter/src/expression/expr_compare.rs @@ -1,33 +1,119 @@ +use crate::comments::leading_comments; use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, + in_parentheses_only_group, NeedsParentheses, OptionalParentheses, Parentheses, }; -use crate::{not_yet_implemented_custom_text, FormatNodeRule, PyFormatter}; - -use crate::comments::Comments; -use ruff_formatter::{write, Buffer, FormatResult}; -use rustpython_parser::ast::ExprCompare; +use crate::prelude::*; +use crate::FormatNodeRule; +use ruff_formatter::{write, FormatOwnedWithRule, FormatRefWithRule, FormatRuleWithOptions}; +use ruff_python_ast::node::AnyNodeRef; +use rustpython_parser::ast::{CmpOp, ExprCompare}; #[derive(Default)] -pub struct FormatExprCompare; +pub struct FormatExprCompare { + parentheses: Option, +} + +impl FormatRuleWithOptions> for FormatExprCompare { + type Options = Option; + + fn with_options(mut self, options: Self::Options) -> Self { + self.parentheses = options; + self + } +} impl FormatNodeRule for FormatExprCompare { - fn fmt_fields(&self, _item: &ExprCompare, f: &mut PyFormatter) -> FormatResult<()> { - write!( - f, - [not_yet_implemented_custom_text( - "NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right" - )] - ) + fn fmt_fields(&self, item: &ExprCompare, f: &mut PyFormatter) -> FormatResult<()> { + let ExprCompare { + range: _, + left, + ops, + comparators, + } = item; + + let comments = f.context().comments().clone(); + + let inner = format_with(|f| { + write!(f, [in_parentheses_only_group(&left.format())])?; + + assert_eq!(comparators.len(), ops.len()); + + for (operator, comparator) in ops.iter().zip(comparators) { + let leading_comparator_comments = comments.leading_comments(comparator); + if leading_comparator_comments.is_empty() { + write!(f, [soft_line_break_or_space()])?; + } else { + // Format the expressions leading comments **before** the operator + write!( + f, + [ + hard_line_break(), + leading_comments(leading_comparator_comments) + ] + )?; + } + + write!( + f, + [ + operator.format(), + space(), + in_parentheses_only_group(&comparator.format()) + ] + )?; + } + + Ok(()) + }); + + in_parentheses_only_group(&inner).fmt(f) } } impl NeedsParentheses for ExprCompare { fn needs_parentheses( &self, - parenthesize: Parenthesize, - source: &str, - comments: &Comments, - ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, source, comments) + _parent: AnyNodeRef, + _context: &PyFormatContext, + ) -> OptionalParentheses { + OptionalParentheses::Multiline + } +} + +#[derive(Copy, Clone)] +pub struct FormatCmpOp; + +impl<'ast> AsFormat> for CmpOp { + type Format<'a> = FormatRefWithRule<'a, CmpOp, FormatCmpOp, PyFormatContext<'ast>>; + + fn format(&self) -> Self::Format<'_> { + FormatRefWithRule::new(self, FormatCmpOp) + } +} + +impl<'ast> IntoFormat> for CmpOp { + type Format = FormatOwnedWithRule>; + + fn into_format(self) -> Self::Format { + FormatOwnedWithRule::new(self, FormatCmpOp) + } +} + +impl FormatRule> for FormatCmpOp { + fn fmt(&self, item: &CmpOp, f: &mut Formatter>) -> FormatResult<()> { + let operator = match item { + CmpOp::Eq => "==", + CmpOp::NotEq => "!=", + CmpOp::Lt => "<", + CmpOp::LtE => "<=", + CmpOp::Gt => ">", + CmpOp::GtE => ">=", + CmpOp::Is => "is", + CmpOp::IsNot => "is not", + CmpOp::In => "in", + CmpOp::NotIn => "not in", + }; + + text(operator).fmt(f) } } diff --git a/crates/ruff_python_formatter/src/expression/expr_constant.rs b/crates/ruff_python_formatter/src/expression/expr_constant.rs index 90ee45c1cf..a1e3ef58e3 100644 --- a/crates/ruff_python_formatter/src/expression/expr_constant.rs +++ b/crates/ruff_python_formatter/src/expression/expr_constant.rs @@ -1,11 +1,14 @@ -use crate::comments::Comments; -use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, -}; +use ruff_text_size::{TextLen, TextRange}; +use rustpython_parser::ast::{Constant, ExprConstant, Ranged}; + +use ruff_formatter::write; +use ruff_python_ast::node::AnyNodeRef; +use ruff_python_ast::str::is_implicit_concatenation; + +use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses}; +use crate::expression::string::{FormatString, StringPrefix, StringQuotes}; use crate::prelude::*; use crate::{not_yet_implemented_custom_text, verbatim_text, FormatNodeRule}; -use ruff_formatter::write; -use rustpython_parser::ast::{Constant, ExprConstant}; #[derive(Default)] pub struct FormatExprConstant; @@ -28,9 +31,7 @@ impl FormatNodeRule for FormatExprConstant { Constant::Int(_) | Constant::Float(_) | Constant::Complex { .. } => { write!(f, [verbatim_text(item)]) } - Constant::Str(_) => { - not_yet_implemented_custom_text(r#""NOT_YET_IMPLEMENTED_STRING""#).fmt(f) - } + Constant::Str(_) => FormatString::new(item).fmt(f), Constant::Bytes(_) => { not_yet_implemented_custom_text(r#"b"NOT_YET_IMPLEMENTED_BYTE_STRING""#).fmt(f) } @@ -39,18 +40,45 @@ impl FormatNodeRule for FormatExprConstant { } } } + + fn fmt_dangling_comments( + &self, + _node: &ExprConstant, + _f: &mut PyFormatter, + ) -> FormatResult<()> { + Ok(()) + } } impl NeedsParentheses for ExprConstant { fn needs_parentheses( &self, - parenthesize: Parenthesize, - source: &str, - comments: &Comments, - ) -> Parentheses { - match default_expression_needs_parentheses(self.into(), parenthesize, source, comments) { - Parentheses::Optional => Parentheses::Never, - parentheses => parentheses, + _parent: AnyNodeRef, + context: &PyFormatContext, + ) -> OptionalParentheses { + if self.value.is_str() { + let contents = context.locator().slice(self.range()); + // Don't wrap triple quoted strings + if is_multiline_string(self, context.source()) || !is_implicit_concatenation(contents) { + OptionalParentheses::Never + } else { + OptionalParentheses::Multiline + } + } else { + OptionalParentheses::Never } } } + +pub(super) fn is_multiline_string(constant: &ExprConstant, source: &str) -> bool { + if constant.value.is_str() { + let contents = &source[constant.range()]; + let prefix = StringPrefix::parse(contents); + let quotes = + StringQuotes::parse(&contents[TextRange::new(prefix.text_len(), contents.text_len())]); + + quotes.map_or(false, StringQuotes::is_triple) && contents.contains(['\n', '\r']) + } else { + false + } +} diff --git a/crates/ruff_python_formatter/src/expression/expr_dict.rs b/crates/ruff_python_formatter/src/expression/expr_dict.rs index 600841f2da..1e8b73e022 100644 --- a/crates/ruff_python_formatter/src/expression/expr_dict.rs +++ b/crates/ruff_python_formatter/src/expression/expr_dict.rs @@ -1,35 +1,105 @@ -use crate::comments::Comments; -use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, -}; -use crate::{not_yet_implemented_custom_text, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; -use rustpython_parser::ast::ExprDict; +use crate::comments::{dangling_node_comments, leading_comments}; +use crate::expression::parentheses::{parenthesized, NeedsParentheses, OptionalParentheses}; +use crate::prelude::*; +use crate::FormatNodeRule; +use ruff_formatter::{format_args, write}; +use ruff_python_ast::node::AnyNodeRef; +use ruff_text_size::TextRange; +use rustpython_parser::ast::Ranged; +use rustpython_parser::ast::{Expr, ExprDict}; #[derive(Default)] pub struct FormatExprDict; +struct KeyValuePair<'a> { + key: &'a Option, + value: &'a Expr, +} + +impl Ranged for KeyValuePair<'_> { + fn range(&self) -> TextRange { + if let Some(key) = self.key { + TextRange::new(key.start(), self.value.end()) + } else { + self.value.range() + } + } +} + +impl Format> for KeyValuePair<'_> { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + if let Some(key) = self.key { + write!( + f, + [group(&format_args![ + key.format(), + text(":"), + space(), + self.value.format() + ])] + ) + } else { + let comments = f.context().comments().clone(); + let leading_value_comments = comments.leading_comments(self.value); + write!( + f, + [ + // make sure the leading comments are hoisted past the `**` + leading_comments(leading_value_comments), + group(&format_args![text("**"), self.value.format()]) + ] + ) + } + } +} + impl FormatNodeRule for FormatExprDict { - fn fmt_fields(&self, _item: &ExprDict, f: &mut PyFormatter) -> FormatResult<()> { - write!( - f, - [not_yet_implemented_custom_text( - "{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value}" - )] - ) + fn fmt_fields(&self, item: &ExprDict, f: &mut PyFormatter) -> FormatResult<()> { + let ExprDict { + range: _, + keys, + values, + } = item; + + debug_assert_eq!(keys.len(), values.len()); + + if values.is_empty() { + return write!( + f, + [ + &text("{"), + block_indent(&dangling_node_comments(item)), + &text("}"), + ] + ); + } + + let format_pairs = format_with(|f| { + let mut joiner = f.join_comma_separated(item.end()); + + for (key, value) in keys.iter().zip(values) { + let key_value_pair = KeyValuePair { key, value }; + joiner.entry(&key_value_pair, &key_value_pair); + } + + joiner.finish() + }); + + parenthesized("{", &format_pairs, "}").fmt(f) + } + + fn fmt_dangling_comments(&self, _node: &ExprDict, _f: &mut PyFormatter) -> FormatResult<()> { + // Handled by `fmt_fields` + Ok(()) } } impl NeedsParentheses for ExprDict { fn needs_parentheses( &self, - parenthesize: Parenthesize, - source: &str, - comments: &Comments, - ) -> Parentheses { - match default_expression_needs_parentheses(self.into(), parenthesize, source, comments) { - Parentheses::Optional => Parentheses::Never, - parentheses => parentheses, - } + _parent: AnyNodeRef, + _context: &PyFormatContext, + ) -> OptionalParentheses { + OptionalParentheses::Never } } diff --git a/crates/ruff_python_formatter/src/expression/expr_dict_comp.rs b/crates/ruff_python_formatter/src/expression/expr_dict_comp.rs index 4121acb76a..322dfea898 100644 --- a/crates/ruff_python_formatter/src/expression/expr_dict_comp.rs +++ b/crates/ruff_python_formatter/src/expression/expr_dict_comp.rs @@ -1,9 +1,8 @@ -use crate::comments::Comments; -use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, -}; +use crate::context::PyFormatContext; +use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses}; use crate::{not_yet_implemented_custom_text, FormatNodeRule, PyFormatter}; use ruff_formatter::{write, Buffer, FormatResult}; +use ruff_python_ast::node::AnyNodeRef; use rustpython_parser::ast::ExprDictComp; #[derive(Default)] @@ -23,13 +22,9 @@ impl FormatNodeRule for FormatExprDictComp { impl NeedsParentheses for ExprDictComp { fn needs_parentheses( &self, - parenthesize: Parenthesize, - source: &str, - comments: &Comments, - ) -> Parentheses { - match default_expression_needs_parentheses(self.into(), parenthesize, source, comments) { - Parentheses::Optional => Parentheses::Never, - parentheses => parentheses, - } + _parent: AnyNodeRef, + _context: &PyFormatContext, + ) -> OptionalParentheses { + OptionalParentheses::Never } } diff --git a/crates/ruff_python_formatter/src/expression/expr_formatted_value.rs b/crates/ruff_python_formatter/src/expression/expr_formatted_value.rs index ef2551de32..d2f224efd5 100644 --- a/crates/ruff_python_formatter/src/expression/expr_formatted_value.rs +++ b/crates/ruff_python_formatter/src/expression/expr_formatted_value.rs @@ -1,9 +1,8 @@ -use crate::comments::Comments; -use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, -}; +use crate::context::PyFormatContext; +use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses}; use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; use ruff_formatter::{write, Buffer, FormatResult}; +use ruff_python_ast::node::AnyNodeRef; use rustpython_parser::ast::ExprFormattedValue; #[derive(Default)] @@ -18,10 +17,9 @@ impl FormatNodeRule for FormatExprFormattedValue { impl NeedsParentheses for ExprFormattedValue { fn needs_parentheses( &self, - parenthesize: Parenthesize, - source: &str, - comments: &Comments, - ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, source, comments) + _parent: AnyNodeRef, + _context: &PyFormatContext, + ) -> OptionalParentheses { + OptionalParentheses::Multiline } } diff --git a/crates/ruff_python_formatter/src/expression/expr_generator_exp.rs b/crates/ruff_python_formatter/src/expression/expr_generator_exp.rs index 54762794e3..b9e8b2ed65 100644 --- a/crates/ruff_python_formatter/src/expression/expr_generator_exp.rs +++ b/crates/ruff_python_formatter/src/expression/expr_generator_exp.rs @@ -1,9 +1,8 @@ -use crate::comments::Comments; -use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, -}; +use crate::context::PyFormatContext; +use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses}; use crate::{not_yet_implemented_custom_text, FormatNodeRule, PyFormatter}; use ruff_formatter::{write, Buffer, FormatResult}; +use ruff_python_ast::node::AnyNodeRef; use rustpython_parser::ast::ExprGeneratorExp; #[derive(Default)] @@ -11,20 +10,21 @@ pub struct FormatExprGeneratorExp; impl FormatNodeRule for FormatExprGeneratorExp { fn fmt_fields(&self, _item: &ExprGeneratorExp, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented_custom_text("(i for i in [])")]) + write!( + f, + [not_yet_implemented_custom_text( + "(NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in [])" + )] + ) } } impl NeedsParentheses for ExprGeneratorExp { fn needs_parentheses( &self, - parenthesize: Parenthesize, - source: &str, - comments: &Comments, - ) -> Parentheses { - match default_expression_needs_parentheses(self.into(), parenthesize, source, comments) { - Parentheses::Optional => Parentheses::Never, - parentheses => parentheses, - } + _parent: AnyNodeRef, + _context: &PyFormatContext, + ) -> OptionalParentheses { + OptionalParentheses::Never } } diff --git a/crates/ruff_python_formatter/src/expression/expr_if_exp.rs b/crates/ruff_python_formatter/src/expression/expr_if_exp.rs index 980571d853..5eb09d3b50 100644 --- a/crates/ruff_python_formatter/src/expression/expr_if_exp.rs +++ b/crates/ruff_python_formatter/src/expression/expr_if_exp.rs @@ -1,21 +1,44 @@ -use crate::comments::Comments; +use crate::comments::leading_comments; use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, + in_parentheses_only_group, NeedsParentheses, OptionalParentheses, }; -use crate::{not_yet_implemented_custom_text, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; +use crate::prelude::*; +use crate::FormatNodeRule; +use ruff_formatter::{format_args, write}; +use ruff_python_ast::node::AnyNodeRef; use rustpython_parser::ast::ExprIfExp; #[derive(Default)] pub struct FormatExprIfExp; impl FormatNodeRule for FormatExprIfExp { - fn fmt_fields(&self, _item: &ExprIfExp, f: &mut PyFormatter) -> FormatResult<()> { + fn fmt_fields(&self, item: &ExprIfExp, f: &mut PyFormatter) -> FormatResult<()> { + let ExprIfExp { + range: _, + test, + body, + orelse, + } = item; + let comments = f.context().comments().clone(); + + // We place `if test` and `else orelse` on a single line, so the `test` and `orelse` leading + // comments go on the line before the `if` or `else` instead of directly ahead `test` or + // `orelse` write!( f, - [not_yet_implemented_custom_text( - "NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false" - )] + [in_parentheses_only_group(&format_args![ + body.format(), + soft_line_break_or_space(), + leading_comments(comments.leading_comments(test.as_ref())), + text("if"), + space(), + test.format(), + soft_line_break_or_space(), + leading_comments(comments.leading_comments(orelse.as_ref())), + text("else"), + space(), + orelse.format() + ])] ) } } @@ -23,10 +46,9 @@ impl FormatNodeRule for FormatExprIfExp { impl NeedsParentheses for ExprIfExp { fn needs_parentheses( &self, - parenthesize: Parenthesize, - source: &str, - comments: &Comments, - ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, source, comments) + _parent: AnyNodeRef, + _context: &PyFormatContext, + ) -> OptionalParentheses { + OptionalParentheses::Multiline } } diff --git a/crates/ruff_python_formatter/src/expression/expr_joined_str.rs b/crates/ruff_python_formatter/src/expression/expr_joined_str.rs index a13ff5f389..cf54ebffcb 100644 --- a/crates/ruff_python_formatter/src/expression/expr_joined_str.rs +++ b/crates/ruff_python_formatter/src/expression/expr_joined_str.rs @@ -1,27 +1,30 @@ -use crate::comments::Comments; -use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, -}; -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; +use crate::context::PyFormatContext; +use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses}; +use crate::{not_yet_implemented_custom_text, FormatNodeRule, PyFormatter}; use ruff_formatter::{write, Buffer, FormatResult}; +use ruff_python_ast::node::AnyNodeRef; use rustpython_parser::ast::ExprJoinedStr; #[derive(Default)] pub struct FormatExprJoinedStr; impl FormatNodeRule for FormatExprJoinedStr { - fn fmt_fields(&self, item: &ExprJoinedStr, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + fn fmt_fields(&self, _item: &ExprJoinedStr, f: &mut PyFormatter) -> FormatResult<()> { + write!( + f, + [not_yet_implemented_custom_text( + r#"f"NOT_YET_IMPLEMENTED_ExprJoinedStr""# + )] + ) } } impl NeedsParentheses for ExprJoinedStr { fn needs_parentheses( &self, - parenthesize: Parenthesize, - source: &str, - comments: &Comments, - ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, source, comments) + _parent: AnyNodeRef, + _context: &PyFormatContext, + ) -> OptionalParentheses { + OptionalParentheses::Multiline } } diff --git a/crates/ruff_python_formatter/src/expression/expr_lambda.rs b/crates/ruff_python_formatter/src/expression/expr_lambda.rs index bd63bfa0f6..e631e80061 100644 --- a/crates/ruff_python_formatter/src/expression/expr_lambda.rs +++ b/crates/ruff_python_formatter/src/expression/expr_lambda.rs @@ -1,9 +1,8 @@ -use crate::comments::Comments; -use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, -}; +use crate::context::PyFormatContext; +use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses}; use crate::{not_yet_implemented_custom_text, FormatNodeRule, PyFormatter}; use ruff_formatter::{write, Buffer, FormatResult}; +use ruff_python_ast::node::AnyNodeRef; use rustpython_parser::ast::ExprLambda; #[derive(Default)] @@ -11,17 +10,21 @@ pub struct FormatExprLambda; impl FormatNodeRule for FormatExprLambda { fn fmt_fields(&self, _item: &ExprLambda, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented_custom_text("lambda x: True")]) + write!( + f, + [not_yet_implemented_custom_text( + "lambda NOT_YET_IMPLEMENTED_lambda: True" + )] + ) } } impl NeedsParentheses for ExprLambda { fn needs_parentheses( &self, - parenthesize: Parenthesize, - source: &str, - comments: &Comments, - ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, source, comments) + _parent: AnyNodeRef, + _context: &PyFormatContext, + ) -> OptionalParentheses { + OptionalParentheses::Multiline } } diff --git a/crates/ruff_python_formatter/src/expression/expr_list.rs b/crates/ruff_python_formatter/src/expression/expr_list.rs index 20153d8aa7..b28085fc00 100644 --- a/crates/ruff_python_formatter/src/expression/expr_list.rs +++ b/crates/ruff_python_formatter/src/expression/expr_list.rs @@ -1,11 +1,10 @@ -use crate::comments::{dangling_comments, Comments}; -use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, -}; +use crate::comments::{dangling_comments, CommentLinePosition}; +use crate::expression::parentheses::{parenthesized, NeedsParentheses, OptionalParentheses}; use crate::prelude::*; use crate::FormatNodeRule; use ruff_formatter::{format_args, write}; -use rustpython_parser::ast::ExprList; +use ruff_python_ast::node::AnyNodeRef; +use rustpython_parser::ast::{ExprList, Ranged}; #[derive(Default)] pub struct FormatExprList; @@ -18,36 +17,47 @@ impl FormatNodeRule for FormatExprList { ctx: _, } = item; + let comments = f.context().comments().clone(); + let dangling = comments.dangling_comments(item); + + // The empty list is special because there can be dangling comments, and they can be in two + // positions: + // ```python + // a3 = [ # end-of-line + // # own line + // ] + // ``` + // In all other cases comments get assigned to a list element + if elts.is_empty() { + let end_of_line_split = dangling.partition_point(|comment| { + comment.line_position() == CommentLinePosition::EndOfLine + }); + debug_assert!(dangling[end_of_line_split..] + .iter() + .all(|comment| comment.line_position() == CommentLinePosition::OwnLine)); + return write!( + f, + [group(&format_args![ + text("["), + dangling_comments(&dangling[..end_of_line_split]), + soft_block_indent(&dangling_comments(&dangling[end_of_line_split..])), + text("]") + ])] + ); + } + + debug_assert!( + dangling.is_empty(), + "A non-empty expression list has dangling comments" + ); + let items = format_with(|f| { - let mut iter = elts.iter(); - - if let Some(first) = iter.next() { - write!(f, [first.format()])?; - } - - for item in iter { - write!(f, [text(","), soft_line_break_or_space(), item.format()])?; - } - - if !elts.is_empty() { - write!(f, [if_group_breaks(&text(","))])?; - } - - Ok(()) + f.join_comma_separated(item.end()) + .nodes(elts.iter()) + .finish() }); - let comments = f.context().comments().clone(); - let dangling = comments.dangling_comments(item.into()); - - write!( - f, - [group(&format_args![ - text("["), - dangling_comments(dangling), - soft_block_indent(&items), - text("]") - ])] - ) + parenthesized("[", &items, "]").fmt(f) } fn fmt_dangling_comments(&self, _node: &ExprList, _f: &mut PyFormatter) -> FormatResult<()> { @@ -59,13 +69,9 @@ impl FormatNodeRule for FormatExprList { impl NeedsParentheses for ExprList { fn needs_parentheses( &self, - parenthesize: Parenthesize, - source: &str, - comments: &Comments, - ) -> Parentheses { - match default_expression_needs_parentheses(self.into(), parenthesize, source, comments) { - Parentheses::Optional => Parentheses::Never, - parentheses => parentheses, - } + _parent: AnyNodeRef, + _context: &PyFormatContext, + ) -> OptionalParentheses { + OptionalParentheses::Never } } diff --git a/crates/ruff_python_formatter/src/expression/expr_list_comp.rs b/crates/ruff_python_formatter/src/expression/expr_list_comp.rs index 3ab6a61f06..764ff399b1 100644 --- a/crates/ruff_python_formatter/src/expression/expr_list_comp.rs +++ b/crates/ruff_python_formatter/src/expression/expr_list_comp.rs @@ -1,30 +1,50 @@ -use crate::comments::Comments; -use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, -}; -use crate::{not_yet_implemented_custom_text, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; +use crate::context::PyFormatContext; +use crate::expression::parentheses::{parenthesized, NeedsParentheses, OptionalParentheses}; +use crate::prelude::*; +use crate::AsFormat; +use crate::{FormatNodeRule, PyFormatter}; +use ruff_formatter::{format_args, write, Buffer, FormatResult}; +use ruff_python_ast::node::AnyNodeRef; use rustpython_parser::ast::ExprListComp; #[derive(Default)] pub struct FormatExprListComp; impl FormatNodeRule for FormatExprListComp { - fn fmt_fields(&self, _item: &ExprListComp, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented_custom_text("[i for i in []]")]) + fn fmt_fields(&self, item: &ExprListComp, f: &mut PyFormatter) -> FormatResult<()> { + let ExprListComp { + range: _, + elt, + generators, + } = item; + + let joined = format_with(|f| { + f.join_with(soft_line_break_or_space()) + .entries(generators.iter().formatted()) + .finish() + }); + + write!( + f, + [parenthesized( + "[", + &format_args!( + group(&elt.format()), + soft_line_break_or_space(), + group(&joined) + ), + "]" + )] + ) } } impl NeedsParentheses for ExprListComp { fn needs_parentheses( &self, - parenthesize: Parenthesize, - source: &str, - comments: &Comments, - ) -> Parentheses { - match default_expression_needs_parentheses(self.into(), parenthesize, source, comments) { - Parentheses::Optional => Parentheses::Never, - parentheses => parentheses, - } + _parent: AnyNodeRef, + _context: &PyFormatContext, + ) -> OptionalParentheses { + OptionalParentheses::Never } } diff --git a/crates/ruff_python_formatter/src/expression/expr_name.rs b/crates/ruff_python_formatter/src/expression/expr_name.rs index 2567349481..b3963adb42 100644 --- a/crates/ruff_python_formatter/src/expression/expr_name.rs +++ b/crates/ruff_python_formatter/src/expression/expr_name.rs @@ -1,10 +1,8 @@ -use crate::comments::Comments; -use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, -}; +use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses}; use crate::prelude::*; use crate::FormatNodeRule; use ruff_formatter::{write, FormatContext}; +use ruff_python_ast::node::AnyNodeRef; use rustpython_parser::ast::ExprName; #[derive(Default)] @@ -29,11 +27,10 @@ impl FormatNodeRule for FormatExprName { impl NeedsParentheses for ExprName { fn needs_parentheses( &self, - parenthesize: Parenthesize, - source: &str, - comments: &Comments, - ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, source, comments) + _parent: AnyNodeRef, + _context: &PyFormatContext, + ) -> OptionalParentheses { + OptionalParentheses::Never } } diff --git a/crates/ruff_python_formatter/src/expression/expr_named_expr.rs b/crates/ruff_python_formatter/src/expression/expr_named_expr.rs index 71d2bae891..4a009b8746 100644 --- a/crates/ruff_python_formatter/src/expression/expr_named_expr.rs +++ b/crates/ruff_python_formatter/src/expression/expr_named_expr.rs @@ -1,9 +1,9 @@ -use crate::comments::Comments; -use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, -}; -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; +use crate::context::PyFormatContext; +use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses}; +use crate::{AsFormat, FormatNodeRule, PyFormatter}; +use ruff_formatter::prelude::{space, text}; use ruff_formatter::{write, Buffer, FormatResult}; +use ruff_python_ast::node::AnyNodeRef; use rustpython_parser::ast::ExprNamedExpr; #[derive(Default)] @@ -11,17 +11,32 @@ pub struct FormatExprNamedExpr; impl FormatNodeRule for FormatExprNamedExpr { fn fmt_fields(&self, item: &ExprNamedExpr, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + let ExprNamedExpr { + target, + value, + range: _, + } = item; + write!( + f, + [ + target.format(), + space(), + text(":="), + space(), + value.format(), + ] + ) } } impl NeedsParentheses for ExprNamedExpr { fn needs_parentheses( &self, - parenthesize: Parenthesize, - source: &str, - comments: &Comments, - ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, source, comments) + _parent: AnyNodeRef, + _context: &PyFormatContext, + ) -> OptionalParentheses { + // Unlike tuples, named expression parentheses are not part of the range even when + // mandatory. See [PEP 572](https://peps.python.org/pep-0572/) for details. + OptionalParentheses::Always } } diff --git a/crates/ruff_python_formatter/src/expression/expr_set.rs b/crates/ruff_python_formatter/src/expression/expr_set.rs index 7e379d1cea..83ff228a83 100644 --- a/crates/ruff_python_formatter/src/expression/expr_set.rs +++ b/crates/ruff_python_formatter/src/expression/expr_set.rs @@ -1,12 +1,8 @@ -use crate::comments::Comments; -use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, -}; -use crate::{FormatNodeRule, FormattedIterExt, PyFormatter}; -use ruff_formatter::prelude::{ - format_with, group, if_group_breaks, soft_block_indent, soft_line_break_or_space, text, -}; -use ruff_formatter::{format_args, write, Buffer, FormatResult}; +use crate::expression::parentheses::{parenthesized, NeedsParentheses, OptionalParentheses}; +use crate::prelude::*; +use crate::FormatNodeRule; +use ruff_formatter::format_args; +use ruff_python_ast::node::AnyNodeRef; use rustpython_parser::ast::ExprSet; #[derive(Default)] @@ -23,27 +19,17 @@ impl FormatNodeRule for FormatExprSet { .entries(elts.iter().formatted()) .finish() }); - write!( - f, - [group(&format_args![ - text("{"), - soft_block_indent(&format_args![joined, if_group_breaks(&text(",")),]), - text("}") - ])] - ) + + parenthesized("{", &format_args![joined, if_group_breaks(&text(","))], "}").fmt(f) } } impl NeedsParentheses for ExprSet { fn needs_parentheses( &self, - parenthesize: Parenthesize, - source: &str, - comments: &Comments, - ) -> Parentheses { - match default_expression_needs_parentheses(self.into(), parenthesize, source, comments) { - Parentheses::Optional => Parentheses::Never, - parentheses => parentheses, - } + _parent: AnyNodeRef, + _context: &PyFormatContext, + ) -> OptionalParentheses { + OptionalParentheses::Never } } diff --git a/crates/ruff_python_formatter/src/expression/expr_set_comp.rs b/crates/ruff_python_formatter/src/expression/expr_set_comp.rs index a73ede1c68..9588dee66f 100644 --- a/crates/ruff_python_formatter/src/expression/expr_set_comp.rs +++ b/crates/ruff_python_formatter/src/expression/expr_set_comp.rs @@ -1,30 +1,30 @@ -use crate::comments::Comments; -use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, -}; -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; +use crate::context::PyFormatContext; +use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses}; +use crate::{not_yet_implemented_custom_text, FormatNodeRule, PyFormatter}; use ruff_formatter::{write, Buffer, FormatResult}; +use ruff_python_ast::node::AnyNodeRef; use rustpython_parser::ast::ExprSetComp; #[derive(Default)] pub struct FormatExprSetComp; impl FormatNodeRule for FormatExprSetComp { - fn fmt_fields(&self, item: &ExprSetComp, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + fn fmt_fields(&self, _item: &ExprSetComp, f: &mut PyFormatter) -> FormatResult<()> { + write!( + f, + [not_yet_implemented_custom_text( + "{NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set}" + )] + ) } } impl NeedsParentheses for ExprSetComp { fn needs_parentheses( &self, - parenthesize: Parenthesize, - source: &str, - comments: &Comments, - ) -> Parentheses { - match default_expression_needs_parentheses(self.into(), parenthesize, source, comments) { - Parentheses::Optional => Parentheses::Never, - parentheses => parentheses, - } + _parent: AnyNodeRef, + _context: &PyFormatContext, + ) -> OptionalParentheses { + OptionalParentheses::Never } } diff --git a/crates/ruff_python_formatter/src/expression/expr_slice.rs b/crates/ruff_python_formatter/src/expression/expr_slice.rs index 529ef4679b..0d9dd7445f 100644 --- a/crates/ruff_python_formatter/src/expression/expr_slice.rs +++ b/crates/ruff_python_formatter/src/expression/expr_slice.rs @@ -1,33 +1,268 @@ -use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, -}; -use crate::{not_yet_implemented_custom_text, FormatNodeRule, PyFormatter}; - -use crate::comments::Comments; -use ruff_formatter::{write, Buffer, FormatResult}; +use crate::comments::{dangling_comments, SourceComment}; +use crate::context::PyFormatContext; +use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses}; +use crate::trivia::Token; +use crate::trivia::{first_non_trivia_token, TokenKind}; +use crate::{AsFormat, FormatNodeRule, PyFormatter}; +use ruff_formatter::prelude::{hard_line_break, line_suffix_boundary, space, text}; +use ruff_formatter::{write, Buffer, Format, FormatError, FormatResult}; +use ruff_python_ast::node::{AnyNodeRef, AstNode}; +use ruff_text_size::TextRange; use rustpython_parser::ast::ExprSlice; +use rustpython_parser::ast::{Expr, Ranged}; #[derive(Default)] pub struct FormatExprSlice; impl FormatNodeRule for FormatExprSlice { - fn fmt_fields(&self, _item: &ExprSlice, f: &mut PyFormatter) -> FormatResult<()> { - write!( - f, - [not_yet_implemented_custom_text( - "NOT_IMPLEMENTED_start:NOT_IMPLEMENTED_end" - )] - ) + /// This implementation deviates from black in that comments are attached to the section of the + /// slice they originate in + fn fmt_fields(&self, item: &ExprSlice, f: &mut PyFormatter) -> FormatResult<()> { + // `[lower:upper:step]` + let ExprSlice { + range, + lower, + upper, + step, + } = item; + + let (first_colon, second_colon) = find_colons(f.context().source(), *range, lower, upper)?; + + // Handle comment placement + // In placements.rs, we marked comment for None nodes a dangling and associated all others + // as leading or dangling wrt to a node. That means we either format a node and only have + // to handle newlines and spacing, or the node is None and we insert the corresponding + // slice of dangling comments + let comments = f.context().comments().clone(); + let slice_dangling_comments = comments.dangling_comments(item.as_any_node_ref()); + // Put the dangling comments (where the nodes are missing) into buckets + let first_colon_partition_index = slice_dangling_comments + .partition_point(|x| x.slice().start() < first_colon.range.start()); + let (dangling_lower_comments, dangling_upper_step_comments) = + slice_dangling_comments.split_at(first_colon_partition_index); + let (dangling_upper_comments, dangling_step_comments) = + if let Some(second_colon) = &second_colon { + let second_colon_partition_index = dangling_upper_step_comments + .partition_point(|x| x.slice().start() < second_colon.range.start()); + dangling_upper_step_comments.split_at(second_colon_partition_index) + } else { + // Without a second colon they remaining dangling comments belong between the first + // colon and the closing parentheses + (dangling_upper_step_comments, [].as_slice()) + }; + + // Ensure there a no dangling comments for a node if the node is present + debug_assert!(lower.is_none() || dangling_lower_comments.is_empty()); + debug_assert!(upper.is_none() || dangling_upper_comments.is_empty()); + debug_assert!(step.is_none() || dangling_step_comments.is_empty()); + + // Handle spacing around the colon(s) + // https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#slices + let lower_simple = lower.as_ref().map_or(true, |expr| is_simple_expr(expr)); + let upper_simple = upper.as_ref().map_or(true, |expr| is_simple_expr(expr)); + let step_simple = step.as_ref().map_or(true, |expr| is_simple_expr(expr)); + let all_simple = lower_simple && upper_simple && step_simple; + + // lower + if let Some(lower) = lower { + write!(f, [lower.format(), line_suffix_boundary()])?; + } else { + dangling_comments(dangling_lower_comments).fmt(f)?; + } + + // First colon + // The spacing after the colon depends on both the lhs and the rhs: + // ``` + // e00 = x[:] + // e01 = x[:1] + // e02 = x[: a()] + // e10 = x[1:] + // e11 = x[1:1] + // e12 = x[1 : a()] + // e20 = x[a() :] + // e21 = x[a() : 1] + // e22 = x[a() : a()] + // e200 = "e"[a() : :] + // e201 = "e"[a() :: 1] + // e202 = "e"[a() :: a()] + // ``` + if !all_simple { + space().fmt(f)?; + } + text(":").fmt(f)?; + // No upper node, no need for a space, e.g. `x[a() :]` + if !all_simple && upper.is_some() { + space().fmt(f)?; + } + + // Upper + if let Some(upper) = upper { + let upper_leading_comments = comments.leading_comments(upper.as_ref()); + leading_comments_spacing(f, upper_leading_comments)?; + write!(f, [upper.format(), line_suffix_boundary()])?; + } else { + if let Some(first) = dangling_upper_comments.first() { + // Here the spacing for end-of-line comments works but own line comments need + // explicit spacing + if first.line_position().is_own_line() { + hard_line_break().fmt(f)?; + } + } + dangling_comments(dangling_upper_comments).fmt(f)?; + } + + // (optionally) step + if second_colon.is_some() { + // Same spacing rules as for the first colon, except for the strange case when the + // second colon exists, but neither upper nor step + // ``` + // e200 = "e"[a() : :] + // e201 = "e"[a() :: 1] + // e202 = "e"[a() :: a()] + // ``` + if !all_simple && (upper.is_some() || step.is_none()) { + space().fmt(f)?; + } + text(":").fmt(f)?; + // No step node, no need for a space + if !all_simple && step.is_some() { + space().fmt(f)?; + } + if let Some(step) = step { + let step_leading_comments = comments.leading_comments(step.as_ref()); + leading_comments_spacing(f, step_leading_comments)?; + step.format().fmt(f)?; + } else { + if !dangling_step_comments.is_empty() { + // Put the colon and comments on their own lines + write!( + f, + [hard_line_break(), dangling_comments(dangling_step_comments)] + )?; + } + } + } else { + debug_assert!(step.is_none(), "step can't exist without a second colon"); + } + Ok(()) } } +/// We're in a slice, so we know there's a first colon, but with have to look into the source +/// to find out whether there is a second one, too, e.g. `[1:2]` and `[1:10:2]`. +/// +/// Returns the first and optionally the second colon. +pub(crate) fn find_colons( + contents: &str, + range: TextRange, + lower: &Option>, + upper: &Option>, +) -> FormatResult<(Token, Option)> { + let after_lower = lower + .as_ref() + .map_or(range.start(), |lower| lower.range().end()); + let first_colon = + first_non_trivia_token(after_lower, contents).ok_or(FormatError::SyntaxError)?; + if first_colon.kind != TokenKind::Colon { + return Err(FormatError::SyntaxError); + } + + let after_upper = upper + .as_ref() + .map_or(first_colon.end(), |upper| upper.range().end()); + // At least the closing bracket must exist, so there must be a token there + let next_token = + first_non_trivia_token(after_upper, contents).ok_or(FormatError::SyntaxError)?; + let second_colon = if next_token.kind == TokenKind::Colon { + debug_assert!( + next_token.range.start() < range.end(), + "The next token in a slice must either be a colon or the closing bracket" + ); + Some(next_token) + } else { + None + }; + Ok((first_colon, second_colon)) +} + +/// Determines whether this expression needs a space around the colon +/// +fn is_simple_expr(expr: &Expr) -> bool { + matches!(expr, Expr::Constant(_) | Expr::Name(_)) +} + +pub(crate) enum ExprSliceCommentSection { + Lower, + Upper, + Step, +} + +/// Assigns a comment to lower/upper/step in `[lower:upper:step]`. +/// +/// ```python +/// "sliceable"[ +/// # lower comment +/// : +/// # upper comment +/// : +/// # step comment +/// ] +/// ``` +pub(crate) fn assign_comment_in_slice( + comment: TextRange, + contents: &str, + expr_slice: &ExprSlice, +) -> ExprSliceCommentSection { + let ExprSlice { + range, + lower, + upper, + step: _, + } = expr_slice; + + let (first_colon, second_colon) = find_colons(contents, *range, lower, upper) + .expect("SyntaxError when trying to parse slice"); + + if comment.start() < first_colon.range.start() { + ExprSliceCommentSection::Lower + } else { + // We are to the right of the first colon + if let Some(second_colon) = second_colon { + if comment.start() < second_colon.range.start() { + ExprSliceCommentSection::Upper + } else { + ExprSliceCommentSection::Step + } + } else { + // No second colon means there is no step + ExprSliceCommentSection::Upper + } + } +} + +/// Manual spacing for the leading comments of upper and step +fn leading_comments_spacing( + f: &mut PyFormatter, + leading_comments: &[SourceComment], +) -> FormatResult<()> { + if let Some(first) = leading_comments.first() { + if first.line_position().is_own_line() { + // Insert a newline after the colon so the comment ends up on its own line + hard_line_break().fmt(f)?; + } else { + // Insert the two spaces between the colon and the end-of-line comment after the colon + write!(f, [space(), space()])?; + } + } + Ok(()) +} + impl NeedsParentheses for ExprSlice { fn needs_parentheses( &self, - parenthesize: Parenthesize, - source: &str, - comments: &Comments, - ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, source, comments) + _parent: AnyNodeRef, + _context: &PyFormatContext, + ) -> OptionalParentheses { + OptionalParentheses::Multiline } } diff --git a/crates/ruff_python_formatter/src/expression/expr_starred.rs b/crates/ruff_python_formatter/src/expression/expr_starred.rs index 14806cb52b..6027ef8430 100644 --- a/crates/ruff_python_formatter/src/expression/expr_starred.rs +++ b/crates/ruff_python_formatter/src/expression/expr_starred.rs @@ -1,27 +1,40 @@ -use crate::comments::Comments; -use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, -}; -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; use rustpython_parser::ast::ExprStarred; +use ruff_formatter::write; +use ruff_python_ast::node::AnyNodeRef; + +use crate::context::PyFormatContext; +use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses}; +use crate::prelude::*; +use crate::FormatNodeRule; + #[derive(Default)] pub struct FormatExprStarred; impl FormatNodeRule for FormatExprStarred { fn fmt_fields(&self, item: &ExprStarred, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + let ExprStarred { + range: _, + value, + ctx: _, + } = item; + + write!(f, [text("*"), value.format()]) + } + + fn fmt_dangling_comments(&self, node: &ExprStarred, f: &mut PyFormatter) -> FormatResult<()> { + debug_assert_eq!(f.context().comments().dangling_comments(node), []); + + Ok(()) } } impl NeedsParentheses for ExprStarred { fn needs_parentheses( &self, - parenthesize: Parenthesize, - source: &str, - comments: &Comments, - ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, source, comments) + _parent: AnyNodeRef, + _context: &PyFormatContext, + ) -> OptionalParentheses { + OptionalParentheses::Multiline } } diff --git a/crates/ruff_python_formatter/src/expression/expr_subscript.rs b/crates/ruff_python_formatter/src/expression/expr_subscript.rs index 6874b1415a..a03cc7ec89 100644 --- a/crates/ruff_python_formatter/src/expression/expr_subscript.rs +++ b/crates/ruff_python_formatter/src/expression/expr_subscript.rs @@ -1,33 +1,96 @@ -use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, -}; -use crate::{not_yet_implemented_custom_text, FormatNodeRule, PyFormatter}; +use rustpython_parser::ast::{Expr, ExprSubscript}; -use crate::comments::Comments; -use ruff_formatter::{write, Buffer, FormatResult}; -use rustpython_parser::ast::ExprSubscript; +use ruff_formatter::{format_args, write}; +use ruff_python_ast::node::{AnyNodeRef, AstNode}; + +use crate::comments::trailing_comments; +use crate::context::NodeLevel; +use crate::context::PyFormatContext; +use crate::expression::expr_tuple::TupleParentheses; +use crate::expression::parentheses::{ + in_parentheses_only_group, NeedsParentheses, OptionalParentheses, +}; +use crate::prelude::*; +use crate::FormatNodeRule; #[derive(Default)] pub struct FormatExprSubscript; impl FormatNodeRule for FormatExprSubscript { - fn fmt_fields(&self, _item: &ExprSubscript, f: &mut PyFormatter) -> FormatResult<()> { + fn fmt_fields(&self, item: &ExprSubscript, f: &mut PyFormatter) -> FormatResult<()> { + let ExprSubscript { + range: _, + value, + slice, + ctx: _, + } = item; + + let comments = f.context().comments().clone(); + let dangling_comments = comments.dangling_comments(item.as_any_node_ref()); + debug_assert!( + dangling_comments.len() <= 1, + "The subscript expression must have at most a single comment, the one after the bracket" + ); + + if let NodeLevel::Expression(Some(group_id)) = f.context().node_level() { + // Enforce the optional parentheses for parenthesized values. + f.context_mut().set_node_level(NodeLevel::Expression(None)); + let result = value.format().fmt(f); + f.context_mut() + .set_node_level(NodeLevel::Expression(Some(group_id))); + result?; + } else { + value.format().fmt(f)?; + } + + let format_slice = format_with(|f: &mut PyFormatter| { + let saved_level = f.context().node_level(); + f.context_mut() + .set_node_level(NodeLevel::ParenthesizedExpression); + + let result = if let Expr::Tuple(tuple) = slice.as_ref() { + tuple + .format() + .with_options(TupleParentheses::Subscript) + .fmt(f) + } else { + slice.format().fmt(f) + }; + + f.context_mut().set_node_level(saved_level); + + result + }); + write!( f, - [not_yet_implemented_custom_text( - "NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key]" - )] + [in_parentheses_only_group(&format_args![ + text("["), + trailing_comments(dangling_comments), + soft_block_indent(&format_slice), + text("]") + ])] ) } + + fn fmt_dangling_comments( + &self, + _node: &ExprSubscript, + _f: &mut PyFormatter, + ) -> FormatResult<()> { + // Handled inside of `fmt_fields` + Ok(()) + } } impl NeedsParentheses for ExprSubscript { fn needs_parentheses( &self, - parenthesize: Parenthesize, - source: &str, - comments: &Comments, - ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, source, comments) + _parent: AnyNodeRef, + _context: &PyFormatContext, + ) -> OptionalParentheses { + { + OptionalParentheses::Never + } } } diff --git a/crates/ruff_python_formatter/src/expression/expr_tuple.rs b/crates/ruff_python_formatter/src/expression/expr_tuple.rs index ba96180a15..e0e5689a9c 100644 --- a/crates/ruff_python_formatter/src/expression/expr_tuple.rs +++ b/crates/ruff_python_formatter/src/expression/expr_tuple.rs @@ -1,30 +1,178 @@ -use crate::comments::Comments; -use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, -}; -use crate::{not_yet_implemented_custom_text, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; +use ruff_text_size::TextRange; use rustpython_parser::ast::ExprTuple; +use rustpython_parser::ast::{Expr, Ranged}; + +use ruff_formatter::{format_args, write, FormatRuleWithOptions}; +use ruff_python_ast::node::AnyNodeRef; + +use crate::builders::parenthesize_if_expands; +use crate::comments::{dangling_comments, CommentLinePosition}; +use crate::expression::parentheses::{ + parenthesized, NeedsParentheses, OptionalParentheses, Parentheses, +}; +use crate::prelude::*; + +#[derive(Eq, PartialEq, Debug, Default)] +pub enum TupleParentheses { + /// Effectively `None` in `Option` + #[default] + Default, + /// Effectively `Some(Parentheses)` in `Option` + Expr(Parentheses), + + /// Black omits parentheses for tuples inside of subscripts except if the tuple is parenthesized + /// in the source code. + Subscript, + + /// Handle the special case where we remove parentheses even if they were initially present + /// + /// Normally, black keeps parentheses, but in the case of loops it formats + /// ```python + /// for (a, b) in x: + /// pass + /// ``` + /// to + /// ```python + /// for a, b in x: + /// pass + /// ``` + /// Black still does use parentheses in this position if the group breaks or magic trailing + /// comma is used. + StripInsideForLoop, +} #[derive(Default)] -pub struct FormatExprTuple; +pub struct FormatExprTuple { + parentheses: TupleParentheses, +} + +impl FormatRuleWithOptions> for FormatExprTuple { + type Options = TupleParentheses; + + fn with_options(mut self, options: Self::Options) -> Self { + self.parentheses = options; + self + } +} impl FormatNodeRule for FormatExprTuple { - fn fmt_fields(&self, _item: &ExprTuple, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented_custom_text("(1, 2)")]) + fn fmt_fields(&self, item: &ExprTuple, f: &mut PyFormatter) -> FormatResult<()> { + let ExprTuple { + range, + elts, + ctx: _, + } = item; + + // Handle the edge cases of an empty tuple and a tuple with one element + // + // there can be dangling comments, and they can be in two + // positions: + // ```python + // a3 = ( # end-of-line + // # own line + // ) + // ``` + // In all other cases comments get assigned to a list element + match elts.as_slice() { + [] => { + let comments = f.context().comments().clone(); + let dangling = comments.dangling_comments(item); + let end_of_line_split = dangling.partition_point(|comment| { + comment.line_position() == CommentLinePosition::EndOfLine + }); + debug_assert!(dangling[end_of_line_split..] + .iter() + .all(|comment| comment.line_position() == CommentLinePosition::OwnLine)); + write!( + f, + [group(&format_args![ + text("("), + dangling_comments(&dangling[..end_of_line_split]), + soft_block_indent(&dangling_comments(&dangling[end_of_line_split..])), + text(")") + ])] + ) + } + [single] => match self.parentheses { + TupleParentheses::Subscript + if !is_parenthesized(*range, elts, f.context().source()) => + { + write!(f, [single.format(), text(",")]) + } + _ => + // A single element tuple always needs parentheses and a trailing comma, except when inside of a subscript + { + parenthesized("(", &format_args![single.format(), text(",")], ")").fmt(f) + } + }, + // If the tuple has parentheses, we generally want to keep them. The exception are for + // loops, see `TupleParentheses::StripInsideForLoop` doc comment. + // + // Unlike other expression parentheses, tuple parentheses are part of the range of the + // tuple itself. + _ if is_parenthesized(*range, elts, f.context().source()) + && self.parentheses != TupleParentheses::StripInsideForLoop => + { + parenthesized("(", &ExprSequence::new(item), ")").fmt(f) + } + _ => match self.parentheses { + TupleParentheses::Subscript => group(&ExprSequence::new(item)).fmt(f), + _ => parenthesize_if_expands(&ExprSequence::new(item)).fmt(f), + }, + } + } + + fn fmt_dangling_comments(&self, _node: &ExprTuple, _f: &mut PyFormatter) -> FormatResult<()> { + // Handled in `fmt_fields` + Ok(()) + } +} + +#[derive(Debug)] +struct ExprSequence<'a> { + tuple: &'a ExprTuple, +} + +impl<'a> ExprSequence<'a> { + const fn new(expr: &'a ExprTuple) -> Self { + Self { tuple: expr } + } +} + +impl Format> for ExprSequence<'_> { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + f.join_comma_separated(self.tuple.end()) + .nodes(&self.tuple.elts) + .finish() } } impl NeedsParentheses for ExprTuple { fn needs_parentheses( &self, - parenthesize: Parenthesize, - source: &str, - comments: &Comments, - ) -> Parentheses { - match default_expression_needs_parentheses(self.into(), parenthesize, source, comments) { - Parentheses::Optional => Parentheses::Never, - parentheses => parentheses, - } + _parent: AnyNodeRef, + _context: &PyFormatContext, + ) -> OptionalParentheses { + OptionalParentheses::Never } } + +/// Check if a tuple has already had parentheses in the input +fn is_parenthesized(tuple_range: TextRange, elts: &[Expr], source: &str) -> bool { + let parentheses = '('; + let first_char = &source[usize::from(tuple_range.start())..].chars().next(); + let Some(first_char) = first_char else { + return false; + }; + if *first_char != parentheses { + return false; + } + + // Consider `a = (1, 2), 3`: The first char of the current expr starts is a parentheses, but + // it's not its own but that of its first tuple child. We know that it belongs to the child + // because if it wouldn't, the child would start (at least) a char later + let Some(first_child) = elts.first() else { + return false; + }; + first_child.range().start() != tuple_range.start() +} diff --git a/crates/ruff_python_formatter/src/expression/expr_unary_op.rs b/crates/ruff_python_formatter/src/expression/expr_unary_op.rs index 9f91d13e40..97462c4d7f 100644 --- a/crates/ruff_python_formatter/src/expression/expr_unary_op.rs +++ b/crates/ruff_python_formatter/src/expression/expr_unary_op.rs @@ -1,27 +1,103 @@ -use crate::comments::Comments; -use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, -}; -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; -use rustpython_parser::ast::ExprUnaryOp; +use crate::comments::trailing_comments; +use crate::context::PyFormatContext; +use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses}; +use crate::trivia::{SimpleTokenizer, TokenKind}; +use crate::{AsFormat, FormatNodeRule, PyFormatter}; +use ruff_formatter::prelude::{hard_line_break, space, text}; +use ruff_formatter::{Format, FormatContext, FormatResult}; +use ruff_python_ast::node::AnyNodeRef; +use ruff_text_size::{TextLen, TextRange}; +use rustpython_parser::ast::UnaryOp; +use rustpython_parser::ast::{ExprUnaryOp, Ranged}; #[derive(Default)] pub struct FormatExprUnaryOp; impl FormatNodeRule for FormatExprUnaryOp { fn fmt_fields(&self, item: &ExprUnaryOp, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + let ExprUnaryOp { + range: _, + op, + operand, + } = item; + + let operator = match op { + UnaryOp::Invert => "~", + UnaryOp::Not => "not", + UnaryOp::UAdd => "+", + UnaryOp::USub => "-", + }; + + text(operator).fmt(f)?; + + let comments = f.context().comments().clone(); + + // Split off the comments that follow after the operator and format them as trailing comments. + // ```python + // (not # comment + // a) + // ``` + let leading_operand_comments = comments.leading_comments(operand.as_ref()); + let trailing_operator_comments_end = + leading_operand_comments.partition_point(|p| p.line_position().is_end_of_line()); + let (trailing_operator_comments, leading_operand_comments) = + leading_operand_comments.split_at(trailing_operator_comments_end); + + if !trailing_operator_comments.is_empty() { + trailing_comments(trailing_operator_comments).fmt(f)?; + } + + // Insert a line break if the operand has comments but itself is not parenthesized. + // ```python + // if ( + // not + // # comment + // a) + // ``` + if !leading_operand_comments.is_empty() + && !is_operand_parenthesized(item, f.context().source_code().as_str()) + { + hard_line_break().fmt(f)?; + } else if op.is_not() { + space().fmt(f)?; + } + + operand.format().fmt(f) } } impl NeedsParentheses for ExprUnaryOp { fn needs_parentheses( &self, - parenthesize: Parenthesize, - source: &str, - comments: &Comments, - ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, source, comments) + _parent: AnyNodeRef, + context: &PyFormatContext, + ) -> OptionalParentheses { + // We preserve the parentheses of the operand. It should not be necessary to break this expression. + if is_operand_parenthesized(self, context.source()) { + OptionalParentheses::Never + } else { + OptionalParentheses::Multiline + } + } +} + +fn is_operand_parenthesized(unary: &ExprUnaryOp, source: &str) -> bool { + let operator_len = match unary.op { + UnaryOp::Invert => '~'.text_len(), + UnaryOp::Not => "not".text_len(), + UnaryOp::UAdd => '+'.text_len(), + UnaryOp::USub => '-'.text_len(), + }; + + let trivia_range = TextRange::new(unary.range.start() + operator_len, unary.operand.start()); + + if let Some(token) = SimpleTokenizer::new(source, trivia_range) + .skip_trivia() + .next() + { + debug_assert_eq!(token.kind(), TokenKind::LParen); + true + } else { + false } } diff --git a/crates/ruff_python_formatter/src/expression/expr_yield.rs b/crates/ruff_python_formatter/src/expression/expr_yield.rs index f2ece7ecac..20fabe7d05 100644 --- a/crates/ruff_python_formatter/src/expression/expr_yield.rs +++ b/crates/ruff_python_formatter/src/expression/expr_yield.rs @@ -1,9 +1,8 @@ -use crate::comments::Comments; -use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, -}; +use crate::context::PyFormatContext; +use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses}; use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; use ruff_formatter::{write, Buffer, FormatResult}; +use ruff_python_ast::node::AnyNodeRef; use rustpython_parser::ast::ExprYield; #[derive(Default)] @@ -18,10 +17,9 @@ impl FormatNodeRule for FormatExprYield { impl NeedsParentheses for ExprYield { fn needs_parentheses( &self, - parenthesize: Parenthesize, - source: &str, - comments: &Comments, - ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, source, comments) + _parent: AnyNodeRef, + _context: &PyFormatContext, + ) -> OptionalParentheses { + OptionalParentheses::Multiline } } diff --git a/crates/ruff_python_formatter/src/expression/expr_yield_from.rs b/crates/ruff_python_formatter/src/expression/expr_yield_from.rs index bfaf101162..3f2f6b6842 100644 --- a/crates/ruff_python_formatter/src/expression/expr_yield_from.rs +++ b/crates/ruff_python_formatter/src/expression/expr_yield_from.rs @@ -1,9 +1,8 @@ -use crate::comments::Comments; -use crate::expression::parentheses::{ - default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize, -}; +use crate::context::PyFormatContext; +use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses}; use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; use ruff_formatter::{write, Buffer, FormatResult}; +use ruff_python_ast::node::AnyNodeRef; use rustpython_parser::ast::ExprYieldFrom; #[derive(Default)] @@ -18,10 +17,9 @@ impl FormatNodeRule for FormatExprYieldFrom { impl NeedsParentheses for ExprYieldFrom { fn needs_parentheses( &self, - parenthesize: Parenthesize, - source: &str, - comments: &Comments, - ) -> Parentheses { - default_expression_needs_parentheses(self.into(), parenthesize, source, comments) + _parent: AnyNodeRef, + _context: &PyFormatContext, + ) -> OptionalParentheses { + OptionalParentheses::Multiline } } diff --git a/crates/ruff_python_formatter/src/expression/mod.rs b/crates/ruff_python_formatter/src/expression/mod.rs index 706c59f29f..121610b81c 100644 --- a/crates/ruff_python_formatter/src/expression/mod.rs +++ b/crates/ruff_python_formatter/src/expression/mod.rs @@ -1,11 +1,20 @@ -use crate::comments::Comments; +use std::cmp::Ordering; + +use rustpython_parser::ast; +use rustpython_parser::ast::{Expr, Operator}; + +use ruff_formatter::{FormatOwnedWithRule, FormatRefWithRule, FormatRule, FormatRuleWithOptions}; +use ruff_python_ast::node::AnyNodeRef; +use ruff_python_ast::visitor::preorder::{walk_expr, PreorderVisitor}; + +use crate::builders::parenthesize_if_expands; use crate::context::NodeLevel; -use crate::expression::parentheses::{NeedsParentheses, Parentheses, Parenthesize}; -use crate::prelude::*; -use ruff_formatter::{ - format_args, write, FormatOwnedWithRule, FormatRefWithRule, FormatRule, FormatRuleWithOptions, +use crate::expression::expr_tuple::TupleParentheses; +use crate::expression::parentheses::{ + is_expression_parenthesized, optional_parentheses, parenthesized, NeedsParentheses, + OptionalParentheses, Parentheses, Parenthesize, }; -use rustpython_parser::ast::Expr; +use crate::prelude::*; pub(crate) mod expr_attribute; pub(crate) mod expr_await; @@ -35,31 +44,28 @@ pub(crate) mod expr_unary_op; pub(crate) mod expr_yield; pub(crate) mod expr_yield_from; pub(crate) mod parentheses; +pub(crate) mod string; -#[derive(Default)] +#[derive(Copy, Clone, PartialEq, Eq, Default)] pub struct FormatExpr { - parenthesize: Parenthesize, + parentheses: Parentheses, } impl FormatRuleWithOptions> for FormatExpr { - type Options = Parenthesize; + type Options = Parentheses; fn with_options(mut self, options: Self::Options) -> Self { - self.parenthesize = options; + self.parentheses = options; self } } impl FormatRule> for FormatExpr { - fn fmt(&self, item: &Expr, f: &mut PyFormatter) -> FormatResult<()> { - let parentheses = item.needs_parentheses( - self.parenthesize, - f.context().contents(), - f.context().comments(), - ); + fn fmt(&self, expression: &Expr, f: &mut PyFormatter) -> FormatResult<()> { + let parentheses = self.parentheses; - let format_expr = format_with(|f| match item { - Expr::BoolOp(expr) => expr.format().fmt(f), + let format_expr = format_with(|f| match expression { + Expr::BoolOp(expr) => expr.format().with_options(Some(parentheses)).fmt(f), Expr::NamedExpr(expr) => expr.format().fmt(f), Expr::BinOp(expr) => expr.format().with_options(Some(parentheses)).fmt(f), Expr::UnaryOp(expr) => expr.format().fmt(f), @@ -74,7 +80,7 @@ impl FormatRule> for FormatExpr { Expr::Await(expr) => expr.format().fmt(f), Expr::Yield(expr) => expr.format().fmt(f), Expr::YieldFrom(expr) => expr.format().fmt(f), - Expr::Compare(expr) => expr.format().fmt(f), + Expr::Compare(expr) => expr.format().with_options(Some(parentheses)).fmt(f), Expr::Call(expr) => expr.format().fmt(f), Expr::FormattedValue(expr) => expr.format().fmt(f), Expr::JoinedStr(expr) => expr.format().fmt(f), @@ -84,85 +90,150 @@ impl FormatRule> for FormatExpr { Expr::Starred(expr) => expr.format().fmt(f), Expr::Name(expr) => expr.format().fmt(f), Expr::List(expr) => expr.format().fmt(f), - Expr::Tuple(expr) => expr.format().fmt(f), + Expr::Tuple(expr) => expr + .format() + .with_options(TupleParentheses::Expr(parentheses)) + .fmt(f), Expr::Slice(expr) => expr.format().fmt(f), }); - let saved_level = f.context().node_level(); - f.context_mut().set_node_level(NodeLevel::Expression); - - let result = match parentheses { - Parentheses::Always => { - write!( - f, - [group(&format_args![ - text("("), - soft_block_indent(&format_expr), - text(")") - ])] - ) + let parenthesize = match parentheses { + Parentheses::Preserve => { + is_expression_parenthesized(AnyNodeRef::from(expression), f.context().source()) } - // Add optional parentheses. Ignore if the item renders parentheses itself. - Parentheses::Optional => { - write!( - f, - [group(&format_args![ - if_group_breaks(&text("(")), - soft_block_indent(&format_expr), - if_group_breaks(&text(")")) - ])] - ) - } - Parentheses::Custom | Parentheses::Never => Format::fmt(&format_expr, f), + Parentheses::Always => true, + Parentheses::Never => false, }; - f.context_mut().set_node_level(saved_level); + if parenthesize { + parenthesized("(", &format_expr, ")").fmt(f) + } else { + let saved_level = match f.context().node_level() { + saved_level @ (NodeLevel::TopLevel | NodeLevel::CompoundStatement) => { + f.context_mut().set_node_level(NodeLevel::Expression(None)); + Some(saved_level) + } + NodeLevel::Expression(_) | NodeLevel::ParenthesizedExpression => None, + }; - result + let result = Format::fmt(&format_expr, f); + + if let Some(saved_level) = saved_level { + f.context_mut().set_node_level(saved_level); + } + + result + } + } +} + +/// Wraps an expression in an optional parentheses except if its [`NeedsParentheses::needs_parentheses`] implementation +/// indicates that it is okay to omit the parentheses. For example, parentheses can always be omitted for lists, +/// because they already bring their own parentheses. +pub(crate) fn maybe_parenthesize_expression<'a, T>( + expression: &'a Expr, + parent: T, + parenthesize: Parenthesize, +) -> MaybeParenthesizeExpression<'a> +where + T: Into>, +{ + MaybeParenthesizeExpression { + expression, + parent: parent.into(), + parenthesize, + } +} + +pub(crate) struct MaybeParenthesizeExpression<'a> { + expression: &'a Expr, + parent: AnyNodeRef<'a>, + parenthesize: Parenthesize, +} + +impl Format> for MaybeParenthesizeExpression<'_> { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + let MaybeParenthesizeExpression { + expression, + parent, + parenthesize, + } = self; + + let parenthesize = match parenthesize { + Parenthesize::Optional => { + is_expression_parenthesized(AnyNodeRef::from(*expression), f.context().source()) + } + Parenthesize::IfBreaks => false, + }; + + let parentheses = + if parenthesize || f.context().comments().has_leading_comments(*expression) { + OptionalParentheses::Always + } else { + expression.needs_parentheses(*parent, f.context()) + }; + + match parentheses { + OptionalParentheses::Multiline => { + if can_omit_optional_parentheses(expression, f.context()) { + optional_parentheses(&expression.format().with_options(Parentheses::Never)) + .fmt(f) + } else { + parenthesize_if_expands(&expression.format().with_options(Parentheses::Never)) + .fmt(f) + } + } + OptionalParentheses::Always => { + expression.format().with_options(Parentheses::Always).fmt(f) + } + OptionalParentheses::Never => { + expression.format().with_options(Parentheses::Never).fmt(f) + } + } } } impl NeedsParentheses for Expr { fn needs_parentheses( &self, - parenthesize: Parenthesize, - source: &str, - comments: &Comments, - ) -> Parentheses { + parent: AnyNodeRef, + context: &PyFormatContext, + ) -> OptionalParentheses { match self { - Expr::BoolOp(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::NamedExpr(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::BinOp(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::UnaryOp(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::Lambda(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::IfExp(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::Dict(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::Set(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::ListComp(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::SetComp(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::DictComp(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::GeneratorExp(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::Await(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::Yield(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::YieldFrom(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::Compare(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::Call(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::FormattedValue(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::JoinedStr(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::Constant(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::Attribute(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::Subscript(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::Starred(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::Name(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::List(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::Tuple(expr) => expr.needs_parentheses(parenthesize, source, comments), - Expr::Slice(expr) => expr.needs_parentheses(parenthesize, source, comments), + Expr::BoolOp(expr) => expr.needs_parentheses(parent, context), + Expr::NamedExpr(expr) => expr.needs_parentheses(parent, context), + Expr::BinOp(expr) => expr.needs_parentheses(parent, context), + Expr::UnaryOp(expr) => expr.needs_parentheses(parent, context), + Expr::Lambda(expr) => expr.needs_parentheses(parent, context), + Expr::IfExp(expr) => expr.needs_parentheses(parent, context), + Expr::Dict(expr) => expr.needs_parentheses(parent, context), + Expr::Set(expr) => expr.needs_parentheses(parent, context), + Expr::ListComp(expr) => expr.needs_parentheses(parent, context), + Expr::SetComp(expr) => expr.needs_parentheses(parent, context), + Expr::DictComp(expr) => expr.needs_parentheses(parent, context), + Expr::GeneratorExp(expr) => expr.needs_parentheses(parent, context), + Expr::Await(expr) => expr.needs_parentheses(parent, context), + Expr::Yield(expr) => expr.needs_parentheses(parent, context), + Expr::YieldFrom(expr) => expr.needs_parentheses(parent, context), + Expr::Compare(expr) => expr.needs_parentheses(parent, context), + Expr::Call(expr) => expr.needs_parentheses(parent, context), + Expr::FormattedValue(expr) => expr.needs_parentheses(parent, context), + Expr::JoinedStr(expr) => expr.needs_parentheses(parent, context), + Expr::Constant(expr) => expr.needs_parentheses(parent, context), + Expr::Attribute(expr) => expr.needs_parentheses(parent, context), + Expr::Subscript(expr) => expr.needs_parentheses(parent, context), + Expr::Starred(expr) => expr.needs_parentheses(parent, context), + Expr::Name(expr) => expr.needs_parentheses(parent, context), + Expr::List(expr) => expr.needs_parentheses(parent, context), + Expr::Tuple(expr) => expr.needs_parentheses(parent, context), + Expr::Slice(expr) => expr.needs_parentheses(parent, context), } } } impl<'ast> AsFormat> for Expr { type Format<'a> = FormatRefWithRule<'a, Expr, FormatExpr, PyFormatContext<'ast>>; + fn format(&self) -> Self::Format<'_> { FormatRefWithRule::new(self, FormatExpr::default()) } @@ -170,7 +241,257 @@ impl<'ast> AsFormat> for Expr { impl<'ast> IntoFormat> for Expr { type Format = FormatOwnedWithRule>; + fn into_format(self) -> Self::Format { FormatOwnedWithRule::new(self, FormatExpr::default()) } } + +/// Tests if it is safe to omit the optional parentheses. +/// +/// We prefer parentheses at least in the following cases: +/// * The expression contains more than one unparenthesized expression with the same priority. For example, +/// the expression `a * b * c` contains two multiply operations. We prefer parentheses in that case. +/// `(a * b) * c` or `a * b + c` are okay, because the subexpression is parenthesized, or the expression uses operands with a lower priority +/// * The expression contains at least one parenthesized sub expression (optimization to avoid unnecessary work) +/// +/// This mimics Black's [`_maybe_split_omitting_optional_parens`](https://github.com/psf/black/blob/d1248ca9beaf0ba526d265f4108836d89cf551b7/src/black/linegen.py#L746-L820) +fn can_omit_optional_parentheses(expr: &Expr, context: &PyFormatContext) -> bool { + let mut visitor = CanOmitOptionalParenthesesVisitor::new(context.source()); + visitor.visit_subexpression(expr); + visitor.can_omit() +} + +#[derive(Clone, Debug)] +struct CanOmitOptionalParenthesesVisitor<'input> { + max_priority: OperatorPriority, + max_priority_count: u32, + any_parenthesized_expressions: bool, + last: Option<&'input Expr>, + first: Option<&'input Expr>, + source: &'input str, +} + +impl<'input> CanOmitOptionalParenthesesVisitor<'input> { + fn new(source: &'input str) -> Self { + Self { + source, + max_priority: OperatorPriority::None, + max_priority_count: 0, + any_parenthesized_expressions: false, + last: None, + first: None, + } + } + + fn update_max_priority(&mut self, current_priority: OperatorPriority) { + self.update_max_priority_with_count(current_priority, 1); + } + + fn update_max_priority_with_count(&mut self, current_priority: OperatorPriority, count: u32) { + match self.max_priority.cmp(¤t_priority) { + Ordering::Less => { + self.max_priority_count = count; + self.max_priority = current_priority; + } + Ordering::Equal => { + self.max_priority_count += count; + } + Ordering::Greater => {} + } + } + + // Visits a subexpression, ignoring whether it is parenthesized or not + fn visit_subexpression(&mut self, expr: &'input Expr) { + match expr { + Expr::Dict(_) | Expr::List(_) | Expr::Tuple(_) | Expr::Set(_) => { + self.any_parenthesized_expressions = true; + // The values are always parenthesized, don't visit. + return; + } + Expr::ListComp(_) | Expr::SetComp(_) | Expr::DictComp(_) => { + self.any_parenthesized_expressions = true; + self.update_max_priority(OperatorPriority::Comprehension); + return; + } + // It's impossible for a file smaller or equal to 4GB to contain more than 2^32 comparisons + // because each comparison requires a left operand, and `n` `operands` and right sides. + #[allow(clippy::cast_possible_truncation)] + Expr::BoolOp(ast::ExprBoolOp { + range: _, + op: _, + values, + }) => self.update_max_priority_with_count( + OperatorPriority::BooleanOperation, + values.len().saturating_sub(1) as u32, + ), + Expr::BinOp(ast::ExprBinOp { + op, + left: _, + right: _, + range: _, + }) => self.update_max_priority(OperatorPriority::from(*op)), + + Expr::IfExp(_) => { + // + 1 for the if and one for the else + self.update_max_priority_with_count(OperatorPriority::Conditional, 2); + } + + // It's impossible for a file smaller or equal to 4GB to contain more than 2^32 comparisons + // because each comparison requires a left operand, and `n` `operands` and right sides. + #[allow(clippy::cast_possible_truncation)] + Expr::Compare(ast::ExprCompare { + range: _, + left: _, + ops, + comparators: _, + }) => { + self.update_max_priority_with_count(OperatorPriority::Comparator, ops.len() as u32); + } + Expr::Call(ast::ExprCall { + range: _, + func, + args: _, + keywords: _, + }) => { + self.any_parenthesized_expressions = true; + // Only walk the function, the arguments are always parenthesized + self.visit_expr(func); + self.last = Some(expr); + return; + } + Expr::Subscript(_) => { + // Don't walk the value. Splitting before the value looks weird. + // Don't walk the slice, because the slice is always parenthesized. + return; + } + Expr::UnaryOp(ast::ExprUnaryOp { + range: _, + op, + operand: _, + }) => { + if op.is_invert() { + self.update_max_priority(OperatorPriority::BitwiseInversion); + } + } + + // `[a, b].test[300].dot` + Expr::Attribute(ast::ExprAttribute { + range: _, + value, + attr: _, + ctx: _, + }) => { + if has_parentheses(value, self.source) { + self.update_max_priority(OperatorPriority::Attribute); + } + } + + Expr::NamedExpr(_) + | Expr::GeneratorExp(_) + | Expr::Lambda(_) + | Expr::Await(_) + | Expr::Yield(_) + | Expr::YieldFrom(_) + | Expr::FormattedValue(_) + | Expr::JoinedStr(_) + | Expr::Constant(_) + | Expr::Starred(_) + | Expr::Name(_) + | Expr::Slice(_) => {} + }; + + walk_expr(self, expr); + } + + fn can_omit(self) -> bool { + if self.max_priority_count > 1 { + false + } else if self.max_priority == OperatorPriority::Attribute { + true + } else if !self.any_parenthesized_expressions { + // Only use the more complex IR when there is any expression that we can possibly split by + false + } else { + // Only use the layout if the first or last expression has parentheses of some sort. + let first_parenthesized = self + .first + .map_or(false, |first| has_parentheses(first, self.source)); + let last_parenthesized = self + .last + .map_or(false, |last| has_parentheses(last, self.source)); + first_parenthesized || last_parenthesized + } + } +} + +impl<'input> PreorderVisitor<'input> for CanOmitOptionalParenthesesVisitor<'input> { + fn visit_expr(&mut self, expr: &'input Expr) { + self.last = Some(expr); + + // Rule only applies for non-parenthesized expressions. + if is_expression_parenthesized(AnyNodeRef::from(expr), self.source) { + self.any_parenthesized_expressions = true; + } else { + self.visit_subexpression(expr); + } + + if self.first.is_none() { + self.first = Some(expr); + } + } +} + +fn has_parentheses(expr: &Expr, source: &str) -> bool { + matches!( + expr, + Expr::Dict(_) + | Expr::List(_) + | Expr::Tuple(_) + | Expr::Set(_) + | Expr::ListComp(_) + | Expr::SetComp(_) + | Expr::DictComp(_) + | Expr::Call(_) + | Expr::Subscript(_) + ) || is_expression_parenthesized(AnyNodeRef::from(expr), source) +} + +#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] +enum OperatorPriority { + None, + Attribute, + Comparator, + Exponential, + BitwiseInversion, + Multiplicative, + Additive, + Shift, + BitwiseAnd, + BitwiseOr, + BitwiseXor, + // TODO(micha) + #[allow(unused)] + String, + BooleanOperation, + Conditional, + Comprehension, +} + +impl From for OperatorPriority { + fn from(value: Operator) -> Self { + match value { + Operator::Add | Operator::Sub => OperatorPriority::Additive, + Operator::Mult + | Operator::MatMult + | Operator::Div + | Operator::Mod + | Operator::FloorDiv => OperatorPriority::Multiplicative, + Operator::Pow => OperatorPriority::Exponential, + Operator::LShift | Operator::RShift => OperatorPriority::Shift, + Operator::BitOr => OperatorPriority::BitwiseOr, + Operator::BitXor => OperatorPriority::BitwiseXor, + Operator::BitAnd => OperatorPriority::BitwiseAnd, + } + } +} diff --git a/crates/ruff_python_formatter/src/expression/parentheses.rs b/crates/ruff_python_formatter/src/expression/parentheses.rs index 58d083c8c6..80405ebcf2 100644 --- a/crates/ruff_python_formatter/src/expression/parentheses.rs +++ b/crates/ruff_python_formatter/src/expression/parentheses.rs @@ -1,53 +1,36 @@ -use crate::comments::Comments; +use crate::context::NodeLevel; +use crate::prelude::*; use crate::trivia::{first_non_trivia_token, first_non_trivia_token_rev, Token, TokenKind}; +use ruff_formatter::prelude::tag::Condition; +use ruff_formatter::{format_args, Argument, Arguments}; use ruff_python_ast::node::AnyNodeRef; use rustpython_parser::ast::Ranged; -pub(crate) trait NeedsParentheses { - fn needs_parentheses( - &self, - parenthesize: Parenthesize, - source: &str, - comments: &Comments, - ) -> Parentheses; +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub(crate) enum OptionalParentheses { + /// Add parentheses if the expression expands over multiple lines + Multiline, + + /// Always set parentheses regardless if the expression breaks or if they were + /// present in the source. + Always, + + /// Never add parentheses + Never, } -pub(super) fn default_expression_needs_parentheses( - node: AnyNodeRef, - parenthesize: Parenthesize, - source: &str, - comments: &Comments, -) -> Parentheses { - debug_assert!( - node.is_expression(), - "Should only be called for expressions" - ); - - // `Optional` or `Preserve` and expression has parentheses in source code. - if !parenthesize.is_if_breaks() && is_expression_parenthesized(node, source) { - Parentheses::Always - } - // `Optional` or `IfBreaks`: Add parentheses if the expression doesn't fit on a line but enforce - // parentheses if the expression has leading comments - else if !parenthesize.is_preserve() { - if comments.has_leading_comments(node) { - Parentheses::Always - } else { - Parentheses::Optional - } - } else { - //`Preserve` and expression has no parentheses in the source code - Parentheses::Never - } +pub(crate) trait NeedsParentheses { + /// Determines if this object needs optional parentheses or if it is safe to omit the parentheses. + fn needs_parentheses( + &self, + parent: AnyNodeRef, + context: &PyFormatContext, + ) -> OptionalParentheses; } /// Configures if the expression should be parenthesized. -#[derive(Copy, Clone, Debug, Default)] -pub enum Parenthesize { - /// Parenthesize the expression if it has parenthesis in the source. - #[default] - Preserve, - +#[derive(Copy, Clone, Debug, PartialEq)] +pub(crate) enum Parenthesize { /// Parenthesizes the expression if it doesn't fit on a line OR if the expression is parenthesized in the source code. Optional, @@ -55,35 +38,23 @@ pub enum Parenthesize { IfBreaks, } -impl Parenthesize { - const fn is_if_breaks(self) -> bool { - matches!(self, Parenthesize::IfBreaks) - } - - const fn is_preserve(self) -> bool { - matches!(self, Parenthesize::Preserve) - } -} - /// Whether it is necessary to add parentheses around an expression. /// This is different from [`Parenthesize`] in that it is the resolved representation: It takes into account /// whether there are parentheses in the source code or not. -#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[derive(Copy, Clone, Debug, Eq, PartialEq, Default)] pub enum Parentheses { - /// Always create parentheses + #[default] + Preserve, + + /// Always set parentheses regardless if the expression breaks or if they were + /// present in the source. Always, - /// Only add parentheses when necessary because the expression breaks over multiple lines. - Optional, - - /// Custom handling by the node's formatter implementation - Custom, - /// Never add parentheses Never, } -fn is_expression_parenthesized(expr: AnyNodeRef, contents: &str) -> bool { +pub(crate) fn is_expression_parenthesized(expr: AnyNodeRef, contents: &str) -> bool { matches!( first_non_trivia_token(expr.end(), contents), Some(Token { @@ -98,3 +69,145 @@ fn is_expression_parenthesized(expr: AnyNodeRef, contents: &str) -> bool { }) ) } + +/// Formats `content` enclosed by the `left` and `right` parentheses. The implementation also ensures +/// that expanding the parenthesized expression (or any of its children) doesn't enforce the +/// optional parentheses around the outer-most expression to materialize. +pub(crate) fn parenthesized<'content, 'ast, Content>( + left: &'static str, + content: &'content Content, + right: &'static str, +) -> FormatParenthesized<'content, 'ast> +where + Content: Format>, +{ + FormatParenthesized { + left, + content: Argument::new(content), + right, + } +} + +pub(crate) struct FormatParenthesized<'content, 'ast> { + left: &'static str, + content: Argument<'content, PyFormatContext<'ast>>, + right: &'static str, +} + +impl<'ast> Format> for FormatParenthesized<'_, 'ast> { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + let inner = format_with(|f| { + group(&format_args![ + text(self.left), + &soft_block_indent(&Arguments::from(&self.content)), + text(self.right) + ]) + .fmt(f) + }); + + let current_level = f.context().node_level(); + + f.context_mut() + .set_node_level(NodeLevel::ParenthesizedExpression); + + let result = if let NodeLevel::Expression(Some(group_id)) = current_level { + // Use fits expanded if there's an enclosing group that adds the optional parentheses. + // This ensures that expanding this parenthesized expression does not expand the optional parentheses group. + fits_expanded(&inner) + .with_condition(Some(Condition::if_group_fits_on_line(group_id))) + .fmt(f) + } else { + // It's not necessary to wrap the content if it is not inside of an optional_parentheses group. + inner.fmt(f) + }; + + f.context_mut().set_node_level(current_level); + + result + } +} + +/// Wraps an expression in parentheses only if it still does not fit after expanding all expressions that start or end with +/// a parentheses (`()`, `[]`, `{}`). +pub(crate) fn optional_parentheses<'content, 'ast, Content>( + content: &'content Content, +) -> FormatOptionalParentheses<'content, 'ast> +where + Content: Format>, +{ + FormatOptionalParentheses { + content: Argument::new(content), + } +} + +pub(crate) struct FormatOptionalParentheses<'content, 'ast> { + content: Argument<'content, PyFormatContext<'ast>>, +} + +impl<'ast> Format> for FormatOptionalParentheses<'_, 'ast> { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + let saved_level = f.context().node_level(); + + // The group id is used as a condition in [`in_parentheses_only`] to create a conditional group + // that is only active if the optional parentheses group expands. + let parens_id = f.group_id("optional_parentheses"); + + f.context_mut() + .set_node_level(NodeLevel::Expression(Some(parens_id))); + + // We can't use `soft_block_indent` here because that would always increment the indent, + // even if the group does not break (the indent is not soft). This would result in + // too deep indentations if a `parenthesized` group expands. Using `indent_if_group_breaks` + // gives us the desired *soft* indentation that is only present if the optional parentheses + // are shown. + let result = group(&format_args![ + if_group_breaks(&text("(")), + indent_if_group_breaks( + &format_args![soft_line_break(), Arguments::from(&self.content)], + parens_id + ), + soft_line_break(), + if_group_breaks(&text(")")) + ]) + .with_group_id(Some(parens_id)) + .fmt(f); + + f.context_mut().set_node_level(saved_level); + + result + } +} + +/// Makes `content` a group, but only if the outer expression is parenthesized (a list, parenthesized expression, dict, ...) +/// or if the expression gets parenthesized because it expands over multiple lines. +pub(crate) fn in_parentheses_only_group<'content, 'ast, Content>( + content: &'content Content, +) -> FormatInParenthesesOnlyGroup<'content, 'ast> +where + Content: Format>, +{ + FormatInParenthesesOnlyGroup { + content: Argument::new(content), + } +} + +pub(crate) struct FormatInParenthesesOnlyGroup<'content, 'ast> { + content: Argument<'content, PyFormatContext<'ast>>, +} + +impl<'ast> Format> for FormatInParenthesesOnlyGroup<'_, 'ast> { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + if let NodeLevel::Expression(Some(group_id)) = f.context().node_level() { + // If this content is enclosed by a group that adds the optional parentheses, then *disable* + // this group *except* if the optional parentheses are shown. + conditional_group( + &Arguments::from(&self.content), + Condition::if_group_breaks(group_id), + ) + .fmt(f) + } else { + // Unconditionally group the content if it is not enclosed by an optional parentheses group. + group(&Arguments::from(&self.content)).fmt(f) + } + } +} diff --git a/crates/ruff_python_formatter/src/expression/string.rs b/crates/ruff_python_formatter/src/expression/string.rs new file mode 100644 index 0000000000..fab95539f2 --- /dev/null +++ b/crates/ruff_python_formatter/src/expression/string.rs @@ -0,0 +1,475 @@ +use std::borrow::Cow; + +use bitflags::bitflags; +use ruff_text_size::{TextLen, TextRange, TextSize}; +use rustpython_parser::ast::{ExprConstant, Ranged}; +use rustpython_parser::lexer::{lex_starts_at, LexicalError, LexicalErrorType}; +use rustpython_parser::{Mode, Tok}; + +use ruff_formatter::{format_args, write, FormatError}; +use ruff_python_ast::str::is_implicit_concatenation; + +use crate::comments::{leading_comments, trailing_comments}; +use crate::expression::parentheses::in_parentheses_only_group; +use crate::prelude::*; +use crate::QuoteStyle; + +pub(super) struct FormatString<'a> { + constant: &'a ExprConstant, +} + +impl<'a> FormatString<'a> { + pub(super) fn new(constant: &'a ExprConstant) -> Self { + debug_assert!(constant.value.is_str()); + Self { constant } + } +} + +impl<'a> Format> for FormatString<'a> { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + let string_range = self.constant.range(); + let string_content = f.context().locator().slice(string_range); + + if is_implicit_concatenation(string_content) { + in_parentheses_only_group(&FormatStringContinuation::new(self.constant)).fmt(f) + } else { + FormatStringPart::new(string_range).fmt(f) + } + } +} + +struct FormatStringContinuation<'a> { + constant: &'a ExprConstant, +} + +impl<'a> FormatStringContinuation<'a> { + fn new(constant: &'a ExprConstant) -> Self { + debug_assert!(constant.value.is_str()); + Self { constant } + } +} + +impl Format> for FormatStringContinuation<'_> { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + let comments = f.context().comments().clone(); + let locator = f.context().locator(); + let mut dangling_comments = comments.dangling_comments(self.constant); + + let string_range = self.constant.range(); + let string_content = locator.slice(string_range); + + // The AST parses implicit concatenation as a single string. + // Call into the lexer to extract the individual chunks and format each string on its own. + // This code does not yet implement the automatic joining of strings that fit on the same line + // because this is a black preview style. + let lexer = lex_starts_at(string_content, Mode::Expression, string_range.start()); + + let mut joiner = f.join_with(soft_line_break_or_space()); + + for token in lexer { + let (token, token_range) = match token { + Ok(spanned) => spanned, + Err(LexicalError { + error: LexicalErrorType::IndentationError, + .. + }) => { + // This can happen if the string continuation appears anywhere inside of a parenthesized expression + // because the lexer doesn't know about the parentheses. For example, the following snipped triggers an Indentation error + // ```python + // { + // "key": ( + // [], + // 'a' + // 'b' + // 'c' + // ) + // } + // ``` + // Ignoring the error here is *safe* because we know that the program once parsed to a valid AST. + continue; + } + Err(_) => { + return Err(FormatError::SyntaxError); + } + }; + + match token { + Tok::String { .. } => { + // ```python + // ( + // "a" + // # leading + // "the comment above" + // ) + // ``` + let leading_comments_end = dangling_comments + .partition_point(|comment| comment.slice().start() <= token_range.start()); + + let (leading_part_comments, rest) = + dangling_comments.split_at(leading_comments_end); + + // ```python + // ( + // "a" # trailing comment + // "the comment above" + // ) + // ``` + let trailing_comments_end = rest.partition_point(|comment| { + comment.line_position().is_end_of_line() + && !locator.contains_line_break(TextRange::new( + token_range.end(), + comment.slice().start(), + )) + }); + + let (trailing_part_comments, rest) = rest.split_at(trailing_comments_end); + + joiner.entry(&format_args![ + line_suffix_boundary(), + leading_comments(leading_part_comments), + FormatStringPart::new(token_range), + trailing_comments(trailing_part_comments) + ]); + + dangling_comments = rest; + } + Tok::Comment(_) + | Tok::NonLogicalNewline + | Tok::Newline + | Tok::Indent + | Tok::Dedent => continue, + token => unreachable!("Unexpected token {token:?}"), + } + } + + debug_assert!(dangling_comments.is_empty()); + + joiner.finish() + } +} + +struct FormatStringPart { + part_range: TextRange, +} + +impl FormatStringPart { + const fn new(range: TextRange) -> Self { + Self { part_range: range } + } +} + +impl Format> for FormatStringPart { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + let string_content = f.context().locator().slice(self.part_range); + + let prefix = StringPrefix::parse(string_content); + let after_prefix = &string_content[usize::from(prefix.text_len())..]; + + let quotes = StringQuotes::parse(after_prefix).ok_or(FormatError::SyntaxError)?; + let relative_raw_content_range = TextRange::new( + prefix.text_len() + quotes.text_len(), + string_content.text_len() - quotes.text_len(), + ); + let raw_content_range = relative_raw_content_range + self.part_range.start(); + + let raw_content = &string_content[relative_raw_content_range]; + let preferred_quotes = preferred_quotes(raw_content, quotes, f.options().quote_style()); + + write!(f, [prefix, preferred_quotes])?; + + let (normalized, contains_newlines) = normalize_string(raw_content, preferred_quotes); + + match normalized { + Cow::Borrowed(_) => { + source_text_slice(raw_content_range, contains_newlines).fmt(f)?; + } + Cow::Owned(normalized) => { + dynamic_text(&normalized, Some(raw_content_range.start())).fmt(f)?; + } + } + + preferred_quotes.fmt(f) + } +} + +bitflags! { + #[derive(Copy, Clone, Debug)] + pub(super) struct StringPrefix: u8 { + const UNICODE = 0b0000_0001; + /// `r"test"` + const RAW = 0b0000_0010; + /// `R"test" + const RAW_UPPER = 0b0000_0100; + const BYTE = 0b0000_1000; + const F_STRING = 0b0001_0000; + } +} + +impl StringPrefix { + pub(super) fn parse(input: &str) -> StringPrefix { + let chars = input.chars(); + let mut prefix = StringPrefix::empty(); + + for c in chars { + let flag = match c { + 'u' | 'U' => StringPrefix::UNICODE, + 'f' | 'F' => StringPrefix::F_STRING, + 'b' | 'B' => StringPrefix::BYTE, + 'r' => StringPrefix::RAW, + 'R' => StringPrefix::RAW_UPPER, + '\'' | '"' => break, + c => { + unreachable!( + "Unexpected character '{c}' terminating the prefix of a string literal" + ); + } + }; + + prefix |= flag; + } + + prefix + } + + pub(super) const fn text_len(self) -> TextSize { + TextSize::new(self.bits().count_ones()) + } +} + +impl Format> for StringPrefix { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + // Retain the casing for the raw prefix: + // https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#r-strings-and-r-strings + if self.contains(StringPrefix::RAW) { + text("r").fmt(f)?; + } else if self.contains(StringPrefix::RAW_UPPER) { + text("R").fmt(f)?; + } + + if self.contains(StringPrefix::BYTE) { + text("b").fmt(f)?; + } + + if self.contains(StringPrefix::F_STRING) { + text("f").fmt(f)?; + } + + // Remove the unicode prefix `u` if any because it is meaningless in Python 3+. + + Ok(()) + } +} + +/// Detects the preferred quotes for `input`. +/// * single quoted strings: The preferred quote style is the one that requires less escape sequences. +/// * triple quoted strings: Use double quotes except the string contains a sequence of `"""`. +fn preferred_quotes( + input: &str, + quotes: StringQuotes, + configured_style: QuoteStyle, +) -> StringQuotes { + let preferred_style = if quotes.triple { + // True if the string contains a triple quote sequence of the configured quote style. + let mut uses_triple_quotes = false; + let mut chars = input.chars().peekable(); + + while let Some(c) = chars.next() { + let configured_quote_char = configured_style.as_char(); + match c { + '\\' => { + if matches!(chars.peek(), Some('"' | '\\')) { + chars.next(); + } + } + // `"` or `'` + c if c == configured_quote_char => { + match chars.peek().copied() { + Some(c) if c == configured_quote_char => { + // `""` or `''` + chars.next(); + + if chars.peek().copied() == Some(configured_quote_char) { + // `"""` or `'''` + chars.next(); + uses_triple_quotes = true; + } + } + Some(_) => { + // A single quote char, this is ok + } + None => { + // Trailing quote at the end of the comment + uses_triple_quotes = true; + } + } + } + _ => continue, + } + } + + if uses_triple_quotes { + // String contains a triple quote sequence of the configured quote style. + // Keep the existing quote style. + quotes.style + } else { + configured_style + } + } else { + let mut single_quotes = 0u32; + let mut double_quotes = 0u32; + + for c in input.chars() { + match c { + '\'' => { + single_quotes += 1; + } + + '"' => { + double_quotes += 1; + } + + _ => continue, + } + } + + match configured_style { + QuoteStyle::Single => { + if single_quotes > double_quotes { + QuoteStyle::Double + } else { + QuoteStyle::Single + } + } + QuoteStyle::Double => { + if double_quotes > single_quotes { + QuoteStyle::Single + } else { + QuoteStyle::Double + } + } + } + }; + + StringQuotes { + triple: quotes.triple, + style: preferred_style, + } +} + +#[derive(Copy, Clone, Debug)] +pub(super) struct StringQuotes { + triple: bool, + style: QuoteStyle, +} + +impl StringQuotes { + pub(super) fn parse(input: &str) -> Option { + let mut chars = input.chars(); + + let quote_char = chars.next()?; + let style = QuoteStyle::try_from(quote_char).ok()?; + + let triple = chars.next() == Some(quote_char) && chars.next() == Some(quote_char); + + Some(Self { triple, style }) + } + + pub(super) const fn is_triple(self) -> bool { + self.triple + } + + const fn text_len(self) -> TextSize { + if self.triple { + TextSize::new(3) + } else { + TextSize::new(1) + } + } +} + +impl Format> for StringQuotes { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + let quotes = match (self.style, self.triple) { + (QuoteStyle::Single, false) => "'", + (QuoteStyle::Single, true) => "'''", + (QuoteStyle::Double, false) => "\"", + (QuoteStyle::Double, true) => "\"\"\"", + }; + + text(quotes).fmt(f) + } +} + +/// Adds the necessary quote escapes and removes unnecessary escape sequences when quoting `input` +/// with the provided `style`. +/// +/// Returns the normalized string and whether it contains new lines. +fn normalize_string(input: &str, quotes: StringQuotes) -> (Cow, ContainsNewlines) { + // The normalized string if `input` is not yet normalized. + // `output` must remain empty if `input` is already normalized. + let mut output = String::new(); + // Tracks the last index of `input` that has been written to `output`. + // If `last_index` is `0` at the end, then the input is already normalized and can be returned as is. + let mut last_index = 0; + + let mut newlines = ContainsNewlines::No; + + let style = quotes.style; + let preferred_quote = style.as_char(); + let opposite_quote = style.invert().as_char(); + + let mut chars = input.char_indices(); + + while let Some((index, c)) = chars.next() { + if c == '\r' { + output.push_str(&input[last_index..index]); + + // Skip over the '\r' character, keep the `\n` + if input.as_bytes().get(index + 1).copied() == Some(b'\n') { + chars.next(); + } + // Replace the `\r` with a `\n` + else { + output.push('\n'); + } + + last_index = index + '\r'.len_utf8(); + newlines = ContainsNewlines::Yes; + } else if c == '\n' { + newlines = ContainsNewlines::Yes; + } else if !quotes.triple { + if c == '\\' { + if let Some(next) = input.as_bytes().get(index + 1).copied().map(char::from) { + #[allow(clippy::if_same_then_else)] + if next == opposite_quote { + // Remove the escape by ending before the backslash and starting again with the quote + chars.next(); + output.push_str(&input[last_index..index]); + last_index = index + '\\'.len_utf8(); + } else if next == preferred_quote { + // Quote is already escaped, skip over it. + chars.next(); + } else if next == '\\' { + // Skip over escaped backslashes + chars.next(); + } + } + } else if c == preferred_quote { + // Escape the quote + output.push_str(&input[last_index..index]); + output.push('\\'); + output.push(c); + last_index = index + preferred_quote.len_utf8(); + } + } + } + + let normalized = if last_index == 0 { + Cow::Borrowed(input) + } else { + output.push_str(&input[last_index..]); + Cow::Owned(output) + }; + + (normalized, newlines) +} diff --git a/crates/ruff_python_formatter/src/generated.rs b/crates/ruff_python_formatter/src/generated.rs index d491ef0065..24bb09cd3b 100644 --- a/crates/ruff_python_formatter/src/generated.rs +++ b/crates/ruff_python_formatter/src/generated.rs @@ -1,4 +1,4 @@ -//! This is a generated file. Don't modify it by hand! Run `scripts/generate.py` to re-generate the file. +//! This is a generated file. Don't modify it by hand! Run `crates/ruff_python_formatter/generate.py` to re-generate the file. use crate::context::PyFormatContext; use crate::{AsFormat, FormatNodeRule, IntoFormat}; use ruff_formatter::formatter::Formatter; @@ -25,7 +25,7 @@ impl<'ast> AsFormat> for ast::ModModule { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new(self, crate::module::mod_module::FormatModModule::default()) + FormatRefWithRule::new(self, crate::module::mod_module::FormatModModule) } } impl<'ast> IntoFormat> for ast::ModModule { @@ -35,7 +35,7 @@ impl<'ast> IntoFormat> for ast::ModModule { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new(self, crate::module::mod_module::FormatModModule::default()) + FormatOwnedWithRule::new(self, crate::module::mod_module::FormatModModule) } } @@ -59,10 +59,7 @@ impl<'ast> AsFormat> for ast::ModInteractive { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::module::mod_interactive::FormatModInteractive::default(), - ) + FormatRefWithRule::new(self, crate::module::mod_interactive::FormatModInteractive) } } impl<'ast> IntoFormat> for ast::ModInteractive { @@ -72,10 +69,7 @@ impl<'ast> IntoFormat> for ast::ModInteractive { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::module::mod_interactive::FormatModInteractive::default(), - ) + FormatOwnedWithRule::new(self, crate::module::mod_interactive::FormatModInteractive) } } @@ -99,10 +93,7 @@ impl<'ast> AsFormat> for ast::ModExpression { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::module::mod_expression::FormatModExpression::default(), - ) + FormatRefWithRule::new(self, crate::module::mod_expression::FormatModExpression) } } impl<'ast> IntoFormat> for ast::ModExpression { @@ -112,10 +103,7 @@ impl<'ast> IntoFormat> for ast::ModExpression { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::module::mod_expression::FormatModExpression::default(), - ) + FormatOwnedWithRule::new(self, crate::module::mod_expression::FormatModExpression) } } @@ -141,7 +129,7 @@ impl<'ast> AsFormat> for ast::ModFunctionType { fn format(&self) -> Self::Format<'_> { FormatRefWithRule::new( self, - crate::module::mod_function_type::FormatModFunctionType::default(), + crate::module::mod_function_type::FormatModFunctionType, ) } } @@ -154,7 +142,7 @@ impl<'ast> IntoFormat> for ast::ModFunctionType { fn into_format(self) -> Self::Format { FormatOwnedWithRule::new( self, - crate::module::mod_function_type::FormatModFunctionType::default(), + crate::module::mod_function_type::FormatModFunctionType, ) } } @@ -181,7 +169,7 @@ impl<'ast> AsFormat> for ast::StmtFunctionDef { fn format(&self) -> Self::Format<'_> { FormatRefWithRule::new( self, - crate::statement::stmt_function_def::FormatStmtFunctionDef::default(), + crate::statement::stmt_function_def::FormatStmtFunctionDef, ) } } @@ -194,7 +182,7 @@ impl<'ast> IntoFormat> for ast::StmtFunctionDef { fn into_format(self) -> Self::Format { FormatOwnedWithRule::new( self, - crate::statement::stmt_function_def::FormatStmtFunctionDef::default(), + crate::statement::stmt_function_def::FormatStmtFunctionDef, ) } } @@ -221,7 +209,7 @@ impl<'ast> AsFormat> for ast::StmtAsyncFunctionDef { fn format(&self) -> Self::Format<'_> { FormatRefWithRule::new( self, - crate::statement::stmt_async_function_def::FormatStmtAsyncFunctionDef::default(), + crate::statement::stmt_async_function_def::FormatStmtAsyncFunctionDef, ) } } @@ -234,7 +222,7 @@ impl<'ast> IntoFormat> for ast::StmtAsyncFunctionDef { fn into_format(self) -> Self::Format { FormatOwnedWithRule::new( self, - crate::statement::stmt_async_function_def::FormatStmtAsyncFunctionDef::default(), + crate::statement::stmt_async_function_def::FormatStmtAsyncFunctionDef, ) } } @@ -259,10 +247,7 @@ impl<'ast> AsFormat> for ast::StmtClassDef { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::statement::stmt_class_def::FormatStmtClassDef::default(), - ) + FormatRefWithRule::new(self, crate::statement::stmt_class_def::FormatStmtClassDef) } } impl<'ast> IntoFormat> for ast::StmtClassDef { @@ -272,10 +257,7 @@ impl<'ast> IntoFormat> for ast::StmtClassDef { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::statement::stmt_class_def::FormatStmtClassDef::default(), - ) + FormatOwnedWithRule::new(self, crate::statement::stmt_class_def::FormatStmtClassDef) } } @@ -299,10 +281,7 @@ impl<'ast> AsFormat> for ast::StmtReturn { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::statement::stmt_return::FormatStmtReturn::default(), - ) + FormatRefWithRule::new(self, crate::statement::stmt_return::FormatStmtReturn) } } impl<'ast> IntoFormat> for ast::StmtReturn { @@ -312,10 +291,7 @@ impl<'ast> IntoFormat> for ast::StmtReturn { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::statement::stmt_return::FormatStmtReturn::default(), - ) + FormatOwnedWithRule::new(self, crate::statement::stmt_return::FormatStmtReturn) } } @@ -339,10 +315,7 @@ impl<'ast> AsFormat> for ast::StmtDelete { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::statement::stmt_delete::FormatStmtDelete::default(), - ) + FormatRefWithRule::new(self, crate::statement::stmt_delete::FormatStmtDelete) } } impl<'ast> IntoFormat> for ast::StmtDelete { @@ -352,10 +325,7 @@ impl<'ast> IntoFormat> for ast::StmtDelete { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::statement::stmt_delete::FormatStmtDelete::default(), - ) + FormatOwnedWithRule::new(self, crate::statement::stmt_delete::FormatStmtDelete) } } @@ -379,10 +349,7 @@ impl<'ast> AsFormat> for ast::StmtAssign { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::statement::stmt_assign::FormatStmtAssign::default(), - ) + FormatRefWithRule::new(self, crate::statement::stmt_assign::FormatStmtAssign) } } impl<'ast> IntoFormat> for ast::StmtAssign { @@ -392,10 +359,7 @@ impl<'ast> IntoFormat> for ast::StmtAssign { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::statement::stmt_assign::FormatStmtAssign::default(), - ) + FormatOwnedWithRule::new(self, crate::statement::stmt_assign::FormatStmtAssign) } } @@ -419,10 +383,7 @@ impl<'ast> AsFormat> for ast::StmtAugAssign { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::statement::stmt_aug_assign::FormatStmtAugAssign::default(), - ) + FormatRefWithRule::new(self, crate::statement::stmt_aug_assign::FormatStmtAugAssign) } } impl<'ast> IntoFormat> for ast::StmtAugAssign { @@ -432,10 +393,7 @@ impl<'ast> IntoFormat> for ast::StmtAugAssign { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::statement::stmt_aug_assign::FormatStmtAugAssign::default(), - ) + FormatOwnedWithRule::new(self, crate::statement::stmt_aug_assign::FormatStmtAugAssign) } } @@ -459,10 +417,7 @@ impl<'ast> AsFormat> for ast::StmtAnnAssign { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::statement::stmt_ann_assign::FormatStmtAnnAssign::default(), - ) + FormatRefWithRule::new(self, crate::statement::stmt_ann_assign::FormatStmtAnnAssign) } } impl<'ast> IntoFormat> for ast::StmtAnnAssign { @@ -472,10 +427,7 @@ impl<'ast> IntoFormat> for ast::StmtAnnAssign { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::statement::stmt_ann_assign::FormatStmtAnnAssign::default(), - ) + FormatOwnedWithRule::new(self, crate::statement::stmt_ann_assign::FormatStmtAnnAssign) } } @@ -493,7 +445,7 @@ impl<'ast> AsFormat> for ast::StmtFor { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new(self, crate::statement::stmt_for::FormatStmtFor::default()) + FormatRefWithRule::new(self, crate::statement::stmt_for::FormatStmtFor) } } impl<'ast> IntoFormat> for ast::StmtFor { @@ -503,7 +455,7 @@ impl<'ast> IntoFormat> for ast::StmtFor { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new(self, crate::statement::stmt_for::FormatStmtFor::default()) + FormatOwnedWithRule::new(self, crate::statement::stmt_for::FormatStmtFor) } } @@ -527,10 +479,7 @@ impl<'ast> AsFormat> for ast::StmtAsyncFor { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::statement::stmt_async_for::FormatStmtAsyncFor::default(), - ) + FormatRefWithRule::new(self, crate::statement::stmt_async_for::FormatStmtAsyncFor) } } impl<'ast> IntoFormat> for ast::StmtAsyncFor { @@ -540,10 +489,7 @@ impl<'ast> IntoFormat> for ast::StmtAsyncFor { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::statement::stmt_async_for::FormatStmtAsyncFor::default(), - ) + FormatOwnedWithRule::new(self, crate::statement::stmt_async_for::FormatStmtAsyncFor) } } @@ -567,10 +513,7 @@ impl<'ast> AsFormat> for ast::StmtWhile { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::statement::stmt_while::FormatStmtWhile::default(), - ) + FormatRefWithRule::new(self, crate::statement::stmt_while::FormatStmtWhile) } } impl<'ast> IntoFormat> for ast::StmtWhile { @@ -580,10 +523,7 @@ impl<'ast> IntoFormat> for ast::StmtWhile { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::statement::stmt_while::FormatStmtWhile::default(), - ) + FormatOwnedWithRule::new(self, crate::statement::stmt_while::FormatStmtWhile) } } @@ -601,7 +541,7 @@ impl<'ast> AsFormat> for ast::StmtIf { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new(self, crate::statement::stmt_if::FormatStmtIf::default()) + FormatRefWithRule::new(self, crate::statement::stmt_if::FormatStmtIf) } } impl<'ast> IntoFormat> for ast::StmtIf { @@ -611,7 +551,7 @@ impl<'ast> IntoFormat> for ast::StmtIf { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new(self, crate::statement::stmt_if::FormatStmtIf::default()) + FormatOwnedWithRule::new(self, crate::statement::stmt_if::FormatStmtIf) } } @@ -635,7 +575,7 @@ impl<'ast> AsFormat> for ast::StmtWith { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new(self, crate::statement::stmt_with::FormatStmtWith::default()) + FormatRefWithRule::new(self, crate::statement::stmt_with::FormatStmtWith) } } impl<'ast> IntoFormat> for ast::StmtWith { @@ -645,7 +585,7 @@ impl<'ast> IntoFormat> for ast::StmtWith { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new(self, crate::statement::stmt_with::FormatStmtWith::default()) + FormatOwnedWithRule::new(self, crate::statement::stmt_with::FormatStmtWith) } } @@ -669,10 +609,7 @@ impl<'ast> AsFormat> for ast::StmtAsyncWith { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::statement::stmt_async_with::FormatStmtAsyncWith::default(), - ) + FormatRefWithRule::new(self, crate::statement::stmt_async_with::FormatStmtAsyncWith) } } impl<'ast> IntoFormat> for ast::StmtAsyncWith { @@ -682,10 +619,7 @@ impl<'ast> IntoFormat> for ast::StmtAsyncWith { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::statement::stmt_async_with::FormatStmtAsyncWith::default(), - ) + FormatOwnedWithRule::new(self, crate::statement::stmt_async_with::FormatStmtAsyncWith) } } @@ -709,10 +643,7 @@ impl<'ast> AsFormat> for ast::StmtMatch { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::statement::stmt_match::FormatStmtMatch::default(), - ) + FormatRefWithRule::new(self, crate::statement::stmt_match::FormatStmtMatch) } } impl<'ast> IntoFormat> for ast::StmtMatch { @@ -722,10 +653,7 @@ impl<'ast> IntoFormat> for ast::StmtMatch { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::statement::stmt_match::FormatStmtMatch::default(), - ) + FormatOwnedWithRule::new(self, crate::statement::stmt_match::FormatStmtMatch) } } @@ -749,10 +677,7 @@ impl<'ast> AsFormat> for ast::StmtRaise { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::statement::stmt_raise::FormatStmtRaise::default(), - ) + FormatRefWithRule::new(self, crate::statement::stmt_raise::FormatStmtRaise) } } impl<'ast> IntoFormat> for ast::StmtRaise { @@ -762,10 +687,7 @@ impl<'ast> IntoFormat> for ast::StmtRaise { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::statement::stmt_raise::FormatStmtRaise::default(), - ) + FormatOwnedWithRule::new(self, crate::statement::stmt_raise::FormatStmtRaise) } } @@ -783,7 +705,7 @@ impl<'ast> AsFormat> for ast::StmtTry { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new(self, crate::statement::stmt_try::FormatStmtTry::default()) + FormatRefWithRule::new(self, crate::statement::stmt_try::FormatStmtTry) } } impl<'ast> IntoFormat> for ast::StmtTry { @@ -793,7 +715,7 @@ impl<'ast> IntoFormat> for ast::StmtTry { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new(self, crate::statement::stmt_try::FormatStmtTry::default()) + FormatOwnedWithRule::new(self, crate::statement::stmt_try::FormatStmtTry) } } @@ -817,10 +739,7 @@ impl<'ast> AsFormat> for ast::StmtTryStar { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::statement::stmt_try_star::FormatStmtTryStar::default(), - ) + FormatRefWithRule::new(self, crate::statement::stmt_try_star::FormatStmtTryStar) } } impl<'ast> IntoFormat> for ast::StmtTryStar { @@ -830,10 +749,7 @@ impl<'ast> IntoFormat> for ast::StmtTryStar { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::statement::stmt_try_star::FormatStmtTryStar::default(), - ) + FormatOwnedWithRule::new(self, crate::statement::stmt_try_star::FormatStmtTryStar) } } @@ -857,10 +773,7 @@ impl<'ast> AsFormat> for ast::StmtAssert { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::statement::stmt_assert::FormatStmtAssert::default(), - ) + FormatRefWithRule::new(self, crate::statement::stmt_assert::FormatStmtAssert) } } impl<'ast> IntoFormat> for ast::StmtAssert { @@ -870,10 +783,7 @@ impl<'ast> IntoFormat> for ast::StmtAssert { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::statement::stmt_assert::FormatStmtAssert::default(), - ) + FormatOwnedWithRule::new(self, crate::statement::stmt_assert::FormatStmtAssert) } } @@ -897,10 +807,7 @@ impl<'ast> AsFormat> for ast::StmtImport { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::statement::stmt_import::FormatStmtImport::default(), - ) + FormatRefWithRule::new(self, crate::statement::stmt_import::FormatStmtImport) } } impl<'ast> IntoFormat> for ast::StmtImport { @@ -910,10 +817,7 @@ impl<'ast> IntoFormat> for ast::StmtImport { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::statement::stmt_import::FormatStmtImport::default(), - ) + FormatOwnedWithRule::new(self, crate::statement::stmt_import::FormatStmtImport) } } @@ -939,7 +843,7 @@ impl<'ast> AsFormat> for ast::StmtImportFrom { fn format(&self) -> Self::Format<'_> { FormatRefWithRule::new( self, - crate::statement::stmt_import_from::FormatStmtImportFrom::default(), + crate::statement::stmt_import_from::FormatStmtImportFrom, ) } } @@ -952,7 +856,7 @@ impl<'ast> IntoFormat> for ast::StmtImportFrom { fn into_format(self) -> Self::Format { FormatOwnedWithRule::new( self, - crate::statement::stmt_import_from::FormatStmtImportFrom::default(), + crate::statement::stmt_import_from::FormatStmtImportFrom, ) } } @@ -977,10 +881,7 @@ impl<'ast> AsFormat> for ast::StmtGlobal { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::statement::stmt_global::FormatStmtGlobal::default(), - ) + FormatRefWithRule::new(self, crate::statement::stmt_global::FormatStmtGlobal) } } impl<'ast> IntoFormat> for ast::StmtGlobal { @@ -990,10 +891,7 @@ impl<'ast> IntoFormat> for ast::StmtGlobal { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::statement::stmt_global::FormatStmtGlobal::default(), - ) + FormatOwnedWithRule::new(self, crate::statement::stmt_global::FormatStmtGlobal) } } @@ -1017,10 +915,7 @@ impl<'ast> AsFormat> for ast::StmtNonlocal { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::statement::stmt_nonlocal::FormatStmtNonlocal::default(), - ) + FormatRefWithRule::new(self, crate::statement::stmt_nonlocal::FormatStmtNonlocal) } } impl<'ast> IntoFormat> for ast::StmtNonlocal { @@ -1030,10 +925,7 @@ impl<'ast> IntoFormat> for ast::StmtNonlocal { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::statement::stmt_nonlocal::FormatStmtNonlocal::default(), - ) + FormatOwnedWithRule::new(self, crate::statement::stmt_nonlocal::FormatStmtNonlocal) } } @@ -1057,7 +949,7 @@ impl<'ast> AsFormat> for ast::StmtExpr { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new(self, crate::statement::stmt_expr::FormatStmtExpr::default()) + FormatRefWithRule::new(self, crate::statement::stmt_expr::FormatStmtExpr) } } impl<'ast> IntoFormat> for ast::StmtExpr { @@ -1067,7 +959,7 @@ impl<'ast> IntoFormat> for ast::StmtExpr { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new(self, crate::statement::stmt_expr::FormatStmtExpr::default()) + FormatOwnedWithRule::new(self, crate::statement::stmt_expr::FormatStmtExpr) } } @@ -1091,7 +983,7 @@ impl<'ast> AsFormat> for ast::StmtPass { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new(self, crate::statement::stmt_pass::FormatStmtPass::default()) + FormatRefWithRule::new(self, crate::statement::stmt_pass::FormatStmtPass) } } impl<'ast> IntoFormat> for ast::StmtPass { @@ -1101,7 +993,7 @@ impl<'ast> IntoFormat> for ast::StmtPass { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new(self, crate::statement::stmt_pass::FormatStmtPass::default()) + FormatOwnedWithRule::new(self, crate::statement::stmt_pass::FormatStmtPass) } } @@ -1125,10 +1017,7 @@ impl<'ast> AsFormat> for ast::StmtBreak { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::statement::stmt_break::FormatStmtBreak::default(), - ) + FormatRefWithRule::new(self, crate::statement::stmt_break::FormatStmtBreak) } } impl<'ast> IntoFormat> for ast::StmtBreak { @@ -1138,10 +1027,7 @@ impl<'ast> IntoFormat> for ast::StmtBreak { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::statement::stmt_break::FormatStmtBreak::default(), - ) + FormatOwnedWithRule::new(self, crate::statement::stmt_break::FormatStmtBreak) } } @@ -1165,10 +1051,7 @@ impl<'ast> AsFormat> for ast::StmtContinue { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::statement::stmt_continue::FormatStmtContinue::default(), - ) + FormatRefWithRule::new(self, crate::statement::stmt_continue::FormatStmtContinue) } } impl<'ast> IntoFormat> for ast::StmtContinue { @@ -1178,10 +1061,7 @@ impl<'ast> IntoFormat> for ast::StmtContinue { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::statement::stmt_continue::FormatStmtContinue::default(), - ) + FormatOwnedWithRule::new(self, crate::statement::stmt_continue::FormatStmtContinue) } } @@ -1247,7 +1127,7 @@ impl<'ast> AsFormat> for ast::ExprNamedExpr { fn format(&self) -> Self::Format<'_> { FormatRefWithRule::new( self, - crate::expression::expr_named_expr::FormatExprNamedExpr::default(), + crate::expression::expr_named_expr::FormatExprNamedExpr, ) } } @@ -1260,7 +1140,7 @@ impl<'ast> IntoFormat> for ast::ExprNamedExpr { fn into_format(self) -> Self::Format { FormatOwnedWithRule::new( self, - crate::expression::expr_named_expr::FormatExprNamedExpr::default(), + crate::expression::expr_named_expr::FormatExprNamedExpr, ) } } @@ -1325,10 +1205,7 @@ impl<'ast> AsFormat> for ast::ExprUnaryOp { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::expression::expr_unary_op::FormatExprUnaryOp::default(), - ) + FormatRefWithRule::new(self, crate::expression::expr_unary_op::FormatExprUnaryOp) } } impl<'ast> IntoFormat> for ast::ExprUnaryOp { @@ -1338,10 +1215,7 @@ impl<'ast> IntoFormat> for ast::ExprUnaryOp { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::expression::expr_unary_op::FormatExprUnaryOp::default(), - ) + FormatOwnedWithRule::new(self, crate::expression::expr_unary_op::FormatExprUnaryOp) } } @@ -1365,10 +1239,7 @@ impl<'ast> AsFormat> for ast::ExprLambda { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::expression::expr_lambda::FormatExprLambda::default(), - ) + FormatRefWithRule::new(self, crate::expression::expr_lambda::FormatExprLambda) } } impl<'ast> IntoFormat> for ast::ExprLambda { @@ -1378,10 +1249,7 @@ impl<'ast> IntoFormat> for ast::ExprLambda { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::expression::expr_lambda::FormatExprLambda::default(), - ) + FormatOwnedWithRule::new(self, crate::expression::expr_lambda::FormatExprLambda) } } @@ -1405,10 +1273,7 @@ impl<'ast> AsFormat> for ast::ExprIfExp { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::expression::expr_if_exp::FormatExprIfExp::default(), - ) + FormatRefWithRule::new(self, crate::expression::expr_if_exp::FormatExprIfExp) } } impl<'ast> IntoFormat> for ast::ExprIfExp { @@ -1418,10 +1283,7 @@ impl<'ast> IntoFormat> for ast::ExprIfExp { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::expression::expr_if_exp::FormatExprIfExp::default(), - ) + FormatOwnedWithRule::new(self, crate::expression::expr_if_exp::FormatExprIfExp) } } @@ -1445,10 +1307,7 @@ impl<'ast> AsFormat> for ast::ExprDict { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::expression::expr_dict::FormatExprDict::default(), - ) + FormatRefWithRule::new(self, crate::expression::expr_dict::FormatExprDict) } } impl<'ast> IntoFormat> for ast::ExprDict { @@ -1458,10 +1317,7 @@ impl<'ast> IntoFormat> for ast::ExprDict { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::expression::expr_dict::FormatExprDict::default(), - ) + FormatOwnedWithRule::new(self, crate::expression::expr_dict::FormatExprDict) } } @@ -1479,7 +1335,7 @@ impl<'ast> AsFormat> for ast::ExprSet { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new(self, crate::expression::expr_set::FormatExprSet::default()) + FormatRefWithRule::new(self, crate::expression::expr_set::FormatExprSet) } } impl<'ast> IntoFormat> for ast::ExprSet { @@ -1489,7 +1345,7 @@ impl<'ast> IntoFormat> for ast::ExprSet { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new(self, crate::expression::expr_set::FormatExprSet::default()) + FormatOwnedWithRule::new(self, crate::expression::expr_set::FormatExprSet) } } @@ -1513,10 +1369,7 @@ impl<'ast> AsFormat> for ast::ExprListComp { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::expression::expr_list_comp::FormatExprListComp::default(), - ) + FormatRefWithRule::new(self, crate::expression::expr_list_comp::FormatExprListComp) } } impl<'ast> IntoFormat> for ast::ExprListComp { @@ -1526,10 +1379,7 @@ impl<'ast> IntoFormat> for ast::ExprListComp { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::expression::expr_list_comp::FormatExprListComp::default(), - ) + FormatOwnedWithRule::new(self, crate::expression::expr_list_comp::FormatExprListComp) } } @@ -1553,10 +1403,7 @@ impl<'ast> AsFormat> for ast::ExprSetComp { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::expression::expr_set_comp::FormatExprSetComp::default(), - ) + FormatRefWithRule::new(self, crate::expression::expr_set_comp::FormatExprSetComp) } } impl<'ast> IntoFormat> for ast::ExprSetComp { @@ -1566,10 +1413,7 @@ impl<'ast> IntoFormat> for ast::ExprSetComp { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::expression::expr_set_comp::FormatExprSetComp::default(), - ) + FormatOwnedWithRule::new(self, crate::expression::expr_set_comp::FormatExprSetComp) } } @@ -1593,10 +1437,7 @@ impl<'ast> AsFormat> for ast::ExprDictComp { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::expression::expr_dict_comp::FormatExprDictComp::default(), - ) + FormatRefWithRule::new(self, crate::expression::expr_dict_comp::FormatExprDictComp) } } impl<'ast> IntoFormat> for ast::ExprDictComp { @@ -1606,10 +1447,7 @@ impl<'ast> IntoFormat> for ast::ExprDictComp { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::expression::expr_dict_comp::FormatExprDictComp::default(), - ) + FormatOwnedWithRule::new(self, crate::expression::expr_dict_comp::FormatExprDictComp) } } @@ -1635,7 +1473,7 @@ impl<'ast> AsFormat> for ast::ExprGeneratorExp { fn format(&self) -> Self::Format<'_> { FormatRefWithRule::new( self, - crate::expression::expr_generator_exp::FormatExprGeneratorExp::default(), + crate::expression::expr_generator_exp::FormatExprGeneratorExp, ) } } @@ -1648,7 +1486,7 @@ impl<'ast> IntoFormat> for ast::ExprGeneratorExp { fn into_format(self) -> Self::Format { FormatOwnedWithRule::new( self, - crate::expression::expr_generator_exp::FormatExprGeneratorExp::default(), + crate::expression::expr_generator_exp::FormatExprGeneratorExp, ) } } @@ -1673,10 +1511,7 @@ impl<'ast> AsFormat> for ast::ExprAwait { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::expression::expr_await::FormatExprAwait::default(), - ) + FormatRefWithRule::new(self, crate::expression::expr_await::FormatExprAwait) } } impl<'ast> IntoFormat> for ast::ExprAwait { @@ -1686,10 +1521,7 @@ impl<'ast> IntoFormat> for ast::ExprAwait { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::expression::expr_await::FormatExprAwait::default(), - ) + FormatOwnedWithRule::new(self, crate::expression::expr_await::FormatExprAwait) } } @@ -1713,10 +1545,7 @@ impl<'ast> AsFormat> for ast::ExprYield { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::expression::expr_yield::FormatExprYield::default(), - ) + FormatRefWithRule::new(self, crate::expression::expr_yield::FormatExprYield) } } impl<'ast> IntoFormat> for ast::ExprYield { @@ -1726,10 +1555,7 @@ impl<'ast> IntoFormat> for ast::ExprYield { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::expression::expr_yield::FormatExprYield::default(), - ) + FormatOwnedWithRule::new(self, crate::expression::expr_yield::FormatExprYield) } } @@ -1755,7 +1581,7 @@ impl<'ast> AsFormat> for ast::ExprYieldFrom { fn format(&self) -> Self::Format<'_> { FormatRefWithRule::new( self, - crate::expression::expr_yield_from::FormatExprYieldFrom::default(), + crate::expression::expr_yield_from::FormatExprYieldFrom, ) } } @@ -1768,7 +1594,7 @@ impl<'ast> IntoFormat> for ast::ExprYieldFrom { fn into_format(self) -> Self::Format { FormatOwnedWithRule::new( self, - crate::expression::expr_yield_from::FormatExprYieldFrom::default(), + crate::expression::expr_yield_from::FormatExprYieldFrom, ) } } @@ -1833,10 +1659,7 @@ impl<'ast> AsFormat> for ast::ExprCall { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::expression::expr_call::FormatExprCall::default(), - ) + FormatRefWithRule::new(self, crate::expression::expr_call::FormatExprCall) } } impl<'ast> IntoFormat> for ast::ExprCall { @@ -1846,10 +1669,7 @@ impl<'ast> IntoFormat> for ast::ExprCall { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::expression::expr_call::FormatExprCall::default(), - ) + FormatOwnedWithRule::new(self, crate::expression::expr_call::FormatExprCall) } } @@ -1875,7 +1695,7 @@ impl<'ast> AsFormat> for ast::ExprFormattedValue { fn format(&self) -> Self::Format<'_> { FormatRefWithRule::new( self, - crate::expression::expr_formatted_value::FormatExprFormattedValue::default(), + crate::expression::expr_formatted_value::FormatExprFormattedValue, ) } } @@ -1888,7 +1708,7 @@ impl<'ast> IntoFormat> for ast::ExprFormattedValue { fn into_format(self) -> Self::Format { FormatOwnedWithRule::new( self, - crate::expression::expr_formatted_value::FormatExprFormattedValue::default(), + crate::expression::expr_formatted_value::FormatExprFormattedValue, ) } } @@ -1915,7 +1735,7 @@ impl<'ast> AsFormat> for ast::ExprJoinedStr { fn format(&self) -> Self::Format<'_> { FormatRefWithRule::new( self, - crate::expression::expr_joined_str::FormatExprJoinedStr::default(), + crate::expression::expr_joined_str::FormatExprJoinedStr, ) } } @@ -1928,7 +1748,7 @@ impl<'ast> IntoFormat> for ast::ExprJoinedStr { fn into_format(self) -> Self::Format { FormatOwnedWithRule::new( self, - crate::expression::expr_joined_str::FormatExprJoinedStr::default(), + crate::expression::expr_joined_str::FormatExprJoinedStr, ) } } @@ -1993,10 +1813,7 @@ impl<'ast> AsFormat> for ast::ExprAttribute { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::expression::expr_attribute::FormatExprAttribute::default(), - ) + FormatRefWithRule::new(self, crate::expression::expr_attribute::FormatExprAttribute) } } impl<'ast> IntoFormat> for ast::ExprAttribute { @@ -2006,10 +1823,7 @@ impl<'ast> IntoFormat> for ast::ExprAttribute { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::expression::expr_attribute::FormatExprAttribute::default(), - ) + FormatOwnedWithRule::new(self, crate::expression::expr_attribute::FormatExprAttribute) } } @@ -2033,10 +1847,7 @@ impl<'ast> AsFormat> for ast::ExprSubscript { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::expression::expr_subscript::FormatExprSubscript::default(), - ) + FormatRefWithRule::new(self, crate::expression::expr_subscript::FormatExprSubscript) } } impl<'ast> IntoFormat> for ast::ExprSubscript { @@ -2046,10 +1857,7 @@ impl<'ast> IntoFormat> for ast::ExprSubscript { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::expression::expr_subscript::FormatExprSubscript::default(), - ) + FormatOwnedWithRule::new(self, crate::expression::expr_subscript::FormatExprSubscript) } } @@ -2073,10 +1881,7 @@ impl<'ast> AsFormat> for ast::ExprStarred { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::expression::expr_starred::FormatExprStarred::default(), - ) + FormatRefWithRule::new(self, crate::expression::expr_starred::FormatExprStarred) } } impl<'ast> IntoFormat> for ast::ExprStarred { @@ -2086,10 +1891,7 @@ impl<'ast> IntoFormat> for ast::ExprStarred { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::expression::expr_starred::FormatExprStarred::default(), - ) + FormatOwnedWithRule::new(self, crate::expression::expr_starred::FormatExprStarred) } } @@ -2113,10 +1915,7 @@ impl<'ast> AsFormat> for ast::ExprName { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::expression::expr_name::FormatExprName::default(), - ) + FormatRefWithRule::new(self, crate::expression::expr_name::FormatExprName) } } impl<'ast> IntoFormat> for ast::ExprName { @@ -2126,10 +1925,7 @@ impl<'ast> IntoFormat> for ast::ExprName { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::expression::expr_name::FormatExprName::default(), - ) + FormatOwnedWithRule::new(self, crate::expression::expr_name::FormatExprName) } } @@ -2153,10 +1949,7 @@ impl<'ast> AsFormat> for ast::ExprList { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::expression::expr_list::FormatExprList::default(), - ) + FormatRefWithRule::new(self, crate::expression::expr_list::FormatExprList) } } impl<'ast> IntoFormat> for ast::ExprList { @@ -2166,10 +1959,7 @@ impl<'ast> IntoFormat> for ast::ExprList { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::expression::expr_list::FormatExprList::default(), - ) + FormatOwnedWithRule::new(self, crate::expression::expr_list::FormatExprList) } } @@ -2233,10 +2023,7 @@ impl<'ast> AsFormat> for ast::ExprSlice { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::expression::expr_slice::FormatExprSlice::default(), - ) + FormatRefWithRule::new(self, crate::expression::expr_slice::FormatExprSlice) } } impl<'ast> IntoFormat> for ast::ExprSlice { @@ -2246,49 +2033,48 @@ impl<'ast> IntoFormat> for ast::ExprSlice { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::expression::expr_slice::FormatExprSlice::default(), - ) + FormatOwnedWithRule::new(self, crate::expression::expr_slice::FormatExprSlice) } } -impl FormatRule> - for crate::other::excepthandler_except_handler::FormatExcepthandlerExceptHandler +impl FormatRule> + for crate::other::except_handler_except_handler::FormatExceptHandlerExceptHandler { #[inline] fn fmt( &self, - node: &ast::ExcepthandlerExceptHandler, + node: &ast::ExceptHandlerExceptHandler, f: &mut Formatter>, ) -> FormatResult<()> { - FormatNodeRule::::fmt(self, node, f) + FormatNodeRule::::fmt(self, node, f) } } -impl<'ast> AsFormat> for ast::ExcepthandlerExceptHandler { +impl<'ast> AsFormat> for ast::ExceptHandlerExceptHandler { type Format<'a> = FormatRefWithRule< 'a, - ast::ExcepthandlerExceptHandler, - crate::other::excepthandler_except_handler::FormatExcepthandlerExceptHandler, + ast::ExceptHandlerExceptHandler, + crate::other::except_handler_except_handler::FormatExceptHandlerExceptHandler, PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { FormatRefWithRule::new( self, - crate::other::excepthandler_except_handler::FormatExcepthandlerExceptHandler::default(), + crate::other::except_handler_except_handler::FormatExceptHandlerExceptHandler::default( + ), ) } } -impl<'ast> IntoFormat> for ast::ExcepthandlerExceptHandler { +impl<'ast> IntoFormat> for ast::ExceptHandlerExceptHandler { type Format = FormatOwnedWithRule< - ast::ExcepthandlerExceptHandler, - crate::other::excepthandler_except_handler::FormatExcepthandlerExceptHandler, + ast::ExceptHandlerExceptHandler, + crate::other::except_handler_except_handler::FormatExceptHandlerExceptHandler, PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { FormatOwnedWithRule::new( self, - crate::other::excepthandler_except_handler::FormatExcepthandlerExceptHandler::default(), + crate::other::except_handler_except_handler::FormatExceptHandlerExceptHandler::default( + ), ) } } @@ -2315,7 +2101,7 @@ impl<'ast> AsFormat> for ast::PatternMatchValue { fn format(&self) -> Self::Format<'_> { FormatRefWithRule::new( self, - crate::pattern::pattern_match_value::FormatPatternMatchValue::default(), + crate::pattern::pattern_match_value::FormatPatternMatchValue, ) } } @@ -2328,7 +2114,7 @@ impl<'ast> IntoFormat> for ast::PatternMatchValue { fn into_format(self) -> Self::Format { FormatOwnedWithRule::new( self, - crate::pattern::pattern_match_value::FormatPatternMatchValue::default(), + crate::pattern::pattern_match_value::FormatPatternMatchValue, ) } } @@ -2355,7 +2141,7 @@ impl<'ast> AsFormat> for ast::PatternMatchSingleton { fn format(&self) -> Self::Format<'_> { FormatRefWithRule::new( self, - crate::pattern::pattern_match_singleton::FormatPatternMatchSingleton::default(), + crate::pattern::pattern_match_singleton::FormatPatternMatchSingleton, ) } } @@ -2368,7 +2154,7 @@ impl<'ast> IntoFormat> for ast::PatternMatchSingleton { fn into_format(self) -> Self::Format { FormatOwnedWithRule::new( self, - crate::pattern::pattern_match_singleton::FormatPatternMatchSingleton::default(), + crate::pattern::pattern_match_singleton::FormatPatternMatchSingleton, ) } } @@ -2395,7 +2181,7 @@ impl<'ast> AsFormat> for ast::PatternMatchSequence { fn format(&self) -> Self::Format<'_> { FormatRefWithRule::new( self, - crate::pattern::pattern_match_sequence::FormatPatternMatchSequence::default(), + crate::pattern::pattern_match_sequence::FormatPatternMatchSequence, ) } } @@ -2408,7 +2194,7 @@ impl<'ast> IntoFormat> for ast::PatternMatchSequence { fn into_format(self) -> Self::Format { FormatOwnedWithRule::new( self, - crate::pattern::pattern_match_sequence::FormatPatternMatchSequence::default(), + crate::pattern::pattern_match_sequence::FormatPatternMatchSequence, ) } } @@ -2435,7 +2221,7 @@ impl<'ast> AsFormat> for ast::PatternMatchMapping { fn format(&self) -> Self::Format<'_> { FormatRefWithRule::new( self, - crate::pattern::pattern_match_mapping::FormatPatternMatchMapping::default(), + crate::pattern::pattern_match_mapping::FormatPatternMatchMapping, ) } } @@ -2448,7 +2234,7 @@ impl<'ast> IntoFormat> for ast::PatternMatchMapping { fn into_format(self) -> Self::Format { FormatOwnedWithRule::new( self, - crate::pattern::pattern_match_mapping::FormatPatternMatchMapping::default(), + crate::pattern::pattern_match_mapping::FormatPatternMatchMapping, ) } } @@ -2475,7 +2261,7 @@ impl<'ast> AsFormat> for ast::PatternMatchClass { fn format(&self) -> Self::Format<'_> { FormatRefWithRule::new( self, - crate::pattern::pattern_match_class::FormatPatternMatchClass::default(), + crate::pattern::pattern_match_class::FormatPatternMatchClass, ) } } @@ -2488,7 +2274,7 @@ impl<'ast> IntoFormat> for ast::PatternMatchClass { fn into_format(self) -> Self::Format { FormatOwnedWithRule::new( self, - crate::pattern::pattern_match_class::FormatPatternMatchClass::default(), + crate::pattern::pattern_match_class::FormatPatternMatchClass, ) } } @@ -2515,7 +2301,7 @@ impl<'ast> AsFormat> for ast::PatternMatchStar { fn format(&self) -> Self::Format<'_> { FormatRefWithRule::new( self, - crate::pattern::pattern_match_star::FormatPatternMatchStar::default(), + crate::pattern::pattern_match_star::FormatPatternMatchStar, ) } } @@ -2528,7 +2314,7 @@ impl<'ast> IntoFormat> for ast::PatternMatchStar { fn into_format(self) -> Self::Format { FormatOwnedWithRule::new( self, - crate::pattern::pattern_match_star::FormatPatternMatchStar::default(), + crate::pattern::pattern_match_star::FormatPatternMatchStar, ) } } @@ -2553,10 +2339,7 @@ impl<'ast> AsFormat> for ast::PatternMatchAs { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::pattern::pattern_match_as::FormatPatternMatchAs::default(), - ) + FormatRefWithRule::new(self, crate::pattern::pattern_match_as::FormatPatternMatchAs) } } impl<'ast> IntoFormat> for ast::PatternMatchAs { @@ -2566,10 +2349,7 @@ impl<'ast> IntoFormat> for ast::PatternMatchAs { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::pattern::pattern_match_as::FormatPatternMatchAs::default(), - ) + FormatOwnedWithRule::new(self, crate::pattern::pattern_match_as::FormatPatternMatchAs) } } @@ -2593,10 +2373,7 @@ impl<'ast> AsFormat> for ast::PatternMatchOr { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::pattern::pattern_match_or::FormatPatternMatchOr::default(), - ) + FormatRefWithRule::new(self, crate::pattern::pattern_match_or::FormatPatternMatchOr) } } impl<'ast> IntoFormat> for ast::PatternMatchOr { @@ -2606,10 +2383,7 @@ impl<'ast> IntoFormat> for ast::PatternMatchOr { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::pattern::pattern_match_or::FormatPatternMatchOr::default(), - ) + FormatOwnedWithRule::new(self, crate::pattern::pattern_match_or::FormatPatternMatchOr) } } @@ -2635,7 +2409,7 @@ impl<'ast> AsFormat> for ast::TypeIgnoreTypeIgnore { fn format(&self) -> Self::Format<'_> { FormatRefWithRule::new( self, - crate::other::type_ignore_type_ignore::FormatTypeIgnoreTypeIgnore::default(), + crate::other::type_ignore_type_ignore::FormatTypeIgnoreTypeIgnore, ) } } @@ -2648,7 +2422,7 @@ impl<'ast> IntoFormat> for ast::TypeIgnoreTypeIgnore { fn into_format(self) -> Self::Format { FormatOwnedWithRule::new( self, - crate::other::type_ignore_type_ignore::FormatTypeIgnoreTypeIgnore::default(), + crate::other::type_ignore_type_ignore::FormatTypeIgnoreTypeIgnore, ) } } @@ -2673,10 +2447,7 @@ impl<'ast> AsFormat> for ast::Comprehension { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new( - self, - crate::other::comprehension::FormatComprehension::default(), - ) + FormatRefWithRule::new(self, crate::other::comprehension::FormatComprehension) } } impl<'ast> IntoFormat> for ast::Comprehension { @@ -2686,10 +2457,7 @@ impl<'ast> IntoFormat> for ast::Comprehension { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new( - self, - crate::other::comprehension::FormatComprehension::default(), - ) + FormatOwnedWithRule::new(self, crate::other::comprehension::FormatComprehension) } } @@ -2711,7 +2479,7 @@ impl<'ast> AsFormat> for ast::Arguments { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new(self, crate::other::arguments::FormatArguments::default()) + FormatRefWithRule::new(self, crate::other::arguments::FormatArguments) } } impl<'ast> IntoFormat> for ast::Arguments { @@ -2721,7 +2489,7 @@ impl<'ast> IntoFormat> for ast::Arguments { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new(self, crate::other::arguments::FormatArguments::default()) + FormatOwnedWithRule::new(self, crate::other::arguments::FormatArguments) } } @@ -2735,14 +2503,48 @@ impl<'ast> AsFormat> for ast::Arg { type Format<'a> = FormatRefWithRule<'a, ast::Arg, crate::other::arg::FormatArg, PyFormatContext<'ast>>; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new(self, crate::other::arg::FormatArg::default()) + FormatRefWithRule::new(self, crate::other::arg::FormatArg) } } impl<'ast> IntoFormat> for ast::Arg { type Format = FormatOwnedWithRule>; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new(self, crate::other::arg::FormatArg::default()) + FormatOwnedWithRule::new(self, crate::other::arg::FormatArg) + } +} + +impl FormatRule> + for crate::other::arg_with_default::FormatArgWithDefault +{ + #[inline] + fn fmt( + &self, + node: &ast::ArgWithDefault, + f: &mut Formatter>, + ) -> FormatResult<()> { + FormatNodeRule::::fmt(self, node, f) + } +} +impl<'ast> AsFormat> for ast::ArgWithDefault { + type Format<'a> = FormatRefWithRule< + 'a, + ast::ArgWithDefault, + crate::other::arg_with_default::FormatArgWithDefault, + PyFormatContext<'ast>, + >; + fn format(&self) -> Self::Format<'_> { + FormatRefWithRule::new(self, crate::other::arg_with_default::FormatArgWithDefault) + } +} +impl<'ast> IntoFormat> for ast::ArgWithDefault { + type Format = FormatOwnedWithRule< + ast::ArgWithDefault, + crate::other::arg_with_default::FormatArgWithDefault, + PyFormatContext<'ast>, + >; + fn into_format(self) -> Self::Format { + FormatOwnedWithRule::new(self, crate::other::arg_with_default::FormatArgWithDefault) } } @@ -2760,7 +2562,7 @@ impl<'ast> AsFormat> for ast::Keyword { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new(self, crate::other::keyword::FormatKeyword::default()) + FormatRefWithRule::new(self, crate::other::keyword::FormatKeyword) } } impl<'ast> IntoFormat> for ast::Keyword { @@ -2770,7 +2572,7 @@ impl<'ast> IntoFormat> for ast::Keyword { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new(self, crate::other::keyword::FormatKeyword::default()) + FormatOwnedWithRule::new(self, crate::other::keyword::FormatKeyword) } } @@ -2784,46 +2586,46 @@ impl<'ast> AsFormat> for ast::Alias { type Format<'a> = FormatRefWithRule<'a, ast::Alias, crate::other::alias::FormatAlias, PyFormatContext<'ast>>; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new(self, crate::other::alias::FormatAlias::default()) + FormatRefWithRule::new(self, crate::other::alias::FormatAlias) } } impl<'ast> IntoFormat> for ast::Alias { type Format = FormatOwnedWithRule>; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new(self, crate::other::alias::FormatAlias::default()) + FormatOwnedWithRule::new(self, crate::other::alias::FormatAlias) } } -impl FormatRule> for crate::other::withitem::FormatWithitem { +impl FormatRule> for crate::other::with_item::FormatWithItem { #[inline] fn fmt( &self, - node: &ast::Withitem, + node: &ast::WithItem, f: &mut Formatter>, ) -> FormatResult<()> { - FormatNodeRule::::fmt(self, node, f) + FormatNodeRule::::fmt(self, node, f) } } -impl<'ast> AsFormat> for ast::Withitem { +impl<'ast> AsFormat> for ast::WithItem { type Format<'a> = FormatRefWithRule< 'a, - ast::Withitem, - crate::other::withitem::FormatWithitem, + ast::WithItem, + crate::other::with_item::FormatWithItem, PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new(self, crate::other::withitem::FormatWithitem::default()) + FormatRefWithRule::new(self, crate::other::with_item::FormatWithItem) } } -impl<'ast> IntoFormat> for ast::Withitem { +impl<'ast> IntoFormat> for ast::WithItem { type Format = FormatOwnedWithRule< - ast::Withitem, - crate::other::withitem::FormatWithitem, + ast::WithItem, + crate::other::with_item::FormatWithItem, PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new(self, crate::other::withitem::FormatWithitem::default()) + FormatOwnedWithRule::new(self, crate::other::with_item::FormatWithItem) } } @@ -2845,7 +2647,7 @@ impl<'ast> AsFormat> for ast::MatchCase { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new(self, crate::other::match_case::FormatMatchCase::default()) + FormatRefWithRule::new(self, crate::other::match_case::FormatMatchCase) } } impl<'ast> IntoFormat> for ast::MatchCase { @@ -2855,7 +2657,7 @@ impl<'ast> IntoFormat> for ast::MatchCase { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new(self, crate::other::match_case::FormatMatchCase::default()) + FormatOwnedWithRule::new(self, crate::other::match_case::FormatMatchCase) } } @@ -2877,7 +2679,7 @@ impl<'ast> AsFormat> for ast::Decorator { PyFormatContext<'ast>, >; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new(self, crate::other::decorator::FormatDecorator::default()) + FormatRefWithRule::new(self, crate::other::decorator::FormatDecorator) } } impl<'ast> IntoFormat> for ast::Decorator { @@ -2887,6 +2689,6 @@ impl<'ast> IntoFormat> for ast::Decorator { PyFormatContext<'ast>, >; fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new(self, crate::other::decorator::FormatDecorator::default()) + FormatOwnedWithRule::new(self, crate::other::decorator::FormatDecorator) } } diff --git a/crates/ruff_python_formatter/src/lib.rs b/crates/ruff_python_formatter/src/lib.rs index 51d03c375b..44ab2e8de4 100644 --- a/crates/ruff_python_formatter/src/lib.rs +++ b/crates/ruff_python_formatter/src/lib.rs @@ -1,19 +1,25 @@ -use anyhow::{anyhow, Context, Result}; -use ruff_formatter::prelude::*; -use ruff_formatter::{format, write}; -use ruff_formatter::{Formatted, IndentStyle, Printed, SimpleFormatOptions, SourceCode}; -use ruff_python_ast::node::{AnyNodeRef, AstNode, NodeKind}; -use ruff_python_ast::source_code::{CommentRanges, CommentRangesBuilder, Locator}; -use ruff_text_size::{TextLen, TextRange}; -use rustpython_parser::ast::{Mod, Ranged}; -use rustpython_parser::lexer::lex; -use rustpython_parser::{parse_tokens, Mode}; -use std::borrow::Cow; - use crate::comments::{ dangling_node_comments, leading_node_comments, trailing_node_comments, Comments, }; use crate::context::PyFormatContext; +pub use crate::options::{MagicTrailingComma, PyFormatOptions, QuoteStyle}; +use ruff_formatter::format_element::tag; +use ruff_formatter::prelude::{ + dynamic_text, source_position, source_text_slice, text, ContainsNewlines, Formatter, Tag, +}; +use ruff_formatter::{ + format, normalize_newlines, write, Buffer, Format, FormatElement, FormatError, FormatResult, + PrintError, +}; +use ruff_formatter::{Formatted, Printed, SourceCode}; +use ruff_python_ast::node::{AnyNodeRef, AstNode, NodeKind}; +use ruff_python_ast::source_code::{CommentRanges, CommentRangesBuilder, Locator}; +use ruff_text_size::{TextLen, TextRange}; +use rustpython_parser::ast::{Mod, Ranged}; +use rustpython_parser::lexer::{lex, LexicalError}; +use rustpython_parser::{parse_tokens, Mode, ParseError}; +use std::borrow::Cow; +use thiserror::Error; pub(crate) mod builders; pub mod cli; @@ -22,6 +28,7 @@ pub(crate) mod context; pub(crate) mod expression; mod generated; pub(crate) mod module; +mod options; pub(crate) mod other; pub(crate) mod pattern; mod prelude; @@ -69,7 +76,7 @@ where /// default implementation formats the dangling comments at the end of the node, which isn't ideal but ensures that /// no comments are dropped. /// - /// A node can have dangling comments if all its children are tokens or if all node childrens are optional. + /// A node can have dangling comments if all its children are tokens or if all node children are optional. fn fmt_dangling_comments(&self, node: &N, f: &mut PyFormatter) -> FormatResult<()> { dangling_node_comments(node).fmt(f) } @@ -83,16 +90,40 @@ where } } -pub fn format_module(contents: &str) -> Result { +#[derive(Error, Debug)] +pub enum FormatModuleError { + #[error("source contains syntax errors (lexer error): {0:?}")] + LexError(LexicalError), + #[error("source contains syntax errors (parser error): {0:?}")] + ParseError(ParseError), + #[error(transparent)] + FormatError(#[from] FormatError), + #[error(transparent)] + PrintError(#[from] PrintError), +} + +impl From for FormatModuleError { + fn from(value: LexicalError) -> Self { + Self::LexError(value) + } +} + +impl From for FormatModuleError { + fn from(value: ParseError) -> Self { + Self::ParseError(value) + } +} + +pub fn format_module( + contents: &str, + options: PyFormatOptions, +) -> Result { // Tokenize once let mut tokens = Vec::new(); let mut comment_ranges = CommentRangesBuilder::default(); for result in lex(contents, Mode::Module) { - let (token, range) = match result { - Ok((token, range)) => (token, range), - Err(err) => return Err(anyhow!("Source contains syntax errors {err:?}")), - }; + let (token, range) = result?; comment_ranges.visit_token(&token, range); tokens.push(Ok((token, range))); @@ -101,34 +132,25 @@ pub fn format_module(contents: &str) -> Result { let comment_ranges = comment_ranges.finish(); // Parse the AST. - let python_ast = parse_tokens(tokens, Mode::Module, "") - .with_context(|| "Syntax error in input")?; + let python_ast = parse_tokens(tokens, Mode::Module, "")?; - let formatted = format_node(&python_ast, &comment_ranges, contents)?; + let formatted = format_node(&python_ast, &comment_ranges, contents, options)?; - formatted - .print() - .with_context(|| "Failed to print the formatter IR") + Ok(formatted.print()?) } pub fn format_node<'a>( root: &'a Mod, comment_ranges: &'a CommentRanges, source: &'a str, + options: PyFormatOptions, ) -> FormatResult>> { let comments = Comments::from_ast(root, SourceCode::new(source), comment_ranges); let locator = Locator::new(source); format!( - PyFormatContext::new( - SimpleFormatOptions { - indent_style: IndentStyle::Space(4), - line_width: 88.try_into().unwrap(), - }, - locator.contents(), - comments, - ), + PyFormatContext::new(options, locator.contents(), comments), [root.format()] ) } @@ -225,18 +247,12 @@ impl Format> for VerbatimText { #[cfg(test)] mod tests { + use crate::{format_module, format_node, PyFormatOptions}; use anyhow::Result; use insta::assert_snapshot; use ruff_python_ast::source_code::CommentRangesBuilder; - use ruff_testing_macros::fixture; use rustpython_parser::lexer::lex; use rustpython_parser::{parse_tokens, Mode}; - use similar::TextDiff; - use std::fmt::{Formatter, Write}; - use std::fs; - use std::path::Path; - - use crate::{format_module, format_node}; /// Very basic test intentionally kept very similar to the CLI #[test] @@ -244,174 +260,36 @@ mod tests { let input = r#" # preceding if True: - print( "hi" ) + pass # trailing "#; let expected = r#"# preceding if True: - NOT_IMPLEMENTED_call() + pass # trailing "#; - let actual = format_module(input)?.as_code().to_string(); + let actual = format_module(input, PyFormatOptions::default())? + .as_code() + .to_string(); assert_eq!(expected, actual); Ok(()) } - #[fixture(pattern = "resources/test/fixtures/black/**/*.py")] - #[test] - fn black_test(input_path: &Path) -> Result<()> { - let content = fs::read_to_string(input_path)?; - - let printed = format_module(&content)?; - - let expected_path = input_path.with_extension("py.expect"); - let expected_output = fs::read_to_string(&expected_path) - .unwrap_or_else(|_| panic!("Expected Black output file '{expected_path:?}' to exist")); - - let formatted_code = printed.as_code(); - - let reformatted = match format_module(formatted_code) { - Ok(reformatted) => reformatted, - Err(err) => { - panic!( - "Expected formatted code to be valid syntax: {err}:\ - \n---\n{formatted_code}---\n", - ); - } - }; - - if reformatted.as_code() != formatted_code { - let diff = TextDiff::from_lines(formatted_code, reformatted.as_code()) - .unified_diff() - .header("Formatted once", "Formatted twice") - .to_string(); - panic!( - r#"Reformatting the formatted code a second time resulted in formatting changes. ---- -{diff}--- - -Formatted once: ---- -{formatted_code}--- - -Formatted twice: ---- -{}---"#, - reformatted.as_code() - ); - } - - if formatted_code == expected_output { - // Black and Ruff formatting matches. Delete any existing snapshot files because the Black output - // already perfectly captures the expected output. - // The following code mimics insta's logic generating the snapshot name for a test. - let workspace_path = std::env::var("CARGO_MANIFEST_DIR").unwrap(); - let snapshot_name = insta::_function_name!() - .strip_prefix(&format!("{}::", module_path!())) - .unwrap(); - let module_path = module_path!().replace("::", "__"); - - let snapshot_path = Path::new(&workspace_path) - .join("src/snapshots") - .join(format!( - "{module_path}__{}.snap", - snapshot_name.replace(&['/', '\\'][..], "__") - )); - - if snapshot_path.exists() && snapshot_path.is_file() { - // SAFETY: This is a convenience feature. That's why we don't want to abort - // when deleting a no longer needed snapshot fails. - fs::remove_file(&snapshot_path).ok(); - } - - let new_snapshot_path = snapshot_path.with_extension("snap.new"); - if new_snapshot_path.exists() && new_snapshot_path.is_file() { - // SAFETY: This is a convenience feature. That's why we don't want to abort - // when deleting a no longer needed snapshot fails. - fs::remove_file(&new_snapshot_path).ok(); - } - } else { - // Black and Ruff have different formatting. Write out a snapshot that covers the differences - // today. - let mut snapshot = String::new(); - write!(snapshot, "{}", Header::new("Input"))?; - write!(snapshot, "{}", CodeFrame::new("py", &content))?; - - write!(snapshot, "{}", Header::new("Black Differences"))?; - - let diff = TextDiff::from_lines(expected_output.as_str(), formatted_code) - .unified_diff() - .header("Black", "Ruff") - .to_string(); - - write!(snapshot, "{}", CodeFrame::new("diff", &diff))?; - - write!(snapshot, "{}", Header::new("Ruff Output"))?; - write!(snapshot, "{}", CodeFrame::new("py", formatted_code))?; - - write!(snapshot, "{}", Header::new("Black Output"))?; - write!(snapshot, "{}", CodeFrame::new("py", &expected_output))?; - - insta::with_settings!({ omit_expression => false, input_file => input_path }, { - insta::assert_snapshot!(snapshot); - }); - } - - Ok(()) - } - - #[fixture(pattern = "resources/test/fixtures/ruff/**/*.py")] - #[test] - fn ruff_test(input_path: &Path) -> Result<()> { - let content = fs::read_to_string(input_path)?; - - let printed = format_module(&content)?; - let formatted_code = printed.as_code(); - - let reformatted = - format_module(formatted_code).unwrap_or_else(|err| panic!("Expected formatted code to be valid syntax but it contains syntax errors: {err}\n{formatted_code}")); - - if reformatted.as_code() != formatted_code { - let diff = TextDiff::from_lines(formatted_code, reformatted.as_code()) - .unified_diff() - .header("Formatted once", "Formatted twice") - .to_string(); - panic!( - r#"Reformatting the formatted code a second time resulted in formatting changes. -{diff} - -Formatted once: -{formatted_code} - -Formatted twice: -{}"#, - reformatted.as_code() - ); - } - - let snapshot = format!( - r#"## Input -{} - -## Output -{}"#, - CodeFrame::new("py", &content), - CodeFrame::new("py", formatted_code) - ); - assert_snapshot!(snapshot); - - Ok(()) - } - /// Use this test to debug the formatting of some snipped #[ignore] #[test] fn quick_test() { let src = r#" -def test(): ... +if a * [ + bbbbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccccccccccccdddddddddddddddddddddddddd, +] + a * e * [ + ffff, + gggg, + hhhhhhhhhhhhhh, +] * c: + pass -# Comment -def with_leading_comment(): ... "#; // Tokenize once let mut tokens = Vec::new(); @@ -428,15 +306,22 @@ def with_leading_comment(): ... // Parse the AST. let python_ast = parse_tokens(tokens, Mode::Module, "").unwrap(); - let formatted = format_node(&python_ast, &comment_ranges, src).unwrap(); + let formatted = format_node( + &python_ast, + &comment_ranges, + src, + PyFormatOptions::default(), + ) + .unwrap(); // Uncomment the `dbg` to print the IR. // Use `dbg_write!(f, []) instead of `write!(f, [])` in your formatting code to print some IR // inside of a `Format` implementation + // use ruff_formatter::FormatContext; // dbg!(formatted // .document() // .display(formatted.context().source_code())); - + // // dbg!(formatted // .context() // .comments() @@ -523,41 +408,4 @@ def with_leading_comment(): ... assert_snapshot!(output.print().expect("Printing to succeed").as_code()); } - - struct Header<'a> { - title: &'a str, - } - - impl<'a> Header<'a> { - fn new(title: &'a str) -> Self { - Self { title } - } - } - - impl std::fmt::Display for Header<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - writeln!(f, "## {}", self.title)?; - writeln!(f) - } - } - - struct CodeFrame<'a> { - language: &'a str, - code: &'a str, - } - - impl<'a> CodeFrame<'a> { - fn new(language: &'a str, code: &'a str) -> Self { - Self { language, code } - } - } - - impl std::fmt::Display for CodeFrame<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - writeln!(f, "```{}", self.language)?; - write!(f, "{}", self.code)?; - writeln!(f, "```")?; - writeln!(f) - } - } } diff --git a/crates/ruff_python_formatter/src/module/mod.rs b/crates/ruff_python_formatter/src/module/mod.rs index 6036af3942..0638d4907c 100644 --- a/crates/ruff_python_formatter/src/module/mod.rs +++ b/crates/ruff_python_formatter/src/module/mod.rs @@ -24,14 +24,16 @@ impl FormatRule> for FormatMod { impl<'ast> AsFormat> for Mod { type Format<'a> = FormatRefWithRule<'a, Mod, FormatMod, PyFormatContext<'ast>>; + fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new(self, FormatMod::default()) + FormatRefWithRule::new(self, FormatMod) } } impl<'ast> IntoFormat> for Mod { type Format = FormatOwnedWithRule>; + fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new(self, FormatMod::default()) + FormatOwnedWithRule::new(self, FormatMod) } } diff --git a/crates/ruff_python_formatter/src/module/mod_expression.rs b/crates/ruff_python_formatter/src/module/mod_expression.rs index d7a8c40db6..f6e49fb696 100644 --- a/crates/ruff_python_formatter/src/module/mod_expression.rs +++ b/crates/ruff_python_formatter/src/module/mod_expression.rs @@ -1,5 +1,5 @@ -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; +use crate::{AsFormat, FormatNodeRule, PyFormatter}; +use ruff_formatter::{Format, FormatResult}; use rustpython_parser::ast::ModExpression; #[derive(Default)] @@ -7,6 +7,7 @@ pub struct FormatModExpression; impl FormatNodeRule for FormatModExpression { fn fmt_fields(&self, item: &ModExpression, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + let ModExpression { body, range: _ } = item; + body.format().fmt(f) } } diff --git a/crates/ruff_python_formatter/src/options.rs b/crates/ruff_python_formatter/src/options.rs new file mode 100644 index 0000000000..65bcf65552 --- /dev/null +++ b/crates/ruff_python_formatter/src/options.rs @@ -0,0 +1,148 @@ +use ruff_formatter::printer::{LineEnding, PrinterOptions}; +use ruff_formatter::{FormatOptions, IndentStyle, LineWidth}; + +#[derive(Clone, Debug)] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(default) +)] +pub struct PyFormatOptions { + /// Specifies the indent style: + /// * Either a tab + /// * or a specific amount of spaces + indent_style: IndentStyle, + + /// The preferred line width at which the formatter should wrap lines. + #[cfg_attr(feature = "serde", serde(default = "default_line_width"))] + line_width: LineWidth, + + /// The preferred quote style to use (single vs double quotes). + quote_style: QuoteStyle, + + /// Whether to expand lists or elements if they have a trailing comma such as `(a, b,)` + magic_trailing_comma: MagicTrailingComma, +} + +fn default_line_width() -> LineWidth { + LineWidth::try_from(88).unwrap() +} + +impl PyFormatOptions { + pub fn magic_trailing_comma(&self) -> MagicTrailingComma { + self.magic_trailing_comma + } + + pub fn quote_style(&self) -> QuoteStyle { + self.quote_style + } + + pub fn with_quote_style(&mut self, style: QuoteStyle) -> &mut Self { + self.quote_style = style; + self + } + + pub fn with_magic_trailing_comma(&mut self, trailing_comma: MagicTrailingComma) -> &mut Self { + self.magic_trailing_comma = trailing_comma; + self + } + + pub fn with_indent_style(&mut self, indent_style: IndentStyle) -> &mut Self { + self.indent_style = indent_style; + self + } + + pub fn with_line_width(&mut self, line_width: LineWidth) -> &mut Self { + self.line_width = line_width; + self + } +} + +impl FormatOptions for PyFormatOptions { + fn indent_style(&self) -> IndentStyle { + self.indent_style + } + + fn line_width(&self) -> LineWidth { + self.line_width + } + + fn as_print_options(&self) -> PrinterOptions { + PrinterOptions { + tab_width: 4, + print_width: self.line_width.into(), + line_ending: LineEnding::LineFeed, + indent_style: self.indent_style, + } + } +} + +impl Default for PyFormatOptions { + fn default() -> Self { + Self { + indent_style: IndentStyle::Space(4), + line_width: LineWidth::try_from(88).unwrap(), + quote_style: QuoteStyle::default(), + magic_trailing_comma: MagicTrailingComma::default(), + } + } +} + +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "kebab-case") +)] +pub enum QuoteStyle { + Single, + #[default] + Double, +} + +impl QuoteStyle { + pub const fn as_char(self) -> char { + match self { + QuoteStyle::Single => '\'', + QuoteStyle::Double => '"', + } + } + + #[must_use] + pub const fn invert(self) -> QuoteStyle { + match self { + QuoteStyle::Single => QuoteStyle::Double, + QuoteStyle::Double => QuoteStyle::Single, + } + } +} + +impl TryFrom for QuoteStyle { + type Error = (); + + fn try_from(value: char) -> std::result::Result { + match value { + '\'' => Ok(QuoteStyle::Single), + '"' => Ok(QuoteStyle::Double), + _ => Err(()), + } + } +} + +#[derive(Copy, Clone, Debug, Default)] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "kebab-case") +)] +pub enum MagicTrailingComma { + #[default] + Respect, + Ignore, +} + +impl MagicTrailingComma { + pub const fn is_respect(self) -> bool { + matches!(self, Self::Respect) + } +} diff --git a/crates/ruff_python_formatter/src/other/alias.rs b/crates/ruff_python_formatter/src/other/alias.rs index 8a1501e09c..f59dd012bd 100644 --- a/crates/ruff_python_formatter/src/other/alias.rs +++ b/crates/ruff_python_formatter/src/other/alias.rs @@ -1,5 +1,6 @@ -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; +use crate::{AsFormat, FormatNodeRule, PyFormatter}; +use ruff_formatter::prelude::{space, text}; +use ruff_formatter::{write, Buffer, Format, FormatResult}; use rustpython_parser::ast::Alias; #[derive(Default)] @@ -7,6 +8,15 @@ pub struct FormatAlias; impl FormatNodeRule for FormatAlias { fn fmt_fields(&self, item: &Alias, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + let Alias { + range: _, + name, + asname, + } = item; + name.format().fmt(f)?; + if let Some(asname) = asname { + write!(f, [space(), text("as"), space(), asname.format()])?; + } + Ok(()) } } diff --git a/crates/ruff_python_formatter/src/other/arg.rs b/crates/ruff_python_formatter/src/other/arg.rs index a0eebd3a96..3974223faa 100644 --- a/crates/ruff_python_formatter/src/other/arg.rs +++ b/crates/ruff_python_formatter/src/other/arg.rs @@ -1,7 +1,6 @@ use crate::prelude::*; use crate::FormatNodeRule; use ruff_formatter::write; -use ruff_text_size::{TextLen, TextRange}; use rustpython_parser::ast::Arg; #[derive(Default)] @@ -10,21 +9,13 @@ pub struct FormatArg; impl FormatNodeRule for FormatArg { fn fmt_fields(&self, item: &Arg, f: &mut PyFormatter) -> FormatResult<()> { let Arg { - range, + range: _, arg, annotation, type_comment: _, } = item; - write!( - f, - [ - // The name of the argument - source_text_slice( - TextRange::at(range.start(), arg.text_len()), - ContainsNewlines::No - ) - ] - )?; + + arg.format().fmt(f)?; if let Some(annotation) = annotation { write!(f, [text(":"), space(), annotation.format()])?; diff --git a/crates/ruff_python_formatter/src/other/arg_with_default.rs b/crates/ruff_python_formatter/src/other/arg_with_default.rs new file mode 100644 index 0000000000..c6badb551a --- /dev/null +++ b/crates/ruff_python_formatter/src/other/arg_with_default.rs @@ -0,0 +1,27 @@ +use ruff_formatter::write; +use rustpython_parser::ast::ArgWithDefault; + +use crate::prelude::*; +use crate::FormatNodeRule; + +#[derive(Default)] +pub struct FormatArgWithDefault; + +impl FormatNodeRule for FormatArgWithDefault { + fn fmt_fields(&self, item: &ArgWithDefault, f: &mut PyFormatter) -> FormatResult<()> { + let ArgWithDefault { + range: _, + def, + default, + } = item; + + write!(f, [def.format()])?; + + if let Some(default) = default { + let space = def.annotation.is_some().then_some(space()); + write!(f, [space, text("="), space, group(&default.format())])?; + } + + Ok(()) + } +} diff --git a/crates/ruff_python_formatter/src/other/arguments.rs b/crates/ruff_python_formatter/src/other/arguments.rs index 19f8107f00..3e84558ad2 100644 --- a/crates/ruff_python_formatter/src/other/arguments.rs +++ b/crates/ruff_python_formatter/src/other/arguments.rs @@ -1,12 +1,20 @@ -use crate::comments::{dangling_node_comments, leading_node_comments}; +use std::usize; + +use rustpython_parser::ast::{Arguments, Ranged}; + +use ruff_formatter::{format_args, write}; +use ruff_python_ast::node::{AnyNodeRef, AstNode}; + +use crate::comments::{ + dangling_comments, leading_comments, leading_node_comments, trailing_comments, + CommentLinePosition, SourceComment, +}; use crate::context::NodeLevel; +use crate::expression::parentheses::parenthesized; use crate::prelude::*; use crate::trivia::{first_non_trivia_token, SimpleTokenizer, Token, TokenKind}; use crate::FormatNodeRule; -use ruff_formatter::{format_args, write, FormatError}; -use ruff_python_ast::node::{AnyNodeRef, AstNode}; -use rustpython_parser::ast::{Arg, Arguments, Expr, Ranged}; -use std::usize; +use ruff_text_size::{TextRange, TextSize}; #[derive(Default)] pub struct FormatArguments; @@ -17,47 +25,63 @@ impl FormatNodeRule for FormatArguments { range: _, posonlyargs, args, - defaults, vararg, kwonlyargs, - kw_defaults, kwarg, } = item; let saved_level = f.context().node_level(); - f.context_mut().set_node_level(NodeLevel::Expression); + f.context_mut() + .set_node_level(NodeLevel::ParenthesizedExpression); + + let comments = f.context().comments().clone(); + let dangling = comments.dangling_comments(item); + let (slash, star) = find_argument_separators(f.context().source(), item); let format_inner = format_with(|f: &mut PyFormatter| { let separator = format_with(|f| write!(f, [text(","), soft_line_break_or_space()])); let mut joiner = f.join_with(separator); let mut last_node: Option = None; - let mut defaults = std::iter::repeat(None) - .take(posonlyargs.len() + args.len() - defaults.len()) - .chain(defaults.iter().map(Some)); + for arg_with_default in posonlyargs { + joiner.entry(&arg_with_default.format()); - for positional in posonlyargs { - let default = defaults.next().ok_or(FormatError::SyntaxError)?; - joiner.entry(&ArgumentWithDefault { - argument: positional, - default, + last_node = Some(arg_with_default.into()); + } + + let slash_comments_end = if posonlyargs.is_empty() { + 0 + } else { + let slash_comments_end = dangling.partition_point(|comment| { + let assignment = assign_argument_separator_comment_placement( + slash.as_ref(), + star.as_ref(), + comment.slice().range(), + comment.line_position(), + ) + .expect("Unexpected dangling comment type in function arguments"); + matches!( + assignment, + ArgumentSeparatorCommentLocation::SlashLeading + | ArgumentSeparatorCommentLocation::SlashTrailing + ) }); + joiner.entry(&CommentsAroundText { + text: "/", + comments: &dangling[..slash_comments_end], + }); + slash_comments_end + }; - last_node = Some(default.map_or_else(|| positional.into(), AnyNodeRef::from)); - } - - if !posonlyargs.is_empty() { - joiner.entry(&text("/")); - } - - for argument in args { - let default = defaults.next().ok_or(FormatError::SyntaxError)?; - - joiner.entry(&ArgumentWithDefault { argument, default }); - - last_node = Some(default.map_or_else(|| argument.into(), AnyNodeRef::from)); + for arg_with_default in args { + joiner.entry(&arg_with_default.format()); + + last_node = Some(arg_with_default.into()); } + // kw only args need either a `*args` ahead of them capturing all var args or a `*` + // pseudo-argument capturing all fields. We can also have `*args` without any kwargs + // afterwards. if let Some(vararg) = vararg { joiner.entry(&format_args![ leading_node_comments(vararg.as_ref()), @@ -65,25 +89,34 @@ impl FormatNodeRule for FormatArguments { vararg.format() ]); last_node = Some(vararg.as_any_node_ref()); - } - - debug_assert!(defaults.next().is_none()); - - let mut defaults = std::iter::repeat(None) - .take(kwonlyargs.len() - kw_defaults.len()) - .chain(kw_defaults.iter().map(Some)); - - for keyword_argument in kwonlyargs { - let default = defaults.next().ok_or(FormatError::SyntaxError)?; - joiner.entry(&ArgumentWithDefault { - argument: keyword_argument, - default, + } else if !kwonlyargs.is_empty() { + // Given very strange comment placement, comments here may not actually have been + // marked as `StarLeading`/`StarTrailing`, but that's fine since we still produce + // a stable formatting in this case + // ```python + // def f42( + // a, + // / # 1 + // # 2 + // , # 3 + // # 4 + // * # 5 + // , # 6 + // c, + // ): + // pass + // ``` + joiner.entry(&CommentsAroundText { + text: "*", + comments: &dangling[slash_comments_end..], }); - - last_node = Some(default.map_or_else(|| keyword_argument.into(), AnyNodeRef::from)); } - debug_assert!(defaults.next().is_none()); + for arg_with_default in kwonlyargs { + joiner.entry(&arg_with_default.format()); + + last_node = Some(arg_with_default.into()); + } if let Some(kwarg) = kwarg { joiner.entry(&format_args![ @@ -109,7 +142,7 @@ impl FormatNodeRule for FormatArguments { let maybe_comma_token = if ends_with_pos_only_argument_separator { // `def a(b, c, /): ... ` let mut tokens = - SimpleTokenizer::starts_at(last_node.end(), f.context().contents()) + SimpleTokenizer::starts_at(last_node.end(), f.context().source()) .skip_trivia(); let comma = tokens.next(); @@ -120,7 +153,7 @@ impl FormatNodeRule for FormatArguments { tokens.next() } else { - first_non_trivia_token(last_node.end(), f.context().contents()) + first_non_trivia_token(last_node.end(), f.context().source()) }; if maybe_comma_token.map_or(false, |token| token.kind() == TokenKind::Comma) { @@ -143,19 +176,12 @@ impl FormatNodeRule for FormatArguments { f, [ text("("), - block_indent(&dangling_node_comments(item)), + block_indent(&dangling_comments(dangling)), text(")") ] )?; } else { - write!( - f, - [group(&format_args!( - text("("), - soft_block_indent(&group(&format_inner)), - text(")") - ))] - )?; + parenthesized("(", &group(&format_inner), ")").fmt(f)?; } f.context_mut().set_node_level(saved_level); @@ -169,20 +195,366 @@ impl FormatNodeRule for FormatArguments { } } -struct ArgumentWithDefault<'a> { - argument: &'a Arg, - default: Option<&'a Expr>, +struct CommentsAroundText<'a> { + text: &'static str, + comments: &'a [SourceComment], } -impl Format> for ArgumentWithDefault<'_> { +impl Format> for CommentsAroundText<'_> { fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { - write!(f, [self.argument.format()])?; - - if let Some(default) = self.default { - let space = self.argument.annotation.is_some().then_some(space()); - write!(f, [space, text("="), space, default.format()])?; + if self.comments.is_empty() { + text(self.text).fmt(f) + } else { + // There might be own line comments in trailing, but those are weird and we can kinda + // ignore them + // ```python + // def f42( + // a, + // # leading comment (own line) + // / # first trailing comment (end-of-line) + // # trailing own line comment + // , + // c, + // ): + // ``` + let (leading, trailing) = self.comments.split_at( + self.comments + .partition_point(|comment| comment.line_position().is_own_line()), + ); + write!( + f, + [ + leading_comments(leading), + text(self.text), + trailing_comments(trailing) + ] + ) } - - Ok(()) } } + +/// `/` and `*` in a function signature +/// +/// ```text +/// def f(arg_a, /, arg_b, *, arg_c): pass +/// ^ ^ ^ ^ ^ ^ slash preceding end +/// ^ ^ ^ ^ ^ slash (a separator) +/// ^ ^ ^ ^ slash following start +/// ^ ^ ^ star preceding end +/// ^ ^ star (a separator) +/// ^ star following start +/// ``` +#[derive(Debug)] +pub(crate) struct ArgumentSeparator { + /// The end of the last node or separator before this separator + pub(crate) preceding_end: TextSize, + /// The range of the separator itself + pub(crate) separator: TextRange, + /// The start of the first node or separator following this separator + pub(crate) following_start: TextSize, +} + +/// Finds slash and star in `f(a, /, b, *, c)` +/// +/// Returns slash and star +pub(crate) fn find_argument_separators( + contents: &str, + arguments: &Arguments, +) -> (Option, Option) { + // We only compute preceding_end and token location here since following_start depends on the + // star location, but the star location depends on slash's position + let slash = if let Some(preceding_end) = arguments.posonlyargs.last().map(Ranged::end) { + // ```text + // def f(a1=1, a2=2, /, a3, a4): pass + // ^^^^^^^^^^^ the range (defaults) + // def f(a1, a2, /, a3, a4): pass + // ^^^^^^^^^^^^ the range (no default) + // ``` + let range = TextRange::new(preceding_end, arguments.end()); + let mut tokens = SimpleTokenizer::new(contents, range).skip_trivia(); + + let comma = tokens + .next() + .expect("The function definition can't end here"); + debug_assert!(comma.kind() == TokenKind::Comma, "{comma:?}"); + let slash = tokens + .next() + .expect("The function definition can't end here"); + debug_assert!(slash.kind() == TokenKind::Slash, "{slash:?}"); + + Some((preceding_end, slash.range)) + } else { + None + }; + + // If we have a vararg we have a node that the comments attach to + let star = if arguments.vararg.is_some() { + // When the vararg is present the comments attach there and we don't need to do manual + // formatting + None + } else if let Some(first_keyword_argument) = arguments.kwonlyargs.first() { + // Check in that order: + // * `f(a, /, b, *, c)` and `f(a=1, /, b=2, *, c)` + // * `f(a, /, *, b)` + // * `f(*, b)` (else branch) + let after_arguments = arguments + .args + .last() + .map(|arg| arg.range.end()) + .or(slash.map(|(_, slash)| slash.end())); + if let Some(preceding_end) = after_arguments { + let range = TextRange::new(preceding_end, arguments.end()); + let mut tokens = SimpleTokenizer::new(contents, range).skip_trivia(); + + let comma = tokens + .next() + .expect("The function definition can't end here"); + debug_assert!(comma.kind() == TokenKind::Comma, "{comma:?}"); + let star = tokens + .next() + .expect("The function definition can't end here"); + debug_assert!(star.kind() == TokenKind::Star, "{star:?}"); + + Some(ArgumentSeparator { + preceding_end, + separator: star.range, + following_start: first_keyword_argument.start(), + }) + } else { + let mut tokens = SimpleTokenizer::new(contents, arguments.range).skip_trivia(); + + let lparen = tokens + .next() + .expect("The function definition can't end here"); + debug_assert!(lparen.kind() == TokenKind::LParen, "{lparen:?}"); + let star = tokens + .next() + .expect("The function definition can't end here"); + debug_assert!(star.kind() == TokenKind::Star, "{star:?}"); + Some(ArgumentSeparator { + preceding_end: arguments.range.start(), + separator: star.range, + following_start: first_keyword_argument.start(), + }) + } + } else { + None + }; + + // Now that we have star, compute how long slash trailing comments can go + // Check in that order: + // * `f(a, /, b)` + // * `f(a, /, *b)` + // * `f(a, /, *, b)` + // * `f(a, /)` + let slash_following_start = arguments + .args + .first() + .map(Ranged::start) + .or(arguments.vararg.as_ref().map(|first| first.start())) + .or(star.as_ref().map(|star| star.separator.start())) + .unwrap_or(arguments.end()); + let slash = slash.map(|(preceding_end, slash)| ArgumentSeparator { + preceding_end, + separator: slash, + following_start: slash_following_start, + }); + + (slash, star) +} + +/// Locates positional only arguments separator `/` or the keywords only arguments +/// separator `*` comments. +/// +/// ```python +/// def test( +/// a, +/// # Positional only arguments after here +/// /, # trailing positional argument comment. +/// b, +/// ): +/// pass +/// ``` +/// or +/// ```python +/// def f( +/// a="", +/// # Keyword only arguments only after here +/// *, # trailing keyword argument comment. +/// b="", +/// ): +/// pass +/// ``` +/// or +/// ```python +/// def f( +/// a, +/// # positional only comment, leading +/// /, # positional only comment, trailing +/// b, +/// # keyword only comment, leading +/// *, # keyword only comment, trailing +/// c, +/// ): +/// pass +/// ``` +/// Notably, the following is possible: +/// ```python +/// def f32( +/// a, +/// # positional only comment, leading +/// /, # positional only comment, trailing +/// # keyword only comment, leading +/// *, # keyword only comment, trailing +/// c, +/// ): +/// pass +/// ``` +/// +/// ## Background +/// +/// ```text +/// def f(a1, a2): pass +/// ^^^^^^ arguments (args) +/// ``` +/// Use a star to separate keyword only arguments: +/// ```text +/// def f(a1, a2, *, a3, a4): pass +/// ^^^^^^ arguments (args) +/// ^^^^^^ keyword only arguments (kwargs) +/// ``` +/// Use a slash to separate positional only arguments. Note that this changes the arguments left +/// of the slash while the star change the arguments right of it: +/// ```text +/// def f(a1, a2, /, a3, a4): pass +/// ^^^^^^ positional only arguments (posonlyargs) +/// ^^^^^^ arguments (args) +/// ``` +/// You can combine both: +/// ```text +/// def f(a1, a2, /, a3, a4, *, a5, a6): pass +/// ^^^^^^ positional only arguments (posonlyargs) +/// ^^^^^^ arguments (args) +/// ^^^^^^ keyword only arguments (kwargs) +/// ``` +/// They can all have defaults, meaning that the preceding node ends at the default instead of the +/// argument itself: +/// ```text +/// def f(a1=1, a2=2, /, a3=3, a4=4, *, a5=5, a6=6): pass +/// ^ ^ ^ ^ ^ ^ defaults +/// ^^^^^^^^^^ positional only arguments (posonlyargs) +/// ^^^^^^^^^^ arguments (args) +/// ^^^^^^^^^^ keyword only arguments (kwargs) +/// ``` +/// An especially difficult case is having no regular arguments, so comments from both slash and +/// star will attach to either a2 or a3 and the next token is incorrect. +/// ```text +/// def f(a1, a2, /, *, a3, a4): pass +/// ^^^^^^ positional only arguments (posonlyargs) +/// ^^^^^^ keyword only arguments (kwargs) +/// ``` +pub(crate) fn assign_argument_separator_comment_placement( + slash: Option<&ArgumentSeparator>, + star: Option<&ArgumentSeparator>, + comment_range: TextRange, + text_position: CommentLinePosition, +) -> Option { + if let Some(ArgumentSeparator { + preceding_end, + separator: slash, + following_start, + }) = slash + { + // ```python + // def f( + // # start too early + // a, # not own line + // # this is the one + // /, # too late (handled later) + // b, + // ) + // ``` + if comment_range.start() > *preceding_end + && comment_range.start() < slash.start() + && text_position.is_own_line() + { + return Some(ArgumentSeparatorCommentLocation::SlashLeading); + } + + // ```python + // def f( + // a, + // # too early (handled above) + // /, # this is the one + // # not end-of-line + // b, + // ) + // ``` + if comment_range.start() > slash.end() + && comment_range.start() < *following_start + && text_position.is_end_of_line() + { + return Some(ArgumentSeparatorCommentLocation::SlashTrailing); + } + } + + if let Some(ArgumentSeparator { + preceding_end, + separator: star, + following_start, + }) = star + { + // ```python + // def f( + // # start too early + // a, # not own line + // # this is the one + // *, # too late (handled later) + // b, + // ) + // ``` + if comment_range.start() > *preceding_end + && comment_range.start() < star.start() + && text_position.is_own_line() + { + return Some(ArgumentSeparatorCommentLocation::StarLeading); + } + + // ```python + // def f( + // a, + // # too early (handled above) + // *, # this is the one + // # not end-of-line + // b, + // ) + // ``` + if comment_range.start() > star.end() + && comment_range.start() < *following_start + && text_position.is_end_of_line() + { + return Some(ArgumentSeparatorCommentLocation::StarTrailing); + } + } + None +} + +/// ```python +/// def f( +/// a, +/// # before slash +/// /, # after slash +/// b, +/// # before star +/// *, # after star +/// c, +/// ): +/// pass +/// ``` +#[derive(Debug)] +pub(crate) enum ArgumentSeparatorCommentLocation { + SlashLeading, + SlashTrailing, + StarLeading, + StarTrailing, +} diff --git a/crates/ruff_python_formatter/src/other/comprehension.rs b/crates/ruff_python_formatter/src/other/comprehension.rs index edf64d8bee..0503f2d93b 100644 --- a/crates/ruff_python_formatter/src/other/comprehension.rs +++ b/crates/ruff_python_formatter/src/other/comprehension.rs @@ -1,12 +1,106 @@ -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; -use rustpython_parser::ast::Comprehension; +use crate::comments::{leading_comments, trailing_comments}; +use crate::prelude::*; +use crate::AsFormat; +use crate::{FormatNodeRule, PyFormatter}; +use ruff_formatter::{format_args, write, Buffer, FormatResult}; +use rustpython_parser::ast::{Comprehension, Expr, Ranged}; #[derive(Default)] pub struct FormatComprehension; impl FormatNodeRule for FormatComprehension { fn fmt_fields(&self, item: &Comprehension, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + struct Spacer<'a>(&'a Expr); + + impl Format> for Spacer<'_> { + fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> { + if f.context().comments().has_leading_comments(self.0) { + soft_line_break_or_space().fmt(f) + } else { + space().fmt(f) + } + } + } + + let Comprehension { + range: _, + target, + iter, + ifs, + is_async, + } = item; + + if *is_async { + write!(f, [text("async"), space()])?; + } + + let comments = f.context().comments().clone(); + let dangling_item_comments = comments.dangling_comments(item); + let (before_target_comments, before_in_comments) = dangling_item_comments.split_at( + dangling_item_comments + .partition_point(|comment| comment.slice().end() < target.range().start()), + ); + + let trailing_in_comments = comments.dangling_comments(iter); + + let in_spacer = format_with(|f| { + if before_in_comments.is_empty() { + space().fmt(f) + } else { + soft_line_break_or_space().fmt(f) + } + }); + + write!( + f, + [ + text("for"), + trailing_comments(before_target_comments), + group(&format_args!( + Spacer(target), + target.format(), + in_spacer, + leading_comments(before_in_comments), + text("in"), + trailing_comments(trailing_in_comments), + Spacer(iter), + iter.format(), + )), + ] + )?; + if !ifs.is_empty() { + let joined = format_with(|f| { + let mut joiner = f.join_with(soft_line_break_or_space()); + for if_case in ifs { + let dangling_if_comments = comments.dangling_comments(if_case); + + let (own_line_if_comments, end_of_line_if_comments) = dangling_if_comments + .split_at( + dangling_if_comments + .partition_point(|comment| comment.line_position().is_own_line()), + ); + joiner.entry(&group(&format_args!( + leading_comments(own_line_if_comments), + text("if"), + trailing_comments(end_of_line_if_comments), + Spacer(if_case), + if_case.format(), + ))); + } + joiner.finish() + }); + + write!(f, [soft_line_break_or_space(), group(&joined)])?; + } + Ok(()) + } + + fn fmt_dangling_comments( + &self, + _node: &Comprehension, + _f: &mut PyFormatter, + ) -> FormatResult<()> { + // dangling comments are formatted as part of fmt_fields + Ok(()) } } diff --git a/crates/ruff_python_formatter/src/other/decorator.rs b/crates/ruff_python_formatter/src/other/decorator.rs index 3ebdca4e70..88b1a92d65 100644 --- a/crates/ruff_python_formatter/src/other/decorator.rs +++ b/crates/ruff_python_formatter/src/other/decorator.rs @@ -1,3 +1,4 @@ +use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::Parenthesize; use crate::prelude::*; use crate::FormatNodeRule; @@ -18,7 +19,7 @@ impl FormatNodeRule for FormatDecorator { f, [ text("@"), - expression.format().with_options(Parenthesize::Optional) + maybe_parenthesize_expression(expression, item, Parenthesize::Optional) ] ) } diff --git a/crates/ruff_python_formatter/src/other/except_handler_except_handler.rs b/crates/ruff_python_formatter/src/other/except_handler_except_handler.rs new file mode 100644 index 0000000000..a66fd39963 --- /dev/null +++ b/crates/ruff_python_formatter/src/other/except_handler_except_handler.rs @@ -0,0 +1,91 @@ +use crate::comments::trailing_comments; +use crate::expression::maybe_parenthesize_expression; +use crate::expression::parentheses::Parenthesize; +use crate::prelude::*; +use crate::{FormatNodeRule, PyFormatter}; +use ruff_formatter::FormatRuleWithOptions; +use ruff_formatter::{write, Buffer, FormatResult}; +use ruff_python_ast::node::AstNode; +use rustpython_parser::ast::ExceptHandlerExceptHandler; + +#[derive(Copy, Clone, Default)] +pub enum ExceptHandlerKind { + #[default] + Regular, + Starred, +} + +#[derive(Default)] +pub struct FormatExceptHandlerExceptHandler { + except_handler_kind: ExceptHandlerKind, +} + +impl FormatRuleWithOptions> + for FormatExceptHandlerExceptHandler +{ + type Options = ExceptHandlerKind; + + fn with_options(mut self, options: Self::Options) -> Self { + self.except_handler_kind = options; + self + } +} + +impl FormatNodeRule for FormatExceptHandlerExceptHandler { + fn fmt_fields( + &self, + item: &ExceptHandlerExceptHandler, + f: &mut PyFormatter, + ) -> FormatResult<()> { + let ExceptHandlerExceptHandler { + range: _, + type_, + name, + body, + } = item; + + let comments_info = f.context().comments().clone(); + let dangling_comments = comments_info.dangling_comments(item.as_any_node_ref()); + + write!( + f, + [ + text("except"), + match self.except_handler_kind { + ExceptHandlerKind::Regular => None, + ExceptHandlerKind::Starred => Some(text("*")), + } + ] + )?; + + if let Some(type_) = type_ { + write!( + f, + [ + space(), + maybe_parenthesize_expression(type_, item, Parenthesize::IfBreaks) + ] + )?; + if let Some(name) = name { + write!(f, [space(), text("as"), space(), name.format()])?; + } + } + write!( + f, + [ + text(":"), + trailing_comments(dangling_comments), + block_indent(&body.format()) + ] + ) + } + + fn fmt_dangling_comments( + &self, + _node: &ExceptHandlerExceptHandler, + _f: &mut PyFormatter, + ) -> FormatResult<()> { + // dangling comments are formatted as part of fmt_fields + Ok(()) + } +} diff --git a/crates/ruff_python_formatter/src/other/excepthandler_except_handler.rs b/crates/ruff_python_formatter/src/other/excepthandler_except_handler.rs deleted file mode 100644 index a5826413fc..0000000000 --- a/crates/ruff_python_formatter/src/other/excepthandler_except_handler.rs +++ /dev/null @@ -1,16 +0,0 @@ -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; -use rustpython_parser::ast::ExcepthandlerExceptHandler; - -#[derive(Default)] -pub struct FormatExcepthandlerExceptHandler; - -impl FormatNodeRule for FormatExcepthandlerExceptHandler { - fn fmt_fields( - &self, - item: &ExcepthandlerExceptHandler, - f: &mut PyFormatter, - ) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) - } -} diff --git a/crates/ruff_python_formatter/src/other/identifier.rs b/crates/ruff_python_formatter/src/other/identifier.rs new file mode 100644 index 0000000000..40e588b491 --- /dev/null +++ b/crates/ruff_python_formatter/src/other/identifier.rs @@ -0,0 +1,28 @@ +use crate::prelude::*; +use crate::AsFormat; +use ruff_formatter::{FormatOwnedWithRule, FormatRefWithRule}; +use rustpython_parser::ast::{Identifier, Ranged}; + +pub struct FormatIdentifier; + +impl FormatRule> for FormatIdentifier { + fn fmt(&self, item: &Identifier, f: &mut PyFormatter) -> FormatResult<()> { + source_text_slice(item.range(), ContainsNewlines::No).fmt(f) + } +} + +impl<'ast> AsFormat> for Identifier { + type Format<'a> = FormatRefWithRule<'a, Identifier, FormatIdentifier, PyFormatContext<'ast>>; + + fn format(&self) -> Self::Format<'_> { + FormatRefWithRule::new(self, FormatIdentifier) + } +} + +impl<'ast> IntoFormat> for Identifier { + type Format = FormatOwnedWithRule>; + + fn into_format(self) -> Self::Format { + FormatOwnedWithRule::new(self, FormatIdentifier) + } +} diff --git a/crates/ruff_python_formatter/src/other/keyword.rs b/crates/ruff_python_formatter/src/other/keyword.rs index 7998efc47c..47d3db19fc 100644 --- a/crates/ruff_python_formatter/src/other/keyword.rs +++ b/crates/ruff_python_formatter/src/other/keyword.rs @@ -1,5 +1,6 @@ -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; +use crate::prelude::*; +use crate::FormatNodeRule; +use ruff_formatter::write; use rustpython_parser::ast::Keyword; #[derive(Default)] @@ -7,6 +8,16 @@ pub struct FormatKeyword; impl FormatNodeRule for FormatKeyword { fn fmt_fields(&self, item: &Keyword, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + let Keyword { + range: _, + arg, + value, + } = item; + if let Some(arg) = arg { + write!(f, [arg.format(), text("="), value.format()]) + } else { + // Comments after the stars are reassigned as trailing value comments + write!(f, [text("**"), value.format()]) + } } } diff --git a/crates/ruff_python_formatter/src/other/mod.rs b/crates/ruff_python_formatter/src/other/mod.rs index b7f843d7bc..76c0b2857d 100644 --- a/crates/ruff_python_formatter/src/other/mod.rs +++ b/crates/ruff_python_formatter/src/other/mod.rs @@ -1,10 +1,12 @@ pub(crate) mod alias; pub(crate) mod arg; +pub(crate) mod arg_with_default; pub(crate) mod arguments; pub(crate) mod comprehension; pub(crate) mod decorator; -pub(crate) mod excepthandler_except_handler; +pub(crate) mod except_handler_except_handler; +pub(crate) mod identifier; pub(crate) mod keyword; pub(crate) mod match_case; pub(crate) mod type_ignore_type_ignore; -pub(crate) mod withitem; +pub(crate) mod with_item; diff --git a/crates/ruff_python_formatter/src/other/with_item.rs b/crates/ruff_python_formatter/src/other/with_item.rs new file mode 100644 index 0000000000..1004639f15 --- /dev/null +++ b/crates/ruff_python_formatter/src/other/with_item.rs @@ -0,0 +1,35 @@ +use crate::expression::maybe_parenthesize_expression; +use crate::expression::parentheses::Parenthesize; +use crate::prelude::*; +use crate::{FormatNodeRule, PyFormatter}; +use ruff_formatter::{write, Buffer, FormatResult}; +use rustpython_parser::ast::WithItem; + +#[derive(Default)] +pub struct FormatWithItem; + +impl FormatNodeRule for FormatWithItem { + fn fmt_fields(&self, item: &WithItem, f: &mut PyFormatter) -> FormatResult<()> { + let WithItem { + range: _, + context_expr, + optional_vars, + } = item; + + let inner = format_with(|f| { + write!( + f, + [maybe_parenthesize_expression( + context_expr, + item, + Parenthesize::IfBreaks + )] + )?; + if let Some(optional_vars) = optional_vars { + write!(f, [space(), text("as"), space(), optional_vars.format()])?; + } + Ok(()) + }); + write!(f, [group(&inner)]) + } +} diff --git a/crates/ruff_python_formatter/src/other/withitem.rs b/crates/ruff_python_formatter/src/other/withitem.rs deleted file mode 100644 index 7edf9ea116..0000000000 --- a/crates/ruff_python_formatter/src/other/withitem.rs +++ /dev/null @@ -1,12 +0,0 @@ -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; -use rustpython_parser::ast::Withitem; - -#[derive(Default)] -pub struct FormatWithitem; - -impl FormatNodeRule for FormatWithitem { - fn fmt_fields(&self, item: &Withitem, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) - } -} diff --git a/crates/ruff_python_formatter/src/pattern/pattern_match_value.rs b/crates/ruff_python_formatter/src/pattern/pattern_match_value.rs index 9a57fbd15a..0a5251f3aa 100644 --- a/crates/ruff_python_formatter/src/pattern/pattern_match_value.rs +++ b/crates/ruff_python_formatter/src/pattern/pattern_match_value.rs @@ -1,7 +1,9 @@ -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; use rustpython_parser::ast::PatternMatchValue; +use ruff_formatter::{write, Buffer, FormatResult}; + +use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; + #[derive(Default)] pub struct FormatPatternMatchValue; diff --git a/crates/ruff_python_formatter/src/prelude.rs b/crates/ruff_python_formatter/src/prelude.rs index 7382684dba..f3f88f6145 100644 --- a/crates/ruff_python_formatter/src/prelude.rs +++ b/crates/ruff_python_formatter/src/prelude.rs @@ -1,7 +1,7 @@ #[allow(unused_imports)] pub(crate) use crate::{ - builders::PyFormatterExtensions, AsFormat, FormattedIterExt as _, IntoFormat, PyFormatContext, - PyFormatter, + builders::PyFormatterExtensions, AsFormat, FormatNodeRule, FormattedIterExt as _, IntoFormat, + PyFormatContext, PyFormatter, }; #[allow(unused_imports)] pub(crate) use ruff_formatter::prelude::*; diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__beginning_backslash_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__beginning_backslash_py.snap deleted file mode 100644 index e6b8b1a44b..0000000000 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__beginning_backslash_py.snap +++ /dev/null @@ -1,40 +0,0 @@ ---- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/beginning_backslash.py ---- -## Input - -```py -\ - - - - - -print("hello, world") -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -1 +1 @@ --print("hello, world") -+NOT_IMPLEMENTED_call() -``` - -## Ruff Output - -```py -NOT_IMPLEMENTED_call() -``` - -## Black Output - -```py -print("hello, world") -``` - - diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__class_blank_parentheses_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__class_blank_parentheses_py.snap deleted file mode 100644 index 301e65440e..0000000000 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__class_blank_parentheses_py.snap +++ /dev/null @@ -1,135 +0,0 @@ ---- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/class_blank_parentheses.py ---- -## Input - -```py -class SimpleClassWithBlankParentheses(): - pass -class ClassWithSpaceParentheses ( ): - first_test_data = 90 - second_test_data = 100 - def test_func(self): - return None -class ClassWithEmptyFunc(object): - - def func_with_blank_parentheses(): - return 5 - - -def public_func_with_blank_parentheses(): - return None -def class_under_the_func_with_blank_parentheses(): - class InsideFunc(): - pass -class NormalClass ( -): - def func_for_testing(self, first, second): - sum = first + second - return sum -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -1,18 +1,10 @@ --class SimpleClassWithBlankParentheses: -- pass -+NOT_YET_IMPLEMENTED_StmtClassDef - - --class ClassWithSpaceParentheses: -- first_test_data = 90 -- second_test_data = 100 -- -- def test_func(self): -- return None -+NOT_YET_IMPLEMENTED_StmtClassDef - - --class ClassWithEmptyFunc(object): -- def func_with_blank_parentheses(): -- return 5 -+NOT_YET_IMPLEMENTED_StmtClassDef - - - def public_func_with_blank_parentheses(): -@@ -20,11 +12,7 @@ - - - def class_under_the_func_with_blank_parentheses(): -- class InsideFunc: -- pass -+ NOT_YET_IMPLEMENTED_StmtClassDef - - --class NormalClass: -- def func_for_testing(self, first, second): -- sum = first + second -- return sum -+NOT_YET_IMPLEMENTED_StmtClassDef -``` - -## Ruff Output - -```py -NOT_YET_IMPLEMENTED_StmtClassDef - - -NOT_YET_IMPLEMENTED_StmtClassDef - - -NOT_YET_IMPLEMENTED_StmtClassDef - - -def public_func_with_blank_parentheses(): - return None - - -def class_under_the_func_with_blank_parentheses(): - NOT_YET_IMPLEMENTED_StmtClassDef - - -NOT_YET_IMPLEMENTED_StmtClassDef -``` - -## Black Output - -```py -class SimpleClassWithBlankParentheses: - pass - - -class ClassWithSpaceParentheses: - first_test_data = 90 - second_test_data = 100 - - def test_func(self): - return None - - -class ClassWithEmptyFunc(object): - def func_with_blank_parentheses(): - return 5 - - -def public_func_with_blank_parentheses(): - return None - - -def class_under_the_func_with_blank_parentheses(): - class InsideFunc: - pass - - -class NormalClass: - def func_for_testing(self, first, second): - sum = first + second - return sum -``` - - diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments3_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments3_py.snap deleted file mode 100644 index 1d64b8c286..0000000000 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments3_py.snap +++ /dev/null @@ -1,193 +0,0 @@ ---- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments3.py ---- -## Input - -```py -# The percent-percent comments are Spyder IDE cells. - - -# %% -def func(): - x = """ - a really long string - """ - lcomp3 = [ - # This one is actually too long to fit in a single line. - element.split("\n", 1)[0] - # yup - for element in collection.select_elements() - # right - if element is not None - ] - # Capture each of the exceptions in the MultiError along with each of their causes and contexts - if isinstance(exc_value, MultiError): - embedded = [] - for exc in exc_value.exceptions: - if exc not in _seen: - embedded.append( - # This should be left alone (before) - traceback.TracebackException.from_exception( - exc, - limit=limit, - lookup_lines=lookup_lines, - capture_locals=capture_locals, - # copy the set of _seen exceptions so that duplicates - # shared between sub-exceptions are not omitted - _seen=set(_seen), - ) - # This should be left alone (after) - ) - - # everything is fine if the expression isn't nested - traceback.TracebackException.from_exception( - exc, - limit=limit, - lookup_lines=lookup_lines, - capture_locals=capture_locals, - # copy the set of _seen exceptions so that duplicates - # shared between sub-exceptions are not omitted - _seen=set(_seen), - ) - - -# %% -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -3,46 +3,15 @@ - - # %% - def func(): -- x = """ -- a really long string -- """ -- lcomp3 = [ -- # This one is actually too long to fit in a single line. -- element.split("\n", 1)[0] -- # yup -- for element in collection.select_elements() -- # right -- if element is not None -- ] -+ x = "NOT_YET_IMPLEMENTED_STRING" -+ lcomp3 = [i for i in []] - # Capture each of the exceptions in the MultiError along with each of their causes and contexts -- if isinstance(exc_value, MultiError): -+ if NOT_IMPLEMENTED_call(): - embedded = [] -- for exc in exc_value.exceptions: -- if exc not in _seen: -- embedded.append( -- # This should be left alone (before) -- traceback.TracebackException.from_exception( -- exc, -- limit=limit, -- lookup_lines=lookup_lines, -- capture_locals=capture_locals, -- # copy the set of _seen exceptions so that duplicates -- # shared between sub-exceptions are not omitted -- _seen=set(_seen), -- ) -- # This should be left alone (after) -- ) -+ NOT_YET_IMPLEMENTED_StmtFor - - # everything is fine if the expression isn't nested -- traceback.TracebackException.from_exception( -- exc, -- limit=limit, -- lookup_lines=lookup_lines, -- capture_locals=capture_locals, -- # copy the set of _seen exceptions so that duplicates -- # shared between sub-exceptions are not omitted -- _seen=set(_seen), -- ) -+ NOT_IMPLEMENTED_call() - - - # %% -``` - -## Ruff Output - -```py -# The percent-percent comments are Spyder IDE cells. - - -# %% -def func(): - x = "NOT_YET_IMPLEMENTED_STRING" - lcomp3 = [i for i in []] - # Capture each of the exceptions in the MultiError along with each of their causes and contexts - if NOT_IMPLEMENTED_call(): - embedded = [] - NOT_YET_IMPLEMENTED_StmtFor - - # everything is fine if the expression isn't nested - NOT_IMPLEMENTED_call() - - -# %% -``` - -## Black Output - -```py -# The percent-percent comments are Spyder IDE cells. - - -# %% -def func(): - x = """ - a really long string - """ - lcomp3 = [ - # This one is actually too long to fit in a single line. - element.split("\n", 1)[0] - # yup - for element in collection.select_elements() - # right - if element is not None - ] - # Capture each of the exceptions in the MultiError along with each of their causes and contexts - if isinstance(exc_value, MultiError): - embedded = [] - for exc in exc_value.exceptions: - if exc not in _seen: - embedded.append( - # This should be left alone (before) - traceback.TracebackException.from_exception( - exc, - limit=limit, - lookup_lines=lookup_lines, - capture_locals=capture_locals, - # copy the set of _seen exceptions so that duplicates - # shared between sub-exceptions are not omitted - _seen=set(_seen), - ) - # This should be left alone (after) - ) - - # everything is fine if the expression isn't nested - traceback.TracebackException.from_exception( - exc, - limit=limit, - lookup_lines=lookup_lines, - capture_locals=capture_locals, - # copy the set of _seen exceptions so that duplicates - # shared between sub-exceptions are not omitted - _seen=set(_seen), - ) - - -# %% -``` - - diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments5_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments5_py.snap deleted file mode 100644 index 4cdfd0bf99..0000000000 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments5_py.snap +++ /dev/null @@ -1,300 +0,0 @@ ---- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments5.py ---- -## Input - -```py -while True: - if something.changed: - do.stuff() # trailing comment - # Comment belongs to the `if` block. - # This one belongs to the `while` block. - - # Should this one, too? I guess so. - -# This one is properly standalone now. - -for i in range(100): - # first we do this - if i % 33 == 0: - break - - # then we do this - print(i) - # and finally we loop around - -with open(some_temp_file) as f: - data = f.read() - -try: - with open(some_other_file) as w: - w.write(data) - -except OSError: - print("problems") - -import sys - - -# leading function comment -def wat(): - ... - # trailing function comment - - -# SECTION COMMENT - - -# leading 1 -@deco1 -# leading 2 -@deco2(with_args=True) -# leading 3 -@deco3 -def decorated1(): - ... - - -# leading 1 -@deco1 -# leading 2 -@deco2(with_args=True) -# leading function comment -def decorated1(): - ... - - -# Note: this is fixed in -# Preview.empty_lines_before_class_or_def_with_leading_comments. -# In the current style, the user will have to split those lines by hand. -some_instruction - - -# This comment should be split from `some_instruction` by two lines but isn't. -def g(): - ... - - -if __name__ == "__main__": - main() -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -1,33 +1,20 @@ - while True: -- if something.changed: -- do.stuff() # trailing comment -+ if something.NOT_IMPLEMENTED_attr: -+ NOT_IMPLEMENTED_call() # trailing comment - # Comment belongs to the `if` block. - # This one belongs to the `while` block. - - # Should this one, too? I guess so. - - # This one is properly standalone now. -- --for i in range(100): -- # first we do this -- if i % 33 == 0: -- break - -- # then we do this -- print(i) -- # and finally we loop around -+NOT_YET_IMPLEMENTED_StmtFor - --with open(some_temp_file) as f: -- data = f.read() -- --try: -- with open(some_other_file) as w: -- w.write(data) -+NOT_YET_IMPLEMENTED_StmtWith - --except OSError: -- print("problems") -+NOT_YET_IMPLEMENTED_StmtTry - --import sys -+NOT_YET_IMPLEMENTED_StmtImport - - - # leading function comment -@@ -42,7 +29,7 @@ - # leading 1 - @deco1 - # leading 2 --@deco2(with_args=True) -+@NOT_IMPLEMENTED_call() - # leading 3 - @deco3 - def decorated1(): -@@ -52,7 +39,7 @@ - # leading 1 - @deco1 - # leading 2 --@deco2(with_args=True) -+@NOT_IMPLEMENTED_call() - # leading function comment - def decorated1(): - ... -@@ -69,5 +56,5 @@ - ... - - --if __name__ == "__main__": -- main() -+if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: -+ NOT_IMPLEMENTED_call() -``` - -## Ruff Output - -```py -while True: - if something.NOT_IMPLEMENTED_attr: - NOT_IMPLEMENTED_call() # trailing comment - # Comment belongs to the `if` block. - # This one belongs to the `while` block. - - # Should this one, too? I guess so. - -# This one is properly standalone now. - -NOT_YET_IMPLEMENTED_StmtFor - -NOT_YET_IMPLEMENTED_StmtWith - -NOT_YET_IMPLEMENTED_StmtTry - -NOT_YET_IMPLEMENTED_StmtImport - - -# leading function comment -def wat(): - ... - # trailing function comment - - -# SECTION COMMENT - - -# leading 1 -@deco1 -# leading 2 -@NOT_IMPLEMENTED_call() -# leading 3 -@deco3 -def decorated1(): - ... - - -# leading 1 -@deco1 -# leading 2 -@NOT_IMPLEMENTED_call() -# leading function comment -def decorated1(): - ... - - -# Note: this is fixed in -# Preview.empty_lines_before_class_or_def_with_leading_comments. -# In the current style, the user will have to split those lines by hand. -some_instruction - - -# This comment should be split from `some_instruction` by two lines but isn't. -def g(): - ... - - -if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: - NOT_IMPLEMENTED_call() -``` - -## Black Output - -```py -while True: - if something.changed: - do.stuff() # trailing comment - # Comment belongs to the `if` block. - # This one belongs to the `while` block. - - # Should this one, too? I guess so. - -# This one is properly standalone now. - -for i in range(100): - # first we do this - if i % 33 == 0: - break - - # then we do this - print(i) - # and finally we loop around - -with open(some_temp_file) as f: - data = f.read() - -try: - with open(some_other_file) as w: - w.write(data) - -except OSError: - print("problems") - -import sys - - -# leading function comment -def wat(): - ... - # trailing function comment - - -# SECTION COMMENT - - -# leading 1 -@deco1 -# leading 2 -@deco2(with_args=True) -# leading 3 -@deco3 -def decorated1(): - ... - - -# leading 1 -@deco1 -# leading 2 -@deco2(with_args=True) -# leading function comment -def decorated1(): - ... - - -# Note: this is fixed in -# Preview.empty_lines_before_class_or_def_with_leading_comments. -# In the current style, the user will have to split those lines by hand. -some_instruction - - -# This comment should be split from `some_instruction` by two lines but isn't. -def g(): - ... - - -if __name__ == "__main__": - main() -``` - - diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_py.snap deleted file mode 100644 index ec61d99846..0000000000 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_py.snap +++ /dev/null @@ -1,397 +0,0 @@ ---- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments.py ---- -## Input - -```py -#!/usr/bin/env python3 -# fmt: on -# Some license here. -# -# Has many lines. Many, many lines. -# Many, many, many lines. -"""Module docstring. - -Possibly also many, many lines. -""" - -import os.path -import sys - -import a -from b.c import X # some noqa comment - -try: - import fast -except ImportError: - import slow as fast - - -# Some comment before a function. -y = 1 -( - # some strings - y # type: ignore -) - - -def function(default=None): - """Docstring comes first. - - Possibly many lines. - """ - # FIXME: Some comment about why this function is crap but still in production. - import inner_imports - - if inner_imports.are_evil(): - # Explains why we have this if. - # In great detail indeed. - x = X() - return x.method1() # type: ignore - - # This return is also commented for some reason. - return default - - -# Explains why we use global state. -GLOBAL_STATE = {"a": a(1), "b": a(2), "c": a(3)} - - -# Another comment! -# This time two lines. - - -class Foo: - """Docstring for class Foo. Example from Sphinx docs.""" - - #: Doc comment for class attribute Foo.bar. - #: It can have multiple lines. - bar = 1 - - flox = 1.5 #: Doc comment for Foo.flox. One line only. - - baz = 2 - """Docstring for class attribute Foo.baz.""" - - def __init__(self): - #: Doc comment for instance attribute qux. - self.qux = 3 - - self.spam = 4 - """Docstring for instance attribute spam.""" - - -#'

This is pweave!

- - -@fast(really=True) -async def wat(): - # This comment, for some reason \ - # contains a trailing backslash. - async with X.open_async() as x: # Some more comments - result = await x.method1() - # Comment after ending a block. - if result: - print("A OK", file=sys.stdout) - # Comment between things. - print() - - -# Some closing comments. -# Maybe Vim or Emacs directives for formatting. -# Who knows. -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -4,24 +4,15 @@ - # - # Has many lines. Many, many lines. - # Many, many, many lines. --"""Module docstring. -- --Possibly also many, many lines. --""" -- --import os.path --import sys -- --import a --from b.c import X # some noqa comment -+"NOT_YET_IMPLEMENTED_STRING" - --try: -- import fast --except ImportError: -- import slow as fast -+NOT_YET_IMPLEMENTED_StmtImport -+NOT_YET_IMPLEMENTED_StmtImport - -+NOT_YET_IMPLEMENTED_StmtImport -+NOT_YET_IMPLEMENTED_StmtImportFrom # some noqa comment - --# Some comment before a function. -+NOT_YET_IMPLEMENTED_StmtTry - y = 1 - ( - # some strings -@@ -30,67 +21,46 @@ - - - def function(default=None): -- """Docstring comes first. -- -- Possibly many lines. -- """ -+ "NOT_YET_IMPLEMENTED_STRING" - # FIXME: Some comment about why this function is crap but still in production. -- import inner_imports -+ NOT_YET_IMPLEMENTED_StmtImport - -- if inner_imports.are_evil(): -+ if NOT_IMPLEMENTED_call(): - # Explains why we have this if. - # In great detail indeed. -- x = X() -- return x.method1() # type: ignore -+ x = NOT_IMPLEMENTED_call() -+ return NOT_IMPLEMENTED_call() # type: ignore - - # This return is also commented for some reason. - return default - - - # Explains why we use global state. --GLOBAL_STATE = {"a": a(1), "b": a(2), "c": a(3)} -+GLOBAL_STATE = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} - - - # Another comment! - # This time two lines. - - --class Foo: -- """Docstring for class Foo. Example from Sphinx docs.""" -- -- #: Doc comment for class attribute Foo.bar. -- #: It can have multiple lines. -- bar = 1 -- -- flox = 1.5 #: Doc comment for Foo.flox. One line only. -- -- baz = 2 -- """Docstring for class attribute Foo.baz.""" -- -- def __init__(self): -- #: Doc comment for instance attribute qux. -- self.qux = 3 -- -- self.spam = 4 -- """Docstring for instance attribute spam.""" -+NOT_YET_IMPLEMENTED_StmtClassDef - - - #'

This is pweave!

- - --@fast(really=True) -+@NOT_IMPLEMENTED_call() - async def wat(): - # This comment, for some reason \ - # contains a trailing backslash. -- async with X.open_async() as x: # Some more comments -- result = await x.method1() -+ NOT_YET_IMPLEMENTED_StmtAsyncWith # Some more comments - # Comment after ending a block. - if result: -- print("A OK", file=sys.stdout) -+ NOT_IMPLEMENTED_call() - # Comment between things. -- print() -+ NOT_IMPLEMENTED_call() - - - # Some closing comments. - # Maybe Vim or Emacs directives for formatting. --# Who knows. -\ No newline at end of file -+# Who knows. -``` - -## Ruff Output - -```py -#!/usr/bin/env python3 -# fmt: on -# Some license here. -# -# Has many lines. Many, many lines. -# Many, many, many lines. -"NOT_YET_IMPLEMENTED_STRING" - -NOT_YET_IMPLEMENTED_StmtImport -NOT_YET_IMPLEMENTED_StmtImport - -NOT_YET_IMPLEMENTED_StmtImport -NOT_YET_IMPLEMENTED_StmtImportFrom # some noqa comment - -NOT_YET_IMPLEMENTED_StmtTry -y = 1 -( - # some strings - y # type: ignore -) - - -def function(default=None): - "NOT_YET_IMPLEMENTED_STRING" - # FIXME: Some comment about why this function is crap but still in production. - NOT_YET_IMPLEMENTED_StmtImport - - if NOT_IMPLEMENTED_call(): - # Explains why we have this if. - # In great detail indeed. - x = NOT_IMPLEMENTED_call() - return NOT_IMPLEMENTED_call() # type: ignore - - # This return is also commented for some reason. - return default - - -# Explains why we use global state. -GLOBAL_STATE = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} - - -# Another comment! -# This time two lines. - - -NOT_YET_IMPLEMENTED_StmtClassDef - - -#'

This is pweave!

- - -@NOT_IMPLEMENTED_call() -async def wat(): - # This comment, for some reason \ - # contains a trailing backslash. - NOT_YET_IMPLEMENTED_StmtAsyncWith # Some more comments - # Comment after ending a block. - if result: - NOT_IMPLEMENTED_call() - # Comment between things. - NOT_IMPLEMENTED_call() - - -# Some closing comments. -# Maybe Vim or Emacs directives for formatting. -# Who knows. -``` - -## Black Output - -```py -#!/usr/bin/env python3 -# fmt: on -# Some license here. -# -# Has many lines. Many, many lines. -# Many, many, many lines. -"""Module docstring. - -Possibly also many, many lines. -""" - -import os.path -import sys - -import a -from b.c import X # some noqa comment - -try: - import fast -except ImportError: - import slow as fast - - -# Some comment before a function. -y = 1 -( - # some strings - y # type: ignore -) - - -def function(default=None): - """Docstring comes first. - - Possibly many lines. - """ - # FIXME: Some comment about why this function is crap but still in production. - import inner_imports - - if inner_imports.are_evil(): - # Explains why we have this if. - # In great detail indeed. - x = X() - return x.method1() # type: ignore - - # This return is also commented for some reason. - return default - - -# Explains why we use global state. -GLOBAL_STATE = {"a": a(1), "b": a(2), "c": a(3)} - - -# Another comment! -# This time two lines. - - -class Foo: - """Docstring for class Foo. Example from Sphinx docs.""" - - #: Doc comment for class attribute Foo.bar. - #: It can have multiple lines. - bar = 1 - - flox = 1.5 #: Doc comment for Foo.flox. One line only. - - baz = 2 - """Docstring for class attribute Foo.baz.""" - - def __init__(self): - #: Doc comment for instance attribute qux. - self.qux = 3 - - self.spam = 4 - """Docstring for instance attribute spam.""" - - -#'

This is pweave!

- - -@fast(really=True) -async def wat(): - # This comment, for some reason \ - # contains a trailing backslash. - async with X.open_async() as x: # Some more comments - result = await x.method1() - # Comment after ending a block. - if result: - print("A OK", file=sys.stdout) - # Comment between things. - print() - - -# Some closing comments. -# Maybe Vim or Emacs directives for formatting. -# Who knows.``` - - diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_no_extra_empty_line_before_eof_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_no_extra_empty_line_before_eof_py.snap deleted file mode 100644 index 69d1e86ad3..0000000000 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_no_extra_empty_line_before_eof_py.snap +++ /dev/null @@ -1,45 +0,0 @@ ---- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/docstring_no_extra_empty_line_before_eof.py ---- -## Input - -```py -# Make sure when the file ends with class's docstring, -# It doesn't add extra blank lines. -class ClassWithDocstring: - """A docstring.""" -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -1,4 +1,3 @@ - # Make sure when the file ends with class's docstring, - # It doesn't add extra blank lines. --class ClassWithDocstring: -- """A docstring.""" -+NOT_YET_IMPLEMENTED_StmtClassDef -``` - -## Ruff Output - -```py -# Make sure when the file ends with class's docstring, -# It doesn't add extra blank lines. -NOT_YET_IMPLEMENTED_StmtClassDef -``` - -## Black Output - -```py -# Make sure when the file ends with class's docstring, -# It doesn't add extra blank lines. -class ClassWithDocstring: - """A docstring.""" -``` - - diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip4_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip4_py.snap deleted file mode 100644 index 6d4c587187..0000000000 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip4_py.snap +++ /dev/null @@ -1,50 +0,0 @@ ---- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtskip4.py ---- -## Input - -```py -a = 2 -# fmt: skip -l = [1, 2, 3,] -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -1,7 +1,3 @@ - a = 2 - # fmt: skip --l = [ -- 1, -- 2, -- 3, --] -+l = [1, 2, 3] -``` - -## Ruff Output - -```py -a = 2 -# fmt: skip -l = [1, 2, 3] -``` - -## Black Output - -```py -a = 2 -# fmt: skip -l = [ - 1, - 2, - 3, -] -``` - - diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip6_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip6_py.snap deleted file mode 100644 index ea2bbeca77..0000000000 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip6_py.snap +++ /dev/null @@ -1,46 +0,0 @@ ---- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtskip6.py ---- -## Input - -```py -class A: - def f(self): - for line in range(10): - if True: - pass # fmt: skip -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -1,5 +1 @@ --class A: -- def f(self): -- for line in range(10): -- if True: -- pass # fmt: skip -+NOT_YET_IMPLEMENTED_StmtClassDef -``` - -## Ruff Output - -```py -NOT_YET_IMPLEMENTED_StmtClassDef -``` - -## Black Output - -```py -class A: - def f(self): - for line in range(10): - if True: - pass # fmt: skip -``` - - diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_trailing_comma_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_trailing_comma_py.snap deleted file mode 100644 index ecc5c3010f..0000000000 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_trailing_comma_py.snap +++ /dev/null @@ -1,379 +0,0 @@ ---- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/function_trailing_comma.py ---- -## Input - -```py -def f(a,): - d = {'key': 'value',} - tup = (1,) - -def f2(a,b,): - d = {'key': 'value', 'key2': 'value2',} - tup = (1,2,) - -def f(a:int=1,): - call(arg={'explode': 'this',}) - call2(arg=[1,2,3],) - x = { - "a": 1, - "b": 2, - }["a"] - if a == {"a": 1,"b": 2,"c": 3,"d": 4,"e": 5,"f": 6,"g": 7,"h": 8,}["a"]: - pass - -def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> Set[ - "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -]: - json = {"k": {"k2": {"k3": [1,]}}} - - - -# The type annotation shouldn't get a trailing comma since that would change its type. -# Relevant bug report: https://github.com/psf/black/issues/2381. -def some_function_with_a_really_long_name() -> ( - returning_a_deeply_nested_import_of_a_type_i_suppose -): - pass - - -def some_method_with_a_really_long_name(very_long_parameter_so_yeah: str, another_long_parameter: int) -> ( - another_case_of_returning_a_deeply_nested_import_of_a_type_i_suppose_cause_why_not -): - pass - - -def func() -> ( - also_super_long_type_annotation_that_may_cause_an_AST_related_crash_in_black(this_shouldn_t_get_a_trailing_comma_too) -): - pass - - -def func() -> ((also_super_long_type_annotation_that_may_cause_an_AST_related_crash_in_black( - this_shouldn_t_get_a_trailing_comma_too - )) -): - pass - - -# Make sure inner one-element tuple won't explode -some_module.some_function( - argument1, (one_element_tuple,), argument4, argument5, argument6 -) - -# Inner trailing comma causes outer to explode -some_module.some_function( - argument1, (one, two,), argument4, argument5, argument6 -) -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -1,69 +1,30 @@ - def f( - a, - ): -- d = { -- "key": "value", -- } -- tup = (1,) -+ d = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} -+ tup = (1, 2) - - - def f2( - a, - b, - ): -- d = { -- "key": "value", -- "key2": "value2", -- } -- tup = ( -- 1, -- 2, -- ) -+ d = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} -+ tup = (1, 2) - - - def f( - a: int = 1, - ): -- call( -- arg={ -- "explode": "this", -- } -- ) -- call2( -- arg=[1, 2, 3], -- ) -- x = { -- "a": 1, -- "b": 2, -- }["a"] -- if ( -- a -- == { -- "a": 1, -- "b": 2, -- "c": 3, -- "d": 4, -- "e": 5, -- "f": 6, -- "g": 7, -- "h": 8, -- }["a"] -- ): -+ NOT_IMPLEMENTED_call() -+ NOT_IMPLEMENTED_call() -+ x = NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+ if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: - pass - - --def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> ( -- Set["xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"] --): -- json = { -- "k": { -- "k2": { -- "k3": [ -- 1, -- ] -- } -- } -- } -+def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key]: -+ json = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} - - - # The type annotation shouldn't get a trailing comma since that would change its type. -@@ -80,35 +41,16 @@ - pass - - --def func() -> ( -- also_super_long_type_annotation_that_may_cause_an_AST_related_crash_in_black( -- this_shouldn_t_get_a_trailing_comma_too -- ) --): -+def func() -> NOT_IMPLEMENTED_call(): - pass - - --def func() -> ( -- also_super_long_type_annotation_that_may_cause_an_AST_related_crash_in_black( -- this_shouldn_t_get_a_trailing_comma_too -- ) --): -+def func() -> NOT_IMPLEMENTED_call(): - pass - - - # Make sure inner one-element tuple won't explode --some_module.some_function( -- argument1, (one_element_tuple,), argument4, argument5, argument6 --) -+NOT_IMPLEMENTED_call() - - # Inner trailing comma causes outer to explode --some_module.some_function( -- argument1, -- ( -- one, -- two, -- ), -- argument4, -- argument5, -- argument6, --) -+NOT_IMPLEMENTED_call() -``` - -## Ruff Output - -```py -def f( - a, -): - d = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} - tup = (1, 2) - - -def f2( - a, - b, -): - d = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} - tup = (1, 2) - - -def f( - a: int = 1, -): - NOT_IMPLEMENTED_call() - NOT_IMPLEMENTED_call() - x = NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] - if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: - pass - - -def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key]: - json = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} - - -# The type annotation shouldn't get a trailing comma since that would change its type. -# Relevant bug report: https://github.com/psf/black/issues/2381. -def some_function_with_a_really_long_name() -> ( - returning_a_deeply_nested_import_of_a_type_i_suppose -): - pass - - -def some_method_with_a_really_long_name( - very_long_parameter_so_yeah: str, another_long_parameter: int -) -> another_case_of_returning_a_deeply_nested_import_of_a_type_i_suppose_cause_why_not: - pass - - -def func() -> NOT_IMPLEMENTED_call(): - pass - - -def func() -> NOT_IMPLEMENTED_call(): - pass - - -# Make sure inner one-element tuple won't explode -NOT_IMPLEMENTED_call() - -# Inner trailing comma causes outer to explode -NOT_IMPLEMENTED_call() -``` - -## Black Output - -```py -def f( - a, -): - d = { - "key": "value", - } - tup = (1,) - - -def f2( - a, - b, -): - d = { - "key": "value", - "key2": "value2", - } - tup = ( - 1, - 2, - ) - - -def f( - a: int = 1, -): - call( - arg={ - "explode": "this", - } - ) - call2( - arg=[1, 2, 3], - ) - x = { - "a": 1, - "b": 2, - }["a"] - if ( - a - == { - "a": 1, - "b": 2, - "c": 3, - "d": 4, - "e": 5, - "f": 6, - "g": 7, - "h": 8, - }["a"] - ): - pass - - -def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> ( - Set["xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"] -): - json = { - "k": { - "k2": { - "k3": [ - 1, - ] - } - } - } - - -# The type annotation shouldn't get a trailing comma since that would change its type. -# Relevant bug report: https://github.com/psf/black/issues/2381. -def some_function_with_a_really_long_name() -> ( - returning_a_deeply_nested_import_of_a_type_i_suppose -): - pass - - -def some_method_with_a_really_long_name( - very_long_parameter_so_yeah: str, another_long_parameter: int -) -> another_case_of_returning_a_deeply_nested_import_of_a_type_i_suppose_cause_why_not: - pass - - -def func() -> ( - also_super_long_type_annotation_that_may_cause_an_AST_related_crash_in_black( - this_shouldn_t_get_a_trailing_comma_too - ) -): - pass - - -def func() -> ( - also_super_long_type_annotation_that_may_cause_an_AST_related_crash_in_black( - this_shouldn_t_get_a_trailing_comma_too - ) -): - pass - - -# Make sure inner one-element tuple won't explode -some_module.some_function( - argument1, (one_element_tuple,), argument4, argument5, argument6 -) - -# Inner trailing comma causes outer to explode -some_module.some_function( - argument1, - ( - one, - two, - ), - argument4, - argument5, - argument6, -) -``` - - diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__import_spacing_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__import_spacing_py.snap deleted file mode 100644 index 4160f7edaf..0000000000 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__import_spacing_py.snap +++ /dev/null @@ -1,278 +0,0 @@ ---- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/import_spacing.py ---- -## Input - -```py -"""The asyncio package, tracking PEP 3156.""" - -# flake8: noqa - -from logging import ( - WARNING -) -from logging import ( - ERROR, -) -import sys - -# This relies on each of the submodules having an __all__ variable. -from .base_events import * -from .coroutines import * -from .events import * # comment here - -from .futures import * -from .locks import * # comment here -from .protocols import * - -from ..runners import * # comment here -from ..queues import * -from ..streams import * - -from some_library import ( - Just, Enough, Libraries, To, Fit, In, This, Nice, Split, Which, We, No, Longer, Use -) -from name_of_a_company.extremely_long_project_name.component.ttypes import CuteLittleServiceHandlerFactoryyy -from name_of_a_company.extremely_long_project_name.extremely_long_component_name.ttypes import * - -from .a.b.c.subprocess import * -from . import (tasks) -from . import (A, B, C) -from . import SomeVeryLongNameAndAllOfItsAdditionalLetters1, \ - SomeVeryLongNameAndAllOfItsAdditionalLetters2 - -__all__ = ( - base_events.__all__ - + coroutines.__all__ - + events.__all__ - + futures.__all__ - + locks.__all__ - + protocols.__all__ - + runners.__all__ - + queues.__all__ - + streams.__all__ - + tasks.__all__ -) -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -1,64 +1,42 @@ --"""The asyncio package, tracking PEP 3156.""" -+"NOT_YET_IMPLEMENTED_STRING" - - # flake8: noqa - --from logging import WARNING --from logging import ( -- ERROR, --) --import sys -+NOT_YET_IMPLEMENTED_StmtImportFrom -+NOT_YET_IMPLEMENTED_StmtImportFrom -+NOT_YET_IMPLEMENTED_StmtImport - - # This relies on each of the submodules having an __all__ variable. --from .base_events import * --from .coroutines import * --from .events import * # comment here -+NOT_YET_IMPLEMENTED_StmtImportFrom -+NOT_YET_IMPLEMENTED_StmtImportFrom -+NOT_YET_IMPLEMENTED_StmtImportFrom # comment here - --from .futures import * --from .locks import * # comment here --from .protocols import * -+NOT_YET_IMPLEMENTED_StmtImportFrom -+NOT_YET_IMPLEMENTED_StmtImportFrom # comment here -+NOT_YET_IMPLEMENTED_StmtImportFrom - --from ..runners import * # comment here --from ..queues import * --from ..streams import * -+NOT_YET_IMPLEMENTED_StmtImportFrom # comment here -+NOT_YET_IMPLEMENTED_StmtImportFrom -+NOT_YET_IMPLEMENTED_StmtImportFrom - --from some_library import ( -- Just, -- Enough, -- Libraries, -- To, -- Fit, -- In, -- This, -- Nice, -- Split, -- Which, -- We, -- No, -- Longer, -- Use, --) --from name_of_a_company.extremely_long_project_name.component.ttypes import ( -- CuteLittleServiceHandlerFactoryyy, --) --from name_of_a_company.extremely_long_project_name.extremely_long_component_name.ttypes import * -+NOT_YET_IMPLEMENTED_StmtImportFrom -+NOT_YET_IMPLEMENTED_StmtImportFrom -+NOT_YET_IMPLEMENTED_StmtImportFrom - --from .a.b.c.subprocess import * --from . import tasks --from . import A, B, C --from . import ( -- SomeVeryLongNameAndAllOfItsAdditionalLetters1, -- SomeVeryLongNameAndAllOfItsAdditionalLetters2, --) -+NOT_YET_IMPLEMENTED_StmtImportFrom -+NOT_YET_IMPLEMENTED_StmtImportFrom -+NOT_YET_IMPLEMENTED_StmtImportFrom -+NOT_YET_IMPLEMENTED_StmtImportFrom - - __all__ = ( -- base_events.__all__ -- + coroutines.__all__ -- + events.__all__ -- + futures.__all__ -- + locks.__all__ -- + protocols.__all__ -- + runners.__all__ -- + queues.__all__ -- + streams.__all__ -- + tasks.__all__ -+ base_events.NOT_IMPLEMENTED_attr -+ + coroutines.NOT_IMPLEMENTED_attr -+ + events.NOT_IMPLEMENTED_attr -+ + futures.NOT_IMPLEMENTED_attr -+ + locks.NOT_IMPLEMENTED_attr -+ + protocols.NOT_IMPLEMENTED_attr -+ + runners.NOT_IMPLEMENTED_attr -+ + queues.NOT_IMPLEMENTED_attr -+ + streams.NOT_IMPLEMENTED_attr -+ + tasks.NOT_IMPLEMENTED_attr - ) -``` - -## Ruff Output - -```py -"NOT_YET_IMPLEMENTED_STRING" - -# flake8: noqa - -NOT_YET_IMPLEMENTED_StmtImportFrom -NOT_YET_IMPLEMENTED_StmtImportFrom -NOT_YET_IMPLEMENTED_StmtImport - -# This relies on each of the submodules having an __all__ variable. -NOT_YET_IMPLEMENTED_StmtImportFrom -NOT_YET_IMPLEMENTED_StmtImportFrom -NOT_YET_IMPLEMENTED_StmtImportFrom # comment here - -NOT_YET_IMPLEMENTED_StmtImportFrom -NOT_YET_IMPLEMENTED_StmtImportFrom # comment here -NOT_YET_IMPLEMENTED_StmtImportFrom - -NOT_YET_IMPLEMENTED_StmtImportFrom # comment here -NOT_YET_IMPLEMENTED_StmtImportFrom -NOT_YET_IMPLEMENTED_StmtImportFrom - -NOT_YET_IMPLEMENTED_StmtImportFrom -NOT_YET_IMPLEMENTED_StmtImportFrom -NOT_YET_IMPLEMENTED_StmtImportFrom - -NOT_YET_IMPLEMENTED_StmtImportFrom -NOT_YET_IMPLEMENTED_StmtImportFrom -NOT_YET_IMPLEMENTED_StmtImportFrom -NOT_YET_IMPLEMENTED_StmtImportFrom - -__all__ = ( - base_events.NOT_IMPLEMENTED_attr - + coroutines.NOT_IMPLEMENTED_attr - + events.NOT_IMPLEMENTED_attr - + futures.NOT_IMPLEMENTED_attr - + locks.NOT_IMPLEMENTED_attr - + protocols.NOT_IMPLEMENTED_attr - + runners.NOT_IMPLEMENTED_attr - + queues.NOT_IMPLEMENTED_attr - + streams.NOT_IMPLEMENTED_attr - + tasks.NOT_IMPLEMENTED_attr -) -``` - -## Black Output - -```py -"""The asyncio package, tracking PEP 3156.""" - -# flake8: noqa - -from logging import WARNING -from logging import ( - ERROR, -) -import sys - -# This relies on each of the submodules having an __all__ variable. -from .base_events import * -from .coroutines import * -from .events import * # comment here - -from .futures import * -from .locks import * # comment here -from .protocols import * - -from ..runners import * # comment here -from ..queues import * -from ..streams import * - -from some_library import ( - Just, - Enough, - Libraries, - To, - Fit, - In, - This, - Nice, - Split, - Which, - We, - No, - Longer, - Use, -) -from name_of_a_company.extremely_long_project_name.component.ttypes import ( - CuteLittleServiceHandlerFactoryyy, -) -from name_of_a_company.extremely_long_project_name.extremely_long_component_name.ttypes import * - -from .a.b.c.subprocess import * -from . import tasks -from . import A, B, C -from . import ( - SomeVeryLongNameAndAllOfItsAdditionalLetters1, - SomeVeryLongNameAndAllOfItsAdditionalLetters2, -) - -__all__ = ( - base_events.__all__ - + coroutines.__all__ - + events.__all__ - + futures.__all__ - + locks.__all__ - + protocols.__all__ - + runners.__all__ - + queues.__all__ - + streams.__all__ - + tasks.__all__ -) -``` - - diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__one_element_subscript_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__one_element_subscript_py.snap deleted file mode 100644 index b52d1de815..0000000000 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__one_element_subscript_py.snap +++ /dev/null @@ -1,103 +0,0 @@ ---- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/one_element_subscript.py ---- -## Input - -```py -# We should not treat the trailing comma -# in a single-element subscript. -a: tuple[int,] -b = tuple[int,] - -# The magic comma still applies to multi-element subscripts. -c: tuple[int, int,] -d = tuple[int, int,] - -# Magic commas still work as expected for non-subscripts. -small_list = [1,] -list_of_types = [tuple[int,],] -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -1,22 +1,12 @@ - # We should not treat the trailing comma - # in a single-element subscript. --a: tuple[int,] --b = tuple[int,] -+NOT_YET_IMPLEMENTED_StmtAnnAssign -+b = NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] - - # The magic comma still applies to multi-element subscripts. --c: tuple[ -- int, -- int, --] --d = tuple[ -- int, -- int, --] -+NOT_YET_IMPLEMENTED_StmtAnnAssign -+d = NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] - - # Magic commas still work as expected for non-subscripts. --small_list = [ -- 1, --] --list_of_types = [ -- tuple[int,], --] -+small_list = [1] -+list_of_types = [NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key]] -``` - -## Ruff Output - -```py -# We should not treat the trailing comma -# in a single-element subscript. -NOT_YET_IMPLEMENTED_StmtAnnAssign -b = NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] - -# The magic comma still applies to multi-element subscripts. -NOT_YET_IMPLEMENTED_StmtAnnAssign -d = NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] - -# Magic commas still work as expected for non-subscripts. -small_list = [1] -list_of_types = [NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key]] -``` - -## Black Output - -```py -# We should not treat the trailing comma -# in a single-element subscript. -a: tuple[int,] -b = tuple[int,] - -# The magic comma still applies to multi-element subscripts. -c: tuple[ - int, - int, -] -d = tuple[ - int, - int, -] - -# Magic commas still work as expected for non-subscripts. -small_list = [ - 1, -] -list_of_types = [ - tuple[int,], -] -``` - - diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__prefer_rhs_split_reformatted_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__prefer_rhs_split_reformatted_py.snap deleted file mode 100644 index b917eaf3b0..0000000000 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__prefer_rhs_split_reformatted_py.snap +++ /dev/null @@ -1,93 +0,0 @@ ---- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/prefer_rhs_split_reformatted.py ---- -## Input - -```py -# Test cases separate from `prefer_rhs_split.py` that contains unformatted source. - -# Left hand side fits in a single line but will still be exploded by the -# magic trailing comma. -first_value, (m1, m2,), third_value = xxxxxx_yyyyyy_zzzzzz_wwwwww_uuuuuuu_vvvvvvvvvvv( - arg1, - arg2, -) - -# Make when when the left side of assignment plus the opening paren "... = (" is -# exactly line length limit + 1, it won't be split like that. -xxxxxxxxx_yyy_zzzzzzzz[xx.xxxxxx(x_yyy_zzzzzz.xxxxx[0]), x_yyy_zzzzzz.xxxxxx(xxxx=1)] = 1 -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -2,20 +2,8 @@ - - # Left hand side fits in a single line but will still be exploded by the - # magic trailing comma. --( -- first_value, -- ( -- m1, -- m2, -- ), -- third_value, --) = xxxxxx_yyyyyy_zzzzzz_wwwwww_uuuuuuu_vvvvvvvvvvv( -- arg1, -- arg2, --) -+(1, 2) = NOT_IMPLEMENTED_call() - - # Make when when the left side of assignment plus the opening paren "... = (" is - # exactly line length limit + 1, it won't be split like that. --xxxxxxxxx_yyy_zzzzzzzz[ -- xx.xxxxxx(x_yyy_zzzzzz.xxxxx[0]), x_yyy_zzzzzz.xxxxxx(xxxx=1) --] = 1 -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] = 1 -``` - -## Ruff Output - -```py -# Test cases separate from `prefer_rhs_split.py` that contains unformatted source. - -# Left hand side fits in a single line but will still be exploded by the -# magic trailing comma. -(1, 2) = NOT_IMPLEMENTED_call() - -# Make when when the left side of assignment plus the opening paren "... = (" is -# exactly line length limit + 1, it won't be split like that. -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] = 1 -``` - -## Black Output - -```py -# Test cases separate from `prefer_rhs_split.py` that contains unformatted source. - -# Left hand side fits in a single line but will still be exploded by the -# magic trailing comma. -( - first_value, - ( - m1, - m2, - ), - third_value, -) = xxxxxx_yyyyyy_zzzzzz_wwwwww_uuuuuuu_vvvvvvvvvvv( - arg1, - arg2, -) - -# Make when when the left side of assignment plus the opening paren "... = (" is -# exactly line length limit + 1, it won't be split like that. -xxxxxxxxx_yyy_zzzzzzzz[ - xx.xxxxxx(x_yyy_zzzzzz.xxxxx[0]), x_yyy_zzzzzz.xxxxxx(xxxx=1) -] = 1 -``` - - diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__skip_magic_trailing_comma_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__skip_magic_trailing_comma_py.snap deleted file mode 100644 index b0626e0c31..0000000000 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__skip_magic_trailing_comma_py.snap +++ /dev/null @@ -1,162 +0,0 @@ ---- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/skip_magic_trailing_comma.py ---- -## Input - -```py -# We should not remove the trailing comma in a single-element subscript. -a: tuple[int,] -b = tuple[int,] - -# But commas in multiple element subscripts should be removed. -c: tuple[int, int,] -d = tuple[int, int,] - -# Remove commas for non-subscripts. -small_list = [1,] -list_of_types = [tuple[int,],] -small_set = {1,} -set_of_types = {tuple[int,],} - -# Except single element tuples -small_tuple = (1,) - -# Trailing commas in multiple chained non-nested parens. -zero( - one, -).two( - three, -).four( - five, -) - -func1(arg1).func2(arg2,).func3(arg3).func4(arg4,).func5(arg5) - -( - a, - b, - c, - d, -) = func1( - arg1 -) and func2(arg2) - -func( - argument1, - ( - one, - two, - ), - argument4, - argument5, - argument6, -) -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -1,25 +1,25 @@ - # We should not remove the trailing comma in a single-element subscript. --a: tuple[int,] --b = tuple[int,] -+NOT_YET_IMPLEMENTED_StmtAnnAssign -+b = NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] - - # But commas in multiple element subscripts should be removed. --c: tuple[int, int] --d = tuple[int, int] -+NOT_YET_IMPLEMENTED_StmtAnnAssign -+d = NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] - - # Remove commas for non-subscripts. - small_list = [1] --list_of_types = [tuple[int,]] -+list_of_types = [NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key]] - small_set = {1} --set_of_types = {tuple[int,]} -+set_of_types = {NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key]} - - # Except single element tuples --small_tuple = (1,) -+small_tuple = (1, 2) - - # Trailing commas in multiple chained non-nested parens. --zero(one).two(three).four(five) -+NOT_IMPLEMENTED_call() - --func1(arg1).func2(arg2).func3(arg3).func4(arg4).func5(arg5) -+NOT_IMPLEMENTED_call() - --(a, b, c, d) = func1(arg1) and func2(arg2) -+(1, 2) = NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 - --func(argument1, (one, two), argument4, argument5, argument6) -+NOT_IMPLEMENTED_call() -``` - -## Ruff Output - -```py -# We should not remove the trailing comma in a single-element subscript. -NOT_YET_IMPLEMENTED_StmtAnnAssign -b = NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] - -# But commas in multiple element subscripts should be removed. -NOT_YET_IMPLEMENTED_StmtAnnAssign -d = NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] - -# Remove commas for non-subscripts. -small_list = [1] -list_of_types = [NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key]] -small_set = {1} -set_of_types = {NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key]} - -# Except single element tuples -small_tuple = (1, 2) - -# Trailing commas in multiple chained non-nested parens. -NOT_IMPLEMENTED_call() - -NOT_IMPLEMENTED_call() - -(1, 2) = NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 - -NOT_IMPLEMENTED_call() -``` - -## Black Output - -```py -# We should not remove the trailing comma in a single-element subscript. -a: tuple[int,] -b = tuple[int,] - -# But commas in multiple element subscripts should be removed. -c: tuple[int, int] -d = tuple[int, int] - -# Remove commas for non-subscripts. -small_list = [1] -list_of_types = [tuple[int,]] -small_set = {1} -set_of_types = {tuple[int,]} - -# Except single element tuples -small_tuple = (1,) - -# Trailing commas in multiple chained non-nested parens. -zero(one).two(three).four(five) - -func1(arg1).func2(arg2).func3(arg3).func4(arg4).func5(arg5) - -(a, b, c, d) = func1(arg1) and func2(arg2) - -func(argument1, (one, two), argument4, argument5, argument6) -``` - - diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__slices_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__slices_py.snap deleted file mode 100644 index 6dc17b505b..0000000000 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__slices_py.snap +++ /dev/null @@ -1,277 +0,0 @@ ---- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/slices.py ---- -## Input - -```py -slice[a.b : c.d] -slice[d :: d + 1] -slice[d + 1 :: d] -slice[d::d] -slice[0] -slice[-1] -slice[:-1] -slice[::-1] -slice[:c, c - 1] -slice[c, c + 1, d::] -slice[ham[c::d] :: 1] -slice[ham[cheese**2 : -1] : 1 : 1, ham[1:2]] -slice[:-1:] -slice[lambda: None : lambda: None] -slice[lambda x, y, *args, really=2, **kwargs: None :, None::] -slice[1 or 2 : True and False] -slice[not so_simple : 1 < val <= 10] -slice[(1 for i in range(42)) : x] -slice[:: [i for i in range(42)]] - - -async def f(): - slice[await x : [i async for i in arange(42)] : 42] - - -# These are from PEP-8: -ham[1:9], ham[1:9:3], ham[:9:3], ham[1::3], ham[1:9:] -ham[lower:upper], ham[lower:upper:], ham[lower::step] -# ham[lower+offset : upper+offset] -ham[: upper_fn(x) : step_fn(x)], ham[:: step_fn(x)] -ham[lower + offset : upper + offset] - -slice[::, ::] -slice[ - # A - : - # B - : - # C -] -slice[ - # A - 1: - # B - 2: - # C - 3 -] - -slice[ - # A - 1 - + 2 : - # B - 3 : - # C - 4 -] -x[ - 1: # A - 2: # B - 3 # C -] -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -1,59 +1,38 @@ --slice[a.b : c.d] --slice[d :: d + 1] --slice[d + 1 :: d] --slice[d::d] --slice[0] --slice[-1] --slice[:-1] --slice[::-1] --slice[:c, c - 1] --slice[c, c + 1, d::] --slice[ham[c::d] :: 1] --slice[ham[cheese**2 : -1] : 1 : 1, ham[1:2]] --slice[:-1:] --slice[lambda: None : lambda: None] --slice[lambda x, y, *args, really=2, **kwargs: None :, None::] --slice[1 or 2 : True and False] --slice[not so_simple : 1 < val <= 10] --slice[(1 for i in range(42)) : x] --slice[:: [i for i in range(42)]] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] - - - async def f(): -- slice[await x : [i async for i in arange(42)] : 42] -+ NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] - - - # These are from PEP-8: --ham[1:9], ham[1:9:3], ham[:9:3], ham[1::3], ham[1:9:] --ham[lower:upper], ham[lower:upper:], ham[lower::step] -+(1, 2) -+(1, 2) - # ham[lower+offset : upper+offset] --ham[: upper_fn(x) : step_fn(x)], ham[:: step_fn(x)] --ham[lower + offset : upper + offset] -+(1, 2) -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] - --slice[::, ::] --slice[ -- # A -- : -- # B -- : -- # C --] --slice[ -- # A -- 1: -- # B -- 2: -- # C -- 3 --] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] - --slice[ -- # A -- 1 -- + 2 : -- # B -- 3 : -- # C -- 4 --] --x[1:2:3] # A # B # C -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -``` - -## Ruff Output - -```py -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] - - -async def f(): - NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] - - -# These are from PEP-8: -(1, 2) -(1, 2) -# ham[lower+offset : upper+offset] -(1, 2) -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] - -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] - -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -``` - -## Black Output - -```py -slice[a.b : c.d] -slice[d :: d + 1] -slice[d + 1 :: d] -slice[d::d] -slice[0] -slice[-1] -slice[:-1] -slice[::-1] -slice[:c, c - 1] -slice[c, c + 1, d::] -slice[ham[c::d] :: 1] -slice[ham[cheese**2 : -1] : 1 : 1, ham[1:2]] -slice[:-1:] -slice[lambda: None : lambda: None] -slice[lambda x, y, *args, really=2, **kwargs: None :, None::] -slice[1 or 2 : True and False] -slice[not so_simple : 1 < val <= 10] -slice[(1 for i in range(42)) : x] -slice[:: [i for i in range(42)]] - - -async def f(): - slice[await x : [i async for i in arange(42)] : 42] - - -# These are from PEP-8: -ham[1:9], ham[1:9:3], ham[:9:3], ham[1::3], ham[1:9:] -ham[lower:upper], ham[lower:upper:], ham[lower::step] -# ham[lower+offset : upper+offset] -ham[: upper_fn(x) : step_fn(x)], ham[:: step_fn(x)] -ham[lower + offset : upper + offset] - -slice[::, ::] -slice[ - # A - : - # B - : - # C -] -slice[ - # A - 1: - # B - 2: - # C - 3 -] - -slice[ - # A - 1 - + 2 : - # B - 3 : - # C - 4 -] -x[1:2:3] # A # B # C -``` - - diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__string_prefixes_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__string_prefixes_py.snap deleted file mode 100644 index eb5b0bf26b..0000000000 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__string_prefixes_py.snap +++ /dev/null @@ -1,116 +0,0 @@ ---- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/string_prefixes.py ---- -## Input - -```py -#!/usr/bin/env python3 - -name = "Łukasz" -(f"hello {name}", F"hello {name}") -(b"", B"") -(u"", U"") -(r"", R"") - -(rf"", fr"", Rf"", fR"", rF"", Fr"", RF"", FR"") -(rb"", br"", Rb"", bR"", rB"", Br"", RB"", BR"") - - -def docstring_singleline(): - R"""2020 was one hell of a year. The good news is that we were able to""" - - -def docstring_multiline(): - R""" - clear out all of the issues opened in that time :p - """ -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -1,20 +1,18 @@ - #!/usr/bin/env python3 - --name = "Łukasz" --(f"hello {name}", f"hello {name}") --(b"", b"") --("", "") --(r"", R"") -+name = "NOT_YET_IMPLEMENTED_STRING" -+(1, 2) -+(1, 2) -+(1, 2) -+(1, 2) - --(rf"", rf"", Rf"", Rf"", rf"", rf"", Rf"", Rf"") --(rb"", rb"", Rb"", Rb"", rb"", rb"", Rb"", Rb"") -+(1, 2) -+(1, 2) - - - def docstring_singleline(): -- R"""2020 was one hell of a year. The good news is that we were able to""" -+ "NOT_YET_IMPLEMENTED_STRING" - - - def docstring_multiline(): -- R""" -- clear out all of the issues opened in that time :p -- """ -+ "NOT_YET_IMPLEMENTED_STRING" -``` - -## Ruff Output - -```py -#!/usr/bin/env python3 - -name = "NOT_YET_IMPLEMENTED_STRING" -(1, 2) -(1, 2) -(1, 2) -(1, 2) - -(1, 2) -(1, 2) - - -def docstring_singleline(): - "NOT_YET_IMPLEMENTED_STRING" - - -def docstring_multiline(): - "NOT_YET_IMPLEMENTED_STRING" -``` - -## Black Output - -```py -#!/usr/bin/env python3 - -name = "Łukasz" -(f"hello {name}", f"hello {name}") -(b"", b"") -("", "") -(r"", R"") - -(rf"", rf"", Rf"", Rf"", rf"", rf"", Rf"", Rf"") -(rb"", rb"", Rb"", Rb"", rb"", rb"", Rb"", Rb"") - - -def docstring_singleline(): - R"""2020 was one hell of a year. The good news is that we were able to""" - - -def docstring_multiline(): - R""" - clear out all of the issues opened in that time :p - """ -``` - - diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens1_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens1_py.snap deleted file mode 100644 index 1528c308d1..0000000000 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens1_py.snap +++ /dev/null @@ -1,138 +0,0 @@ ---- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/trailing_comma_optional_parens1.py ---- -## Input - -```py -if e1234123412341234.winerror not in (_winapi.ERROR_SEM_TIMEOUT, - _winapi.ERROR_PIPE_BUSY) or _check_timeout(t): - pass - -if x: - if y: - new_id = max(Vegetable.objects.order_by('-id')[0].id, - Mineral.objects.order_by('-id')[0].id) + 1 - -class X: - def get_help_text(self): - return ngettext( - "Your password must contain at least %(min_length)d character.", - "Your password must contain at least %(min_length)d characters.", - self.min_length, - ) % {'min_length': self.min_length} - -class A: - def b(self): - if self.connection.mysql_is_mariadb and ( - 10, - 4, - 3, - ) < self.connection.mysql_version < (10, 5, 2): - pass -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -1,34 +1,12 @@ --if e1234123412341234.winerror not in ( -- _winapi.ERROR_SEM_TIMEOUT, -- _winapi.ERROR_PIPE_BUSY, --) or _check_timeout(t): -+if NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: - pass - - if x: - if y: -- new_id = ( -- max( -- Vegetable.objects.order_by("-id")[0].id, -- Mineral.objects.order_by("-id")[0].id, -- ) -- + 1 -- ) -+ new_id = NOT_IMPLEMENTED_call() + 1 - - --class X: -- def get_help_text(self): -- return ngettext( -- "Your password must contain at least %(min_length)d character.", -- "Your password must contain at least %(min_length)d characters.", -- self.min_length, -- ) % {"min_length": self.min_length} -+NOT_YET_IMPLEMENTED_StmtClassDef - - --class A: -- def b(self): -- if self.connection.mysql_is_mariadb and ( -- 10, -- 4, -- 3, -- ) < self.connection.mysql_version < (10, 5, 2): -- pass -+NOT_YET_IMPLEMENTED_StmtClassDef -``` - -## Ruff Output - -```py -if NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: - pass - -if x: - if y: - new_id = NOT_IMPLEMENTED_call() + 1 - - -NOT_YET_IMPLEMENTED_StmtClassDef - - -NOT_YET_IMPLEMENTED_StmtClassDef -``` - -## Black Output - -```py -if e1234123412341234.winerror not in ( - _winapi.ERROR_SEM_TIMEOUT, - _winapi.ERROR_PIPE_BUSY, -) or _check_timeout(t): - pass - -if x: - if y: - new_id = ( - max( - Vegetable.objects.order_by("-id")[0].id, - Mineral.objects.order_by("-id")[0].id, - ) - + 1 - ) - - -class X: - def get_help_text(self): - return ngettext( - "Your password must contain at least %(min_length)d character.", - "Your password must contain at least %(min_length)d characters.", - self.min_length, - ) % {"min_length": self.min_length} - - -class A: - def b(self): - if self.connection.mysql_is_mariadb and ( - 10, - 4, - 3, - ) < self.connection.mysql_version < (10, 5, 2): - pass -``` - - diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens2_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens2_py.snap deleted file mode 100644 index 2f591416f5..0000000000 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens2_py.snap +++ /dev/null @@ -1,47 +0,0 @@ ---- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/trailing_comma_optional_parens2.py ---- -## Input - -```py -if (e123456.get_tk_patchlevel() >= (8, 6, 0, 'final') or - (8, 5, 8) <= get_tk_patchlevel() < (8, 6)): - pass -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -1,6 +1,2 @@ --if e123456.get_tk_patchlevel() >= (8, 6, 0, "final") or ( -- 8, -- 5, -- 8, --) <= get_tk_patchlevel() < (8, 6): -+if NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: - pass -``` - -## Ruff Output - -```py -if NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: - pass -``` - -## Black Output - -```py -if e123456.get_tk_patchlevel() >= (8, 6, 0, "final") or ( - 8, - 5, - 8, -) <= get_tk_patchlevel() < (8, 6): - pass -``` - - diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__binary_expression_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__binary_expression_py.snap deleted file mode 100644 index ab5311bf8a..0000000000 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__expression__binary_expression_py.snap +++ /dev/null @@ -1,134 +0,0 @@ ---- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot ---- -## Input -```py -(aaaaaaaa - + # trailing operator comment - b # trailing right comment -) - - -(aaaaaaaa # trailing left comment - + # trailing operator comment - # leading right comment - b -) - - -# Black breaks the right side first for the following expressions: -aaaaaaaaaaaaaa + caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaal(argument1, argument2, argument3) -aaaaaaaaaaaaaa + [bbbbbbbbbbbbbbbbbbbbbb, ccccccccccccccccccccc, dddddddddddddddd, eeeeeee] -aaaaaaaaaaaaaa + (bbbbbbbbbbbbbbbbbbbbbb, ccccccccccccccccccccc, dddddddddddddddd, eeeeeee) -aaaaaaaaaaaaaa + { key1:bbbbbbbbbbbbbbbbbbbbbb, key2: ccccccccccccccccccccc, key3: dddddddddddddddd, key4: eeeeeee } -aaaaaaaaaaaaaa + { bbbbbbbbbbbbbbbbbbbbbb, ccccccccccccccccccccc, dddddddddddddddd, eeeeeee } -aaaaaaaaaaaaaa + [a for x in bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb ] -aaaaaaaaaaaaaa + (a for x in bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb ) -aaaaaaaaaaaaaa + {a for x in bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb} - -# Wraps it in parentheses if it needs to break both left and right -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + [ - bbbbbbbbbbbbbbbbbbbbbb, - ccccccccccccccccccccc, - dddddddddddddddd, - eee -] # comment - - - -# But only for expressions that have a statement parent. -not (aaaaaaaaaaaaaa + {a for x in bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb}) -[a + [bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb] in c ] - - -# leading comment -( - # comment - content + b -) - - -if ( - aaaaaaaaaaaaaaaaaa + - # has the child process finished? - bbbbbbbbbbbbbbb + - # the child process has finished, but the - # transport hasn't been notified yet? - ccccccccccc -): - pass -``` - - - -## Output -```py -( - aaaaaaaa - + b # trailing operator comment # trailing right comment -) - - -( - aaaaaaaa # trailing left comment - + # trailing operator comment - # leading right comment - b -) - - -# Black breaks the right side first for the following expressions: -aaaaaaaaaaaaaa + NOT_IMPLEMENTED_call() -aaaaaaaaaaaaaa + [ - bbbbbbbbbbbbbbbbbbbbbb, - ccccccccccccccccccccc, - dddddddddddddddd, - eeeeeee, -] -aaaaaaaaaaaaaa + (1, 2) -aaaaaaaaaaaaaa + {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} -aaaaaaaaaaaaaa + { - bbbbbbbbbbbbbbbbbbbbbb, - ccccccccccccccccccccc, - dddddddddddddddd, - eeeeeee, -} -aaaaaaaaaaaaaa + [i for i in []] -aaaaaaaaaaaaaa + (i for i in []) -aaaaaaaaaaaaaa + NOT_YET_IMPLEMENTED_ExprSetComp - -# Wraps it in parentheses if it needs to break both left and right -( - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - + [bbbbbbbbbbbbbbbbbbbbbb, ccccccccccccccccccccc, dddddddddddddddd, eee] -) # comment - - -# But only for expressions that have a statement parent. -NOT_YET_IMPLEMENTED_ExprUnaryOp -[NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right] - - -# leading comment -( - # comment - content - + b -) - - -if ( - aaaaaaaaaaaaaaaaaa - + - # has the child process finished? - bbbbbbbbbbbbbbb - + - # the child process has finished, but the - # transport hasn't been notified yet? - ccccccccccc -): - pass -``` - - diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__function_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__function_py.snap deleted file mode 100644 index 3d448396f6..0000000000 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__function_py.snap +++ /dev/null @@ -1,182 +0,0 @@ ---- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot ---- -## Input -```py -# Dangling comments -def test( - # comment - - # another - -): ... - - -# Argument empty line spacing -def test( - # comment - a, - - # another - - b, -): ... - - -### Different function argument wrappings - -def single_line(aaaaaaaaaaaaaaaaaaaaaaaaaaaaa, bbbbbbbbbbbbbbb, ccccccccccccccccc): - pass - -def arguments_on_their_own_line(aaaaaaaaaaaaaaaaaaaaaaaaaaaaa, bbbbbbbbbbbbbbb, ccccccccccc, ddddddddddddd, eeeeeee): - pass - -def argument_per_line(aaaaaaaaaaaaaaaaaaaaaaaaaaaaa, bbbbbbbbbbbbbbb, ccccccccccccccccc, ddddddddddddd, eeeeeeeeeeeeeeee, ffffffffffff): - pass - -def last_pos_only_trailing_comma(a, b, /,): - pass - -def last_pos_no_trailing_comma(a, b, /): - pass - - -def varg_with_leading_comments( - a, b, - # comment - *args -): ... - -def kwarg_with_leading_comments( - a, b, - # comment - **kwargs -): ... - -def argument_with_long_default( - a, - b = ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc + [ - dddddddddddddddddddd, eeeeeeeeeeeeeeeeeeee, ffffffffffffffffffffffff - ], - h = [] -): ... - - -def argument_with_long_type_annotation( - a, - b: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx | yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy | zzzzzzzzzzzzzzzzzzz = [0, 1, 2, 3], - h = [] -): ... - - -def test(): ... - -# Comment -def with_leading_comment(): ... -``` - - - -## Output -```py -# Dangling comments -def test( - # comment - # another -): - ... - - -# Argument empty line spacing -def test( - # comment - a, - # another - b, -): - ... - - -### Different function argument wrappings - -def single_line(aaaaaaaaaaaaaaaaaaaaaaaaaaaaa, bbbbbbbbbbbbbbb, ccccccccccccccccc): - pass - - -def arguments_on_their_own_line( - aaaaaaaaaaaaaaaaaaaaaaaaaaaaa, bbbbbbbbbbbbbbb, ccccccccccc, ddddddddddddd, eeeeeee -): - pass - - -def argument_per_line( - aaaaaaaaaaaaaaaaaaaaaaaaaaaaa, - bbbbbbbbbbbbbbb, - ccccccccccccccccc, - ddddddddddddd, - eeeeeeeeeeeeeeee, - ffffffffffff, -): - pass - - -def last_pos_only_trailing_comma( - a, - b, - /, -): - pass - - -def last_pos_no_trailing_comma(a, b, /): - pass - - -def varg_with_leading_comments( - a, - b, - # comment - *args, -): - ... - - -def kwarg_with_leading_comments( - a, - b, - # comment - **kwargs, -): - ... - - -def argument_with_long_default( - a, - b=ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc - + [dddddddddddddddddddd, eeeeeeeeeeeeeeeeeeee, ffffffffffffffffffffffff], - h=[], -): - ... - - -def argument_with_long_type_annotation( - a, - b: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx - | yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy - | zzzzzzzzzzzzzzzzzzz = [0, 1, 2, 3], - h=[], -): - ... - - -def test(): - ... - - -# Comment -def with_leading_comment(): - ... -``` - - diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__if_statement_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__if_statement_py.snap deleted file mode 100644 index a58a992e81..0000000000 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__if_statement_py.snap +++ /dev/null @@ -1,88 +0,0 @@ ---- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot ---- -## Input -```py -if x == y: # trailing if condition - pass # trailing `pass` comment - # Root `if` trailing comment - -# Leading elif comment -elif x < y: # trailing elif condition - pass - # `elif` trailing comment - -# Leading else comment -else: # trailing else condition - pass - # `else` trailing comment - - -if x == y: - if y == z: - ... - - if a == b: - ... - else: # trailing comment - ... - - # trailing else comment - -# leading else if comment -elif aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + [ - 11111111111111111111111111, - 2222222222222222222222, - 3333333333 - ]: - ... - - -else: - ... -``` - - - -## Output -```py -if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: # trailing if condition - pass # trailing `pass` comment - # Root `if` trailing comment - -# Leading elif comment -elif NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: # trailing elif condition - pass - # `elif` trailing comment - -# Leading else comment -else: # trailing else condition - pass - # `else` trailing comment - - -if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: - if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: - ... - - if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: - ... - else: # trailing comment - ... - - # trailing else comment - -# leading else if comment -elif aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + [ - 11111111111111111111111111, - 2222222222222222222222, - 3333333333, -]: - ... - -else: - ... -``` - - diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__identifier_ending_in_non_start_char.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__identifier_ending_in_non_start_char.snap new file mode 100644 index 0000000000..15e9d84407 --- /dev/null +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__identifier_ending_in_non_start_char.snap @@ -0,0 +1,10 @@ +--- +source: crates/ruff_python_formatter/src/trivia.rs +expression: test_case.tokens() +--- +[ + Token { + kind: Other, + range: 0..2, + }, +] diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__ignore_word_with_only_id_continuing_chars.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__ignore_word_with_only_id_continuing_chars.snap new file mode 100644 index 0000000000..26e9fd18bc --- /dev/null +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__ignore_word_with_only_id_continuing_chars.snap @@ -0,0 +1,18 @@ +--- +source: crates/ruff_python_formatter/src/trivia.rs +expression: test_case.tokens() +--- +[ + Token { + kind: Other, + range: 0..1, + }, + Token { + kind: Bogus, + range: 1..2, + }, + Token { + kind: Bogus, + range: 2..3, + }, +] diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tokenize_comma.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tokenize_comma.snap index aade2db2c9..38d1fed60a 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tokenize_comma.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tokenize_comma.snap @@ -1,6 +1,6 @@ --- source: crates/ruff_python_formatter/src/trivia.rs -expression: tokens +expression: test_case.tokens() --- [ Token { diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tokenize_continuation.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tokenize_continuation.snap index b537ae611c..83079fe81a 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tokenize_continuation.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tokenize_continuation.snap @@ -1,6 +1,6 @@ --- source: crates/ruff_python_formatter/src/trivia.rs -expression: tokens +expression: test_case.tokens() --- [ Token { diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tokenize_multichar.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tokenize_multichar.snap new file mode 100644 index 0000000000..16a1293b44 --- /dev/null +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tokenize_multichar.snap @@ -0,0 +1,34 @@ +--- +source: crates/ruff_python_formatter/src/trivia.rs +expression: test_case.tokens() +--- +[ + Token { + kind: If, + range: 0..2, + }, + Token { + kind: Whitespace, + range: 2..3, + }, + Token { + kind: In, + range: 3..5, + }, + Token { + kind: Whitespace, + range: 5..6, + }, + Token { + kind: Else, + range: 6..10, + }, + Token { + kind: Whitespace, + range: 10..11, + }, + Token { + kind: Match, + range: 11..16, + }, +] diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tokenize_parentheses.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tokenize_parentheses.snap index f9de9526ae..ccd6969c2d 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tokenize_parentheses.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tokenize_parentheses.snap @@ -1,6 +1,6 @@ --- source: crates/ruff_python_formatter/src/trivia.rs -expression: tokens +expression: test_case.tokens() --- [ Token { diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tokenize_substring.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tokenize_substring.snap index 747d504c4b..181b438c3f 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tokenize_substring.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tokenize_substring.snap @@ -1,6 +1,6 @@ --- source: crates/ruff_python_formatter/src/trivia.rs -expression: tokens +expression: test_case.tokens() --- [ Token { diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tokenize_trivia.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tokenize_trivia.snap index 685a032be7..f1d708d6cb 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tokenize_trivia.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tokenize_trivia.snap @@ -1,6 +1,6 @@ --- source: crates/ruff_python_formatter/src/trivia.rs -expression: tokens +expression: test_case.tokens() --- [ Token { diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tricky_unicode.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tricky_unicode.snap new file mode 100644 index 0000000000..91b9cb397a --- /dev/null +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__trivia__tests__tricky_unicode.snap @@ -0,0 +1,10 @@ +--- +source: crates/ruff_python_formatter/src/trivia.rs +expression: test_case.tokens() +--- +[ + Token { + kind: Other, + range: 0..6, + }, +] diff --git a/crates/ruff_python_formatter/src/statement/mod.rs b/crates/ruff_python_formatter/src/statement/mod.rs index 42a18d1d3e..4d5804a847 100644 --- a/crates/ruff_python_formatter/src/statement/mod.rs +++ b/crates/ruff_python_formatter/src/statement/mod.rs @@ -70,14 +70,16 @@ impl FormatRule> for FormatStmt { impl<'ast> AsFormat> for Stmt { type Format<'a> = FormatRefWithRule<'a, Stmt, FormatStmt, PyFormatContext<'ast>>; + fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new(self, FormatStmt::default()) + FormatRefWithRule::new(self, FormatStmt) } } impl<'ast> IntoFormat> for Stmt { type Format = FormatOwnedWithRule>; + fn into_format(self) -> Self::Format { - FormatOwnedWithRule::new(self, FormatStmt::default()) + FormatOwnedWithRule::new(self, FormatStmt) } } diff --git a/crates/ruff_python_formatter/src/statement/stmt_ann_assign.rs b/crates/ruff_python_formatter/src/statement/stmt_ann_assign.rs index 645808d940..418b4d52c1 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_ann_assign.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_ann_assign.rs @@ -1,5 +1,8 @@ -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; +use crate::expression::maybe_parenthesize_expression; +use crate::expression::parentheses::Parenthesize; +use crate::prelude::*; +use crate::FormatNodeRule; +use ruff_formatter::write; use rustpython_parser::ast::StmtAnnAssign; #[derive(Default)] @@ -7,6 +10,36 @@ pub struct FormatStmtAnnAssign; impl FormatNodeRule for FormatStmtAnnAssign { fn fmt_fields(&self, item: &StmtAnnAssign, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + let StmtAnnAssign { + range: _, + target, + annotation, + value, + simple: _, + } = item; + + write!( + f, + [ + target.format(), + text(":"), + space(), + maybe_parenthesize_expression(annotation, item, Parenthesize::IfBreaks) + ] + )?; + + if let Some(value) = value { + write!( + f, + [ + space(), + text("="), + space(), + maybe_parenthesize_expression(value, item, Parenthesize::IfBreaks) + ] + )?; + } + + Ok(()) } } diff --git a/crates/ruff_python_formatter/src/statement/stmt_assign.rs b/crates/ruff_python_formatter/src/statement/stmt_assign.rs index 79182dafd9..36a53891b5 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_assign.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_assign.rs @@ -1,13 +1,12 @@ -use crate::context::PyFormatContext; -use crate::expression::parentheses::Parenthesize; -use crate::{AsFormat, FormatNodeRule, PyFormatter}; -use ruff_formatter::formatter::Formatter; -use ruff_formatter::prelude::{space, text}; -use ruff_formatter::{write, Buffer, Format, FormatResult}; -use ruff_python_ast::prelude::Expr; use rustpython_parser::ast::StmtAssign; -// +use ruff_formatter::write; + +use crate::expression::maybe_parenthesize_expression; +use crate::expression::parentheses::Parenthesize; +use crate::prelude::*; +use crate::FormatNodeRule; + // Note: This currently does wrap but not the black way so the types below likely need to be // replaced entirely // @@ -23,32 +22,18 @@ impl FormatNodeRule for FormatStmtAssign { value, type_comment: _, } = item; + + for target in targets { + write!(f, [target.format(), space(), text("="), space()])?; + } + write!( f, - [ - LhsAssignList::new(targets), - value.format().with_options(Parenthesize::IfBreaks) - ] + [maybe_parenthesize_expression( + value, + item, + Parenthesize::IfBreaks + )] ) } } - -#[derive(Debug)] -struct LhsAssignList<'a> { - lhs_assign_list: &'a [Expr], -} - -impl<'a> LhsAssignList<'a> { - const fn new(lhs_assign_list: &'a [Expr]) -> Self { - Self { lhs_assign_list } - } -} - -impl Format> for LhsAssignList<'_> { - fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { - for element in self.lhs_assign_list { - write!(f, [&element.format(), space(), text("="), space(),])?; - } - Ok(()) - } -} diff --git a/crates/ruff_python_formatter/src/statement/stmt_async_function_def.rs b/crates/ruff_python_formatter/src/statement/stmt_async_function_def.rs index a0e28aa381..1a001d91b7 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_async_function_def.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_async_function_def.rs @@ -1,7 +1,9 @@ +use rustpython_parser::ast::StmtAsyncFunctionDef; + +use ruff_python_ast::function::AnyFunctionDefinition; + use crate::prelude::*; use crate::FormatNodeRule; -use ruff_python_ast::function::AnyFunctionDefinition; -use rustpython_parser::ast::StmtAsyncFunctionDef; #[derive(Default)] pub struct FormatStmtAsyncFunctionDef; diff --git a/crates/ruff_python_formatter/src/statement/stmt_async_with.rs b/crates/ruff_python_formatter/src/statement/stmt_async_with.rs index 0555642a10..e8d0e0ebd9 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_async_with.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_async_with.rs @@ -1,5 +1,6 @@ -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; +use crate::prelude::*; +use crate::statement::stmt_with::AnyStatementWith; +use crate::FormatNodeRule; use rustpython_parser::ast::StmtAsyncWith; #[derive(Default)] @@ -7,6 +8,15 @@ pub struct FormatStmtAsyncWith; impl FormatNodeRule for FormatStmtAsyncWith { fn fmt_fields(&self, item: &StmtAsyncWith, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + AnyStatementWith::from(item).fmt(f) + } + + fn fmt_dangling_comments( + &self, + _node: &StmtAsyncWith, + _f: &mut PyFormatter, + ) -> FormatResult<()> { + // Handled in `fmt_fields` + Ok(()) } } diff --git a/crates/ruff_python_formatter/src/statement/stmt_aug_assign.rs b/crates/ruff_python_formatter/src/statement/stmt_aug_assign.rs index 8db5a6a8d4..6682ef547b 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_aug_assign.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_aug_assign.rs @@ -1,4 +1,7 @@ -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; +use crate::expression::maybe_parenthesize_expression; +use crate::expression::parentheses::Parenthesize; +use crate::{AsFormat, FormatNodeRule, PyFormatter}; +use ruff_formatter::prelude::{space, text}; use ruff_formatter::{write, Buffer, FormatResult}; use rustpython_parser::ast::StmtAugAssign; @@ -7,6 +10,22 @@ pub struct FormatStmtAugAssign; impl FormatNodeRule for FormatStmtAugAssign { fn fmt_fields(&self, item: &StmtAugAssign, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + let StmtAugAssign { + target, + op, + value, + range: _, + } = item; + write!( + f, + [ + target.format(), + space(), + op.format(), + text("="), + space(), + maybe_parenthesize_expression(value, item, Parenthesize::IfBreaks) + ] + ) } } diff --git a/crates/ruff_python_formatter/src/statement/stmt_break.rs b/crates/ruff_python_formatter/src/statement/stmt_break.rs index f3e9af3ca2..53cb75f6cc 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_break.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_break.rs @@ -1,12 +1,13 @@ -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; +use crate::{FormatNodeRule, PyFormatter}; +use ruff_formatter::prelude::text; +use ruff_formatter::{Format, FormatResult}; use rustpython_parser::ast::StmtBreak; #[derive(Default)] pub struct FormatStmtBreak; impl FormatNodeRule for FormatStmtBreak { - fn fmt_fields(&self, item: &StmtBreak, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + fn fmt_fields(&self, _item: &StmtBreak, f: &mut PyFormatter) -> FormatResult<()> { + text("break").fmt(f) } } diff --git a/crates/ruff_python_formatter/src/statement/stmt_class_def.rs b/crates/ruff_python_formatter/src/statement/stmt_class_def.rs index 5c2f8db3ab..bd903d8b39 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_class_def.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_class_def.rs @@ -1,12 +1,116 @@ -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; -use rustpython_parser::ast::StmtClassDef; +use crate::comments::trailing_comments; + +use crate::expression::parentheses::Parentheses; +use crate::prelude::*; +use crate::trivia::{SimpleTokenizer, TokenKind}; +use ruff_formatter::{format_args, write}; +use ruff_text_size::TextRange; +use rustpython_parser::ast::{Ranged, StmtClassDef}; #[derive(Default)] pub struct FormatStmtClassDef; impl FormatNodeRule for FormatStmtClassDef { fn fmt_fields(&self, item: &StmtClassDef, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + let StmtClassDef { + range: _, + name, + bases, + keywords, + body, + decorator_list, + } = item; + + f.join_with(hard_line_break()) + .entries(decorator_list.iter().formatted()) + .finish()?; + + if !decorator_list.is_empty() { + hard_line_break().fmt(f)?; + } + + write!(f, [text("class"), space(), name.format()])?; + + if !(bases.is_empty() && keywords.is_empty()) { + write!( + f, + [group(&format_args![ + text("("), + soft_block_indent(&FormatInheritanceClause { + class_definition: item + }), + text(")") + ])] + )?; + } + + let comments = f.context().comments().clone(); + let trailing_head_comments = comments.dangling_comments(item); + + write!( + f, + [ + text(":"), + trailing_comments(trailing_head_comments), + block_indent(&body.format()) + ] + ) + } + + fn fmt_dangling_comments( + &self, + _node: &StmtClassDef, + _f: &mut PyFormatter, + ) -> FormatResult<()> { + // handled in fmt_fields + Ok(()) + } +} + +struct FormatInheritanceClause<'a> { + class_definition: &'a StmtClassDef, +} + +impl Format> for FormatInheritanceClause<'_> { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + let StmtClassDef { + bases, + keywords, + name, + body, + .. + } = self.class_definition; + + let source = f.context().source(); + + let mut joiner = f.join_comma_separated(body.first().unwrap().start()); + + if let Some((first, rest)) = bases.split_first() { + // Manually handle parentheses for the first expression because the logic in `FormatExpr` + // doesn't know that it should disregard the parentheses of the inheritance clause. + // ```python + // class Test(A) # A is not parenthesized, the parentheses belong to the inheritance clause + // class Test((A)) # A is parenthesized + // ``` + // parentheses from the inheritance clause belong to the expression. + let tokenizer = SimpleTokenizer::new(source, TextRange::new(name.end(), first.start())) + .skip_trivia(); + + let left_paren_count = tokenizer + .take_while(|token| token.kind() == TokenKind::LParen) + .count(); + + // Ignore the first parentheses count + let parentheses = if left_paren_count > 1 { + Parentheses::Always + } else { + Parentheses::Never + }; + + joiner.entry(first, &first.format().with_options(parentheses)); + joiner.nodes(rest.iter()); + } + + joiner.nodes(keywords.iter()).finish() } } diff --git a/crates/ruff_python_formatter/src/statement/stmt_continue.rs b/crates/ruff_python_formatter/src/statement/stmt_continue.rs index b216ba7c46..d6403fd1bf 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_continue.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_continue.rs @@ -1,12 +1,13 @@ -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; +use crate::{FormatNodeRule, PyFormatter}; +use ruff_formatter::prelude::text; +use ruff_formatter::{Format, FormatResult}; use rustpython_parser::ast::StmtContinue; #[derive(Default)] pub struct FormatStmtContinue; impl FormatNodeRule for FormatStmtContinue { - fn fmt_fields(&self, item: &StmtContinue, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + fn fmt_fields(&self, _item: &StmtContinue, f: &mut PyFormatter) -> FormatResult<()> { + text("continue").fmt(f) } } diff --git a/crates/ruff_python_formatter/src/statement/stmt_delete.rs b/crates/ruff_python_formatter/src/statement/stmt_delete.rs index 0a75dcd016..a61e86c028 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_delete.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_delete.rs @@ -1,12 +1,60 @@ -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; -use rustpython_parser::ast::StmtDelete; +use crate::builders::{parenthesize_if_expands, PyFormatterExtensions}; +use crate::comments::dangling_node_comments; +use crate::expression::maybe_parenthesize_expression; +use crate::expression::parentheses::Parenthesize; +use crate::{FormatNodeRule, PyFormatter}; +use ruff_formatter::prelude::{block_indent, format_with, space, text}; +use ruff_formatter::{write, Buffer, Format, FormatResult}; +use rustpython_parser::ast::{Ranged, StmtDelete}; #[derive(Default)] pub struct FormatStmtDelete; impl FormatNodeRule for FormatStmtDelete { fn fmt_fields(&self, item: &StmtDelete, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + let StmtDelete { range: _, targets } = item; + + write!(f, [text("del"), space()])?; + + match targets.as_slice() { + [] => { + write!( + f, + [ + // Handle special case of delete statements without targets. + // ``` + // del ( + // # Dangling comment + // ) + &text("("), + block_indent(&dangling_node_comments(item)), + &text(")"), + ] + ) + } + [single] => { + write!( + f, + [maybe_parenthesize_expression( + single, + item, + Parenthesize::IfBreaks + )] + ) + } + targets => { + let item = format_with(|f| { + f.join_comma_separated(item.end()) + .nodes(targets.iter()) + .finish() + }); + parenthesize_if_expands(&item).fmt(f) + } + } + } + + fn fmt_dangling_comments(&self, _node: &StmtDelete, _f: &mut PyFormatter) -> FormatResult<()> { + // Handled in `fmt_fields` + Ok(()) } } diff --git a/crates/ruff_python_formatter/src/statement/stmt_expr.rs b/crates/ruff_python_formatter/src/statement/stmt_expr.rs index ffb86ee9df..0eb13b2493 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_expr.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_expr.rs @@ -1,7 +1,9 @@ +use rustpython_parser::ast::StmtExpr; + +use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::Parenthesize; use crate::prelude::*; use crate::FormatNodeRule; -use rustpython_parser::ast::StmtExpr; #[derive(Default)] pub struct FormatStmtExpr; @@ -10,6 +12,6 @@ impl FormatNodeRule for FormatStmtExpr { fn fmt_fields(&self, item: &StmtExpr, f: &mut PyFormatter) -> FormatResult<()> { let StmtExpr { value, .. } = item; - value.format().with_options(Parenthesize::Optional).fmt(f) + maybe_parenthesize_expression(value, item, Parenthesize::Optional).fmt(f) } } diff --git a/crates/ruff_python_formatter/src/statement/stmt_for.rs b/crates/ruff_python_formatter/src/statement/stmt_for.rs index b943de7cc1..b03283cabc 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_for.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_for.rs @@ -1,12 +1,92 @@ -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; +use crate::comments::{leading_alternate_branch_comments, trailing_comments}; +use crate::expression::expr_tuple::TupleParentheses; +use crate::expression::maybe_parenthesize_expression; +use crate::expression::parentheses::Parenthesize; +use crate::prelude::*; +use crate::{FormatNodeRule, PyFormatter}; use ruff_formatter::{write, Buffer, FormatResult}; -use rustpython_parser::ast::StmtFor; +use ruff_python_ast::node::AstNode; +use rustpython_parser::ast::{Expr, Ranged, Stmt, StmtFor}; + +#[derive(Debug)] +struct ExprTupleWithoutParentheses<'a>(&'a Expr); + +impl Format> for ExprTupleWithoutParentheses<'_> { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + match self.0 { + Expr::Tuple(expr_tuple) => expr_tuple + .format() + .with_options(TupleParentheses::StripInsideForLoop) + .fmt(f), + other => maybe_parenthesize_expression(other, self.0, Parenthesize::IfBreaks).fmt(f), + } + } +} #[derive(Default)] pub struct FormatStmtFor; impl FormatNodeRule for FormatStmtFor { fn fmt_fields(&self, item: &StmtFor, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + let StmtFor { + range: _, + target, + iter, + body, + orelse, + type_comment: _, + } = item; + + let comments = f.context().comments().clone(); + let dangling_comments = comments.dangling_comments(item.as_any_node_ref()); + let body_start = body.first().map_or(iter.end(), Stmt::start); + let or_else_comments_start = + dangling_comments.partition_point(|comment| comment.slice().end() < body_start); + + let (trailing_condition_comments, or_else_comments) = + dangling_comments.split_at(or_else_comments_start); + + write!( + f, + [ + text("for"), + space(), + ExprTupleWithoutParentheses(target.as_ref()), + space(), + text("in"), + space(), + maybe_parenthesize_expression(iter, item, Parenthesize::IfBreaks), + text(":"), + trailing_comments(trailing_condition_comments), + block_indent(&body.format()) + ] + )?; + + if orelse.is_empty() { + debug_assert!(or_else_comments.is_empty()); + } else { + // Split between leading comments before the `else` keyword and end of line comments at the end of + // the `else:` line. + let trailing_start = + or_else_comments.partition_point(|comment| comment.line_position().is_own_line()); + let (leading, trailing) = or_else_comments.split_at(trailing_start); + + write!( + f, + [ + leading_alternate_branch_comments(leading, body.last()), + text("else:"), + trailing_comments(trailing), + block_indent(&orelse.format()) + ] + )?; + } + + Ok(()) + } + + fn fmt_dangling_comments(&self, _node: &StmtFor, _f: &mut PyFormatter) -> FormatResult<()> { + // Handled in `fmt_fields` + Ok(()) } } diff --git a/crates/ruff_python_formatter/src/statement/stmt_function_def.rs b/crates/ruff_python_formatter/src/statement/stmt_function_def.rs index ed48d4820d..69f370e7c1 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_function_def.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_function_def.rs @@ -1,12 +1,14 @@ +use rustpython_parser::ast::{Ranged, StmtFunctionDef}; + +use ruff_formatter::{write, FormatOwnedWithRule, FormatRefWithRule}; +use ruff_python_ast::function::AnyFunctionDefinition; + use crate::comments::{leading_comments, trailing_comments}; use crate::context::NodeLevel; -use crate::expression::parentheses::Parenthesize; +use crate::expression::parentheses::{optional_parentheses, Parentheses}; use crate::prelude::*; use crate::trivia::{lines_after, skip_trailing_trivia}; use crate::FormatNodeRule; -use ruff_formatter::{write, FormatOwnedWithRule, FormatRefWithRule}; -use ruff_python_ast::function::AnyFunctionDefinition; -use rustpython_parser::ast::{Ranged, StmtFunctionDef}; #[derive(Default)] pub struct FormatStmtFunctionDef; @@ -37,9 +39,9 @@ impl FormatRule, PyFormatContext<'_>> for FormatAnyFun ) -> FormatResult<()> { let comments = f.context().comments().clone(); - let dangling_comments = comments.dangling_comments(item.into()); + let dangling_comments = comments.dangling_comments(item); let trailing_definition_comments_start = - dangling_comments.partition_point(|comment| comment.position().is_own_line()); + dangling_comments.partition_point(|comment| comment.line_position().is_own_line()); let (leading_function_definition_comments, trailing_definition_comments) = dangling_comments.split_at(trailing_definition_comments_start); @@ -56,9 +58,9 @@ impl FormatRule, PyFormatContext<'_>> for FormatAnyFun // while maintaining the right amount of empty lines between the comment // and the last decorator. let decorator_end = - skip_trailing_trivia(last_decorator.end(), f.context().contents()); + skip_trailing_trivia(last_decorator.end(), f.context().source()); - let leading_line = if lines_after(decorator_end, f.context().contents()) <= 1 { + let leading_line = if lines_after(decorator_end, f.context().source()) <= 1 { hard_line_break() } else { empty_line() @@ -85,7 +87,7 @@ impl FormatRule, PyFormatContext<'_>> for FormatAnyFun [ text("def"), space(), - dynamic_text(name.as_str(), None), + name.format(), item.arguments().format(), ] )?; @@ -97,9 +99,9 @@ impl FormatRule, PyFormatContext<'_>> for FormatAnyFun space(), text("->"), space(), - return_annotation - .format() - .with_options(Parenthesize::IfBreaks) + optional_parentheses( + &return_annotation.format().with_options(Parentheses::Never) + ) ] )?; } @@ -124,7 +126,7 @@ impl<'def, 'ast> AsFormat> for AnyFunctionDefinition<'def> > where Self: 'a; fn format(&self) -> Self::Format<'_> { - FormatRefWithRule::new(self, FormatAnyFunctionDef::default()) + FormatRefWithRule::new(self, FormatAnyFunctionDef) } } diff --git a/crates/ruff_python_formatter/src/statement/stmt_if.rs b/crates/ruff_python_formatter/src/statement/stmt_if.rs index 33fad5466f..ab249f228e 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_if.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_if.rs @@ -1,4 +1,5 @@ use crate::comments::{leading_alternate_branch_comments, trailing_comments, SourceComment}; +use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::Parenthesize; use crate::prelude::*; use crate::FormatNodeRule; @@ -23,7 +24,7 @@ impl FormatNodeRule for FormatStmtIf { } = current_statement; let first_statement = body.first().ok_or(FormatError::SyntaxError)?; - let trailing = comments.dangling_comments(current_statement.into()); + let trailing = comments.dangling_comments(current_statement); let trailing_if_comments_end = trailing .partition_point(|comment| comment.slice().start() < first_statement.start()); @@ -32,7 +33,7 @@ impl FormatNodeRule for FormatStmtIf { trailing.split_at(trailing_if_comments_end); if current.is_elif() { - let elif_leading = comments.leading_comments(current_statement.into()); + let elif_leading = comments.leading_comments(current_statement); // Manually format the leading comments because the formatting bypasses `NodeRule::fmt` write!( f, @@ -48,7 +49,7 @@ impl FormatNodeRule for FormatStmtIf { [ text(current.keyword()), space(), - test.format().with_options(Parenthesize::IfBreaks), + maybe_parenthesize_expression(test, current_statement, Parenthesize::IfBreaks), text(":"), trailing_comments(if_trailing_comments), block_indent(&body.format()) @@ -73,7 +74,7 @@ impl FormatNodeRule for FormatStmtIf { if !orelse.is_empty() { // Leading comments are always own line comments let leading_else_comments_end = - else_comments.partition_point(|comment| comment.position().is_own_line()); + else_comments.partition_point(|comment| comment.line_position().is_own_line()); let (else_leading, else_trailing) = else_comments.split_at(leading_else_comments_end); write!( diff --git a/crates/ruff_python_formatter/src/statement/stmt_import.rs b/crates/ruff_python_formatter/src/statement/stmt_import.rs index 2585dfade7..10aec39721 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_import.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_import.rs @@ -1,4 +1,5 @@ -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; +use crate::{FormatNodeRule, FormattedIterExt, PyFormatter}; +use ruff_formatter::prelude::{format_args, format_with, space, text}; use ruff_formatter::{write, Buffer, FormatResult}; use rustpython_parser::ast::StmtImport; @@ -7,6 +8,12 @@ pub struct FormatStmtImport; impl FormatNodeRule for FormatStmtImport { fn fmt_fields(&self, item: &StmtImport, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + let StmtImport { names, range: _ } = item; + let names = format_with(|f| { + f.join_with(&format_args![text(","), space()]) + .entries(names.iter().formatted()) + .finish() + }); + write!(f, [text("import"), space(), names]) } } diff --git a/crates/ruff_python_formatter/src/statement/stmt_import_from.rs b/crates/ruff_python_formatter/src/statement/stmt_import_from.rs index bdae4d56ba..6611899906 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_import_from.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_import_from.rs @@ -1,12 +1,48 @@ -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; -use rustpython_parser::ast::StmtImportFrom; +use crate::builders::{parenthesize_if_expands, PyFormatterExtensions}; +use crate::{AsFormat, FormatNodeRule, PyFormatter}; +use ruff_formatter::prelude::{dynamic_text, format_with, space, text}; +use ruff_formatter::{write, Buffer, Format, FormatResult}; +use rustpython_parser::ast::{Ranged, StmtImportFrom}; #[derive(Default)] pub struct FormatStmtImportFrom; impl FormatNodeRule for FormatStmtImportFrom { fn fmt_fields(&self, item: &StmtImportFrom, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + let StmtImportFrom { + module, + names, + range: _, + level, + } = item; + + let level_str = level + .map(|level| ".".repeat(level.to_usize())) + .unwrap_or(String::default()); + + write!( + f, + [ + text("from"), + space(), + dynamic_text(&level_str, None), + module.as_ref().map(AsFormat::format), + space(), + text("import"), + space(), + ] + )?; + if let [name] = names.as_slice() { + // star can't be surrounded by parentheses + if name.name.as_str() == "*" { + return text("*").fmt(f); + } + } + let names = format_with(|f| { + f.join_comma_separated(item.end()) + .entries(names.iter().map(|name| (name, name.format()))) + .finish() + }); + parenthesize_if_expands(&names).fmt(f) } } diff --git a/crates/ruff_python_formatter/src/statement/stmt_raise.rs b/crates/ruff_python_formatter/src/statement/stmt_raise.rs index 47372372a5..78c8591b5a 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_raise.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_raise.rs @@ -1,5 +1,9 @@ -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; +use crate::expression::parentheses::Parenthesize; +use crate::{FormatNodeRule, PyFormatter}; +use ruff_formatter::prelude::{space, text}; +use ruff_formatter::{write, Buffer, Format, FormatResult}; + +use crate::expression::maybe_parenthesize_expression; use rustpython_parser::ast::StmtRaise; #[derive(Default)] @@ -7,6 +11,35 @@ pub struct FormatStmtRaise; impl FormatNodeRule for FormatStmtRaise { fn fmt_fields(&self, item: &StmtRaise, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + let StmtRaise { + range: _, + exc, + cause, + } = item; + + text("raise").fmt(f)?; + + if let Some(value) = exc { + write!( + f, + [ + space(), + maybe_parenthesize_expression(value, item, Parenthesize::Optional) + ] + )?; + } + + if let Some(value) = cause { + write!( + f, + [ + space(), + text("from"), + space(), + maybe_parenthesize_expression(value, item, Parenthesize::Optional) + ] + )?; + } + Ok(()) } } diff --git a/crates/ruff_python_formatter/src/statement/stmt_return.rs b/crates/ruff_python_formatter/src/statement/stmt_return.rs index db9b28b0d4..c298379c47 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_return.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_return.rs @@ -1,5 +1,6 @@ +use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::Parenthesize; -use crate::{AsFormat, FormatNodeRule, PyFormatter}; +use crate::{FormatNodeRule, PyFormatter}; use ruff_formatter::prelude::{space, text}; use ruff_formatter::{write, Buffer, Format, FormatResult}; use rustpython_parser::ast::StmtReturn; @@ -16,7 +17,7 @@ impl FormatNodeRule for FormatStmtReturn { [ text("return"), space(), - value.format().with_options(Parenthesize::IfBreaks) + maybe_parenthesize_expression(value, item, Parenthesize::IfBreaks) ] ) } else { diff --git a/crates/ruff_python_formatter/src/statement/stmt_try.rs b/crates/ruff_python_formatter/src/statement/stmt_try.rs index 35afc06669..49bcef4387 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_try.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_try.rs @@ -1,12 +1,199 @@ -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; +use crate::comments; +use crate::comments::leading_alternate_branch_comments; +use crate::comments::SourceComment; +use crate::other::except_handler_except_handler::ExceptHandlerKind; +use crate::prelude::*; +use crate::statement::FormatRefWithRule; +use crate::statement::Stmt; +use crate::{FormatNodeRule, PyFormatter}; +use ruff_formatter::FormatRuleWithOptions; use ruff_formatter::{write, Buffer, FormatResult}; -use rustpython_parser::ast::StmtTry; +use ruff_python_ast::node::AnyNodeRef; +use ruff_text_size::TextRange; +use rustpython_parser::ast::{ExceptHandler, Ranged, StmtTry, StmtTryStar, Suite}; + +pub(super) enum AnyStatementTry<'a> { + Try(&'a StmtTry), + TryStar(&'a StmtTryStar), +} +impl<'a> AnyStatementTry<'a> { + const fn except_handler_kind(&self) -> ExceptHandlerKind { + match self { + AnyStatementTry::Try(_) => ExceptHandlerKind::Regular, + AnyStatementTry::TryStar(_) => ExceptHandlerKind::Starred, + } + } + + fn body(&self) -> &Suite { + match self { + AnyStatementTry::Try(try_) => &try_.body, + AnyStatementTry::TryStar(try_) => &try_.body, + } + } + + fn handlers(&self) -> &[ExceptHandler] { + match self { + AnyStatementTry::Try(try_) => try_.handlers.as_slice(), + AnyStatementTry::TryStar(try_) => try_.handlers.as_slice(), + } + } + fn orelse(&self) -> &Suite { + match self { + AnyStatementTry::Try(try_) => &try_.orelse, + AnyStatementTry::TryStar(try_) => &try_.orelse, + } + } + + fn finalbody(&self) -> &Suite { + match self { + AnyStatementTry::Try(try_) => &try_.finalbody, + AnyStatementTry::TryStar(try_) => &try_.finalbody, + } + } +} + +impl Ranged for AnyStatementTry<'_> { + fn range(&self) -> TextRange { + match self { + AnyStatementTry::Try(with) => with.range(), + AnyStatementTry::TryStar(with) => with.range(), + } + } +} + +impl<'a> From<&'a StmtTry> for AnyStatementTry<'a> { + fn from(value: &'a StmtTry) -> Self { + AnyStatementTry::Try(value) + } +} + +impl<'a> From<&'a StmtTryStar> for AnyStatementTry<'a> { + fn from(value: &'a StmtTryStar) -> Self { + AnyStatementTry::TryStar(value) + } +} + +impl<'a> From<&AnyStatementTry<'a>> for AnyNodeRef<'a> { + fn from(value: &AnyStatementTry<'a>) -> Self { + match value { + AnyStatementTry::Try(with) => AnyNodeRef::StmtTry(with), + AnyStatementTry::TryStar(with) => AnyNodeRef::StmtTryStar(with), + } + } +} #[derive(Default)] pub struct FormatStmtTry; -impl FormatNodeRule for FormatStmtTry { - fn fmt_fields(&self, item: &StmtTry, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) +#[derive(Copy, Clone, Default)] +pub struct FormatExceptHandler { + except_handler_kind: ExceptHandlerKind, +} + +impl FormatRuleWithOptions> for FormatExceptHandler { + type Options = ExceptHandlerKind; + + fn with_options(mut self, options: Self::Options) -> Self { + self.except_handler_kind = options; + self } } + +impl FormatRule> for FormatExceptHandler { + fn fmt( + &self, + item: &ExceptHandler, + f: &mut Formatter>, + ) -> FormatResult<()> { + match item { + ExceptHandler::ExceptHandler(x) => { + x.format().with_options(self.except_handler_kind).fmt(f) + } + } + } +} + +impl<'ast> AsFormat> for ExceptHandler { + type Format<'a> = FormatRefWithRule< + 'a, + ExceptHandler, + FormatExceptHandler, + PyFormatContext<'ast>, + > where Self: 'a; + + fn format(&self) -> Self::Format<'_> { + FormatRefWithRule::new(self, FormatExceptHandler::default()) + } +} +impl Format> for AnyStatementTry<'_> { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + let comments_info = f.context().comments().clone(); + let mut dangling_comments = comments_info.dangling_comments(self); + let body = self.body(); + let handlers = self.handlers(); + let orelse = self.orelse(); + let finalbody = self.finalbody(); + + write!(f, [text("try:"), block_indent(&body.format())])?; + + let mut previous_node = body.last(); + + for handler in handlers { + let handler_comments = comments_info.leading_comments(handler); + write!( + f, + [ + leading_alternate_branch_comments(handler_comments, previous_node), + &handler.format().with_options(self.except_handler_kind()), + ] + )?; + previous_node = match handler { + ExceptHandler::ExceptHandler(handler) => handler.body.last(), + }; + } + + (previous_node, dangling_comments) = + format_case("else", orelse, previous_node, dangling_comments, f)?; + + format_case("finally", finalbody, previous_node, dangling_comments, f)?; + + write!(f, [comments::dangling_comments(dangling_comments)]) + } +} + +impl FormatNodeRule for FormatStmtTry { + fn fmt_fields(&self, item: &StmtTry, f: &mut PyFormatter) -> FormatResult<()> { + AnyStatementTry::from(item).fmt(f) + } + + fn fmt_dangling_comments(&self, _node: &StmtTry, _f: &mut PyFormatter) -> FormatResult<()> { + // dangling comments are formatted as part of AnyStatementTry::fmt + Ok(()) + } +} + +fn format_case<'a>( + name: &'static str, + body: &Suite, + previous_node: Option<&Stmt>, + dangling_comments: &'a [SourceComment], + f: &mut PyFormatter, +) -> FormatResult<(Option<&'a Stmt>, &'a [SourceComment])> { + Ok(if let Some(last) = body.last() { + let case_comments_start = + dangling_comments.partition_point(|comment| comment.slice().end() <= last.end()); + let (case_comments, rest) = dangling_comments.split_at(case_comments_start); + write!( + f, + [leading_alternate_branch_comments( + case_comments, + previous_node + )] + )?; + + write!(f, [text(name), text(":"), block_indent(&body.format())])?; + (None, rest) + } else { + (None, dangling_comments) + }) +} diff --git a/crates/ruff_python_formatter/src/statement/stmt_try_star.rs b/crates/ruff_python_formatter/src/statement/stmt_try_star.rs index aa4d45c444..3c61258248 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_try_star.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_try_star.rs @@ -1,5 +1,7 @@ -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; +use crate::statement::stmt_try::AnyStatementTry; +use crate::{FormatNodeRule, PyFormatter}; +use ruff_formatter::Format; +use ruff_formatter::FormatResult; use rustpython_parser::ast::StmtTryStar; #[derive(Default)] @@ -7,6 +9,11 @@ pub struct FormatStmtTryStar; impl FormatNodeRule for FormatStmtTryStar { fn fmt_fields(&self, item: &StmtTryStar, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + AnyStatementTry::from(item).fmt(f) + } + + fn fmt_dangling_comments(&self, _node: &StmtTryStar, _f: &mut PyFormatter) -> FormatResult<()> { + // dangling comments are formatted as part of AnyStatementTry::fmt + Ok(()) } } diff --git a/crates/ruff_python_formatter/src/statement/stmt_while.rs b/crates/ruff_python_formatter/src/statement/stmt_while.rs index 1ed71397fd..d89e02fb85 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_while.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_while.rs @@ -1,4 +1,5 @@ use crate::comments::{leading_alternate_branch_comments, trailing_comments}; +use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::Parenthesize; use crate::prelude::*; use crate::FormatNodeRule; @@ -33,7 +34,7 @@ impl FormatNodeRule for FormatStmtWhile { [ text("while"), space(), - test.format().with_options(Parenthesize::IfBreaks), + maybe_parenthesize_expression(test, item, Parenthesize::IfBreaks), text(":"), trailing_comments(trailing_condition_comments), block_indent(&body.format()) @@ -44,7 +45,7 @@ impl FormatNodeRule for FormatStmtWhile { // Split between leading comments before the `else` keyword and end of line comments at the end of // the `else:` line. let trailing_start = - or_else_comments.partition_point(|comment| comment.position().is_own_line()); + or_else_comments.partition_point(|comment| comment.line_position().is_own_line()); let (leading, trailing) = or_else_comments.split_at(trailing_start); write!( diff --git a/crates/ruff_python_formatter/src/statement/stmt_with.rs b/crates/ruff_python_formatter/src/statement/stmt_with.rs index a68ee33113..8474d70240 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_with.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_with.rs @@ -1,12 +1,107 @@ -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; use ruff_formatter::{write, Buffer, FormatResult}; -use rustpython_parser::ast::StmtWith; +use ruff_python_ast::node::AnyNodeRef; +use ruff_text_size::TextRange; +use rustpython_parser::ast::{Ranged, StmtAsyncWith, StmtWith, Suite, WithItem}; + +use crate::builders::parenthesize_if_expands; +use crate::comments::trailing_comments; +use crate::prelude::*; +use crate::FormatNodeRule; + +pub(super) enum AnyStatementWith<'a> { + With(&'a StmtWith), + AsyncWith(&'a StmtAsyncWith), +} + +impl<'a> AnyStatementWith<'a> { + const fn is_async(&self) -> bool { + matches!(self, AnyStatementWith::AsyncWith(_)) + } + + fn items(&self) -> &[WithItem] { + match self { + AnyStatementWith::With(with) => with.items.as_slice(), + AnyStatementWith::AsyncWith(with) => with.items.as_slice(), + } + } + + fn body(&self) -> &Suite { + match self { + AnyStatementWith::With(with) => &with.body, + AnyStatementWith::AsyncWith(with) => &with.body, + } + } +} + +impl Ranged for AnyStatementWith<'_> { + fn range(&self) -> TextRange { + match self { + AnyStatementWith::With(with) => with.range(), + AnyStatementWith::AsyncWith(with) => with.range(), + } + } +} + +impl<'a> From<&'a StmtWith> for AnyStatementWith<'a> { + fn from(value: &'a StmtWith) -> Self { + AnyStatementWith::With(value) + } +} + +impl<'a> From<&'a StmtAsyncWith> for AnyStatementWith<'a> { + fn from(value: &'a StmtAsyncWith) -> Self { + AnyStatementWith::AsyncWith(value) + } +} + +impl<'a> From<&AnyStatementWith<'a>> for AnyNodeRef<'a> { + fn from(value: &AnyStatementWith<'a>) -> Self { + match value { + AnyStatementWith::With(with) => AnyNodeRef::StmtWith(with), + AnyStatementWith::AsyncWith(with) => AnyNodeRef::StmtAsyncWith(with), + } + } +} + +impl Format> for AnyStatementWith<'_> { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + let comments = f.context().comments().clone(); + let dangling_comments = comments.dangling_comments(self); + + let joined_items = format_with(|f| { + f.join_comma_separated(self.body().first().unwrap().start()) + .nodes(self.items().iter()) + .finish() + }); + + if self.is_async() { + write!(f, [text("async"), space()])?; + } + + write!( + f, + [ + text("with"), + space(), + group(&parenthesize_if_expands(&joined_items)), + text(":"), + trailing_comments(dangling_comments), + block_indent(&self.body().format()) + ] + ) + } +} #[derive(Default)] pub struct FormatStmtWith; impl FormatNodeRule for FormatStmtWith { fn fmt_fields(&self, item: &StmtWith, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + AnyStatementWith::from(item).fmt(f) + } + + fn fmt_dangling_comments(&self, _node: &StmtWith, _f: &mut PyFormatter) -> FormatResult<()> { + // Handled in `fmt_fields` + Ok(()) } } diff --git a/crates/ruff_python_formatter/src/statement/suite.rs b/crates/ruff_python_formatter/src/statement/suite.rs index 23b98f1798..92fc32ed4e 100644 --- a/crates/ruff_python_formatter/src/statement/suite.rs +++ b/crates/ruff_python_formatter/src/statement/suite.rs @@ -43,7 +43,7 @@ impl FormatRule> for FormatSuite { }; let comments = f.context().comments().clone(); - let source = f.context().contents(); + let source = f.context().source(); let saved_level = f.context().node_level(); f.context_mut().set_node_level(node_level); @@ -52,7 +52,7 @@ impl FormatRule> for FormatSuite { let mut iter = statements.iter(); let Some(first) = iter.next() else { - return Ok(()) + return Ok(()); }; // First entry has never any separator, doesn't matter which one we take; @@ -96,13 +96,12 @@ impl FormatRule> for FormatSuite { // the leading comment. This is why the suite handling counts the lines before the // start of the next statement or before the first leading comments for compound statements. let separator = format_with(|f| { - let start = if let Some(first_leading) = - comments.leading_comments(statement.into()).first() - { - first_leading.slice().start() - } else { - statement.start() - }; + let start = + if let Some(first_leading) = comments.leading_comments(statement).first() { + first_leading.slice().start() + } else { + statement.start() + }; match lines_before(start, source) { 0 | 1 => hard_line_break().fmt(f), @@ -178,6 +177,7 @@ impl<'ast> AsFormat> for Suite { impl<'ast> IntoFormat> for Suite { type Format = FormatOwnedWithRule>; + fn into_format(self) -> Self::Format { FormatOwnedWithRule::new(self, FormatSuite::default()) } @@ -188,7 +188,8 @@ mod tests { use crate::comments::Comments; use crate::prelude::*; use crate::statement::suite::SuiteLevel; - use ruff_formatter::{format, IndentStyle, SimpleFormatOptions}; + use crate::PyFormatOptions; + use ruff_formatter::format; use rustpython_parser::ast::Suite; use rustpython_parser::Parse; @@ -216,14 +217,7 @@ def trailing_func(): let statements = Suite::parse(source, "test.py").unwrap(); - let context = PyFormatContext::new( - SimpleFormatOptions { - indent_style: IndentStyle::Space(4), - ..SimpleFormatOptions::default() - }, - source, - Comments::default(), - ); + let context = PyFormatContext::new(PyFormatOptions::default(), source, Comments::default()); let test_formatter = format_with(|f: &mut PyFormatter| statements.format().with_options(level).fmt(f)); @@ -252,7 +246,8 @@ one_leading_newline = 10 no_leading_newline = 30 -NOT_YET_IMPLEMENTED_StmtClassDef +class InTheMiddle: + pass trailing_statement = 1 @@ -283,7 +278,8 @@ two_leading_newlines = 20 one_leading_newline = 10 no_leading_newline = 30 -NOT_YET_IMPLEMENTED_StmtClassDef +class InTheMiddle: + pass trailing_statement = 1 diff --git a/crates/ruff_python_formatter/src/trivia.rs b/crates/ruff_python_formatter/src/trivia.rs index c1f5bcfdf6..b6432e5336 100644 --- a/crates/ruff_python_formatter/src/trivia.rs +++ b/crates/ruff_python_formatter/src/trivia.rs @@ -1,8 +1,7 @@ -use std::str::Chars; - use ruff_text_size::{TextLen, TextRange, TextSize}; +use unic_ucd_ident::{is_xid_continue, is_xid_start}; -use ruff_python_whitespace::is_python_whitespace; +use ruff_python_whitespace::{is_python_whitespace, Cursor}; /// Searches for the first non-trivia character in `range`. /// @@ -92,6 +91,24 @@ pub(crate) fn skip_trailing_trivia(offset: TextSize, code: &str) -> TextSize { offset } +fn is_identifier_start(c: char) -> bool { + c.is_ascii_alphabetic() || c == '_' || is_non_ascii_identifier_start(c) +} + +// Checks if the character c is a valid continuation character as described +// in https://docs.python.org/3/reference/lexical_analysis.html#identifiers +fn is_identifier_continuation(c: char) -> bool { + if c.is_ascii() { + matches!(c, 'a'..='z' | 'A'..='Z' | '_' | '0'..='9') + } else { + is_xid_continue(c) + } +} + +fn is_non_ascii_identifier_start(c: char) -> bool { + is_xid_start(c) +} + #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub(crate) struct Token { pub(crate) kind: TokenKind, @@ -112,9 +129,8 @@ impl Token { self.range.start() } - #[allow(unused)] pub(crate) const fn end(&self) -> TextSize { - self.range.start() + self.range.end() } } @@ -162,7 +178,25 @@ pub(crate) enum TokenKind { /// '/' Slash, - /// Any other non trivia token. Always has a length of 1 + /// '*' + Star, + + /// `.`. + Dot, + + /// `else` + Else, + + /// `if` + If, + + /// `in` + In, + + /// `match` + Match, + + /// Any other non trivia token. Other, /// Returned for each character after [`TokenKind::Other`] has been returned once. @@ -181,6 +215,8 @@ impl TokenKind { ',' => TokenKind::Comma, ':' => TokenKind::Colon, '/' => TokenKind::Slash, + '*' => TokenKind::Star, + '.' => TokenKind::Dot, _ => TokenKind::Other, } } @@ -208,6 +244,7 @@ pub(crate) struct SimpleTokenizer<'a> { /// `true` when it is known that the current `back` line has no comment for sure. back_line_has_no_comment: bool, bogus: bool, + source: &'a str, cursor: Cursor<'a>, } @@ -218,6 +255,7 @@ impl<'a> SimpleTokenizer<'a> { back_offset: range.end(), back_line_has_no_comment: false, bogus: false, + source, cursor: Cursor::new(&source[range]), } } @@ -231,6 +269,18 @@ impl<'a> SimpleTokenizer<'a> { Self::new(source, TextRange::up_to(offset)) } + fn to_keyword_or_other(&self, range: TextRange) -> TokenKind { + let source = &self.source[range]; + match source { + "if" => TokenKind::If, + "else" => TokenKind::Else, + "in" => TokenKind::In, + "match" => TokenKind::Match, // Match is a soft keyword that depends on the context but we can always lex it as a keyword and leave it to the caller (parser) to decide if it should be handled as an identifier or keyword. + // ..., + _ => TokenKind::Other, // Potentially an identifier, but only if it isn't a string prefix. We can ignore this for now https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals + } + } + fn next_token(&mut self) -> Token { self.cursor.start_token(); @@ -238,7 +288,7 @@ impl<'a> SimpleTokenizer<'a> { return Token { kind: TokenKind::EndOfFile, range: TextRange::empty(self.offset), - } + }; }; if self.bogus { @@ -272,12 +322,19 @@ impl<'a> SimpleTokenizer<'a> { '\\' => TokenKind::Continuation, c => { - let kind = TokenKind::from_non_trivia_char(c); + let kind = if is_identifier_start(c) { + self.cursor.eat_while(is_identifier_continuation); + let token_len = self.cursor.token_len(); + + let range = TextRange::at(self.offset, token_len); + self.to_keyword_or_other(range) + } else { + TokenKind::from_non_trivia_char(c) + }; if kind == TokenKind::Other { self.bogus = true; } - kind } }; @@ -303,7 +360,7 @@ impl<'a> SimpleTokenizer<'a> { return Token { kind: TokenKind::EndOfFile, range: TextRange::empty(self.back_offset), - } + }; }; if self.bogus { @@ -344,9 +401,7 @@ impl<'a> SimpleTokenizer<'a> { // Skip the test whether there's a preceding comment if it has been performed before. if !self.back_line_has_no_comment { - let rest = self.cursor.chars.as_str(); - - for (back_index, c) in rest.chars().rev().enumerate() { + for (back_index, c) in self.cursor.chars().rev().enumerate() { match c { '#' => { // Potentially a comment @@ -379,7 +434,29 @@ impl<'a> SimpleTokenizer<'a> { } else if c == '\\' { TokenKind::Continuation } else { - let kind = TokenKind::from_non_trivia_char(c); + let kind = if is_identifier_continuation(c) { + // if we only have identifier continuations but no start (e.g. 555) we + // don't want to consume the chars, so in that case, we want to rewind the + // cursor to here + let savepoint = self.cursor.clone(); + self.cursor.eat_back_while(is_identifier_continuation); + + let token_len = self.cursor.token_len(); + let range = TextRange::at(self.back_offset - token_len, token_len); + + if self.source[range] + .chars() + .next() + .is_some_and(is_identifier_start) + { + self.to_keyword_or_other(range) + } else { + self.cursor = savepoint; + TokenKind::Other + } + } else { + TokenKind::from_non_trivia_char(c) + }; if kind == TokenKind::Other { self.bogus = true; @@ -435,99 +512,6 @@ impl DoubleEndedIterator for SimpleTokenizer<'_> { } } -const EOF_CHAR: char = '\0'; - -#[derive(Debug, Clone)] -struct Cursor<'a> { - chars: Chars<'a>, - source_length: TextSize, -} - -impl<'a> Cursor<'a> { - fn new(source: &'a str) -> Self { - Self { - source_length: source.text_len(), - chars: source.chars(), - } - } - - /// Peeks the next character from the input stream without consuming it. - /// Returns [`EOF_CHAR`] if the file is at the end of the file. - fn first(&self) -> char { - self.chars.clone().next().unwrap_or(EOF_CHAR) - } - - /// Peeks the next character from the input stream without consuming it. - /// Returns [`EOF_CHAR`] if the file is at the end of the file. - fn last(&self) -> char { - self.chars.clone().next_back().unwrap_or(EOF_CHAR) - } - - // SAFETY: THe `source.text_len` call in `new` would panic if the string length is larger than a `u32`. - #[allow(clippy::cast_possible_truncation)] - fn text_len(&self) -> TextSize { - TextSize::new(self.chars.as_str().len() as u32) - } - - fn token_len(&self) -> TextSize { - self.source_length - self.text_len() - } - - fn start_token(&mut self) { - self.source_length = self.text_len(); - } - - fn is_eof(&self) -> bool { - self.chars.as_str().is_empty() - } - - /// Consumes the next character - fn bump(&mut self) -> Option { - self.chars.next() - } - - /// Consumes the next character from the back - fn bump_back(&mut self) -> Option { - self.chars.next_back() - } - - fn eat_char(&mut self, c: char) -> bool { - if self.first() == c { - self.bump(); - true - } else { - false - } - } - - fn eat_char_back(&mut self, c: char) -> bool { - if self.last() == c { - self.bump_back(); - true - } else { - false - } - } - - /// Eats symbols while predicate returns true or until the end of file is reached. - fn eat_while(&mut self, mut predicate: impl FnMut(char) -> bool) { - // It was tried making optimized version of this for eg. line comments, but - // LLVM can inline all of this and compile it down to fast iteration over bytes. - while predicate(self.first()) && !self.is_eof() { - self.bump(); - } - } - - /// Eats symbols from the back while predicate returns true or until the beginning of file is reached. - fn eat_back_while(&mut self, mut predicate: impl FnMut(char) -> bool) { - // It was tried making optimized version of this for eg. line comments, but - // LLVM can inline all of this and compile it down to fast iteration over bytes. - while predicate(self.last()) && !self.is_eof() { - self.bump_back(); - } - } -} - #[cfg(test)] mod tests { use insta::assert_debug_snapshot; @@ -616,6 +600,44 @@ mod tests { test_case.assert_reverse_tokenization(); } + #[test] + fn tricky_unicode() { + let source = "មុ"; + + let test_case = tokenize(source); + assert_debug_snapshot!(test_case.tokens()); + test_case.assert_reverse_tokenization(); + } + + #[test] + fn identifier_ending_in_non_start_char() { + let source = "i5"; + + let test_case = tokenize(source); + assert_debug_snapshot!(test_case.tokens()); + test_case.assert_reverse_tokenization(); + } + + #[test] + fn ignore_word_with_only_id_continuing_chars() { + let source = "555"; + + let test_case = tokenize(source); + assert_debug_snapshot!(test_case.tokens()); + + // note: not reversible: [other, bogus, bogus] vs [bogus, bogus, other] + } + + #[test] + fn tokenize_multichar() { + let source = "if in else match"; + + let test_case = tokenize(source); + + assert_debug_snapshot!(test_case.tokens()); + test_case.assert_reverse_tokenization(); + } + #[test] fn tokenize_substring() { let source = "('some string') # comment"; diff --git a/crates/ruff_python_formatter/tests/fixtures.rs b/crates/ruff_python_formatter/tests/fixtures.rs new file mode 100644 index 0000000000..5d9be66a41 --- /dev/null +++ b/crates/ruff_python_formatter/tests/fixtures.rs @@ -0,0 +1,261 @@ +use ruff_formatter::FormatOptions; +use ruff_python_formatter::{format_module, PyFormatOptions}; +use similar::TextDiff; +use std::fmt::{Formatter, Write}; +use std::io::BufReader; +use std::path::Path; +use std::{fmt, fs}; + +#[test] +fn black_compatibility() { + let test_file = |input_path: &Path| { + let content = fs::read_to_string(input_path).unwrap(); + + let options_path = input_path.with_extension("options.json"); + + let options: PyFormatOptions = if let Ok(options_file) = fs::File::open(options_path) { + let reader = BufReader::new(options_file); + serde_json::from_reader(reader).expect("Options to be a valid Json file") + } else { + PyFormatOptions::default() + }; + + let printed = format_module(&content, options.clone()).unwrap_or_else(|err| { + panic!( + "Formatting of {} to succeed but encountered error {err}", + input_path.display() + ) + }); + + let expected_path = input_path.with_extension("py.expect"); + let expected_output = fs::read_to_string(&expected_path) + .unwrap_or_else(|_| panic!("Expected Black output file '{expected_path:?}' to exist")); + + let formatted_code = printed.as_code(); + + ensure_stability_when_formatting_twice(formatted_code, options, input_path); + + if formatted_code == expected_output { + // Black and Ruff formatting matches. Delete any existing snapshot files because the Black output + // already perfectly captures the expected output. + // The following code mimics insta's logic generating the snapshot name for a test. + let workspace_path = std::env::var("CARGO_MANIFEST_DIR").unwrap(); + + let mut components = input_path.components().rev(); + let file_name = components.next().unwrap(); + let test_suite = components.next().unwrap(); + + let snapshot_name = format!( + "black_compatibility@{}__{}.snap", + test_suite.as_os_str().to_string_lossy(), + file_name.as_os_str().to_string_lossy() + ); + + let snapshot_path = Path::new(&workspace_path) + .join("tests/snapshots") + .join(snapshot_name); + if snapshot_path.exists() && snapshot_path.is_file() { + // SAFETY: This is a convenience feature. That's why we don't want to abort + // when deleting a no longer needed snapshot fails. + fs::remove_file(&snapshot_path).ok(); + } + + let new_snapshot_path = snapshot_path.with_extension("snap.new"); + if new_snapshot_path.exists() && new_snapshot_path.is_file() { + // SAFETY: This is a convenience feature. That's why we don't want to abort + // when deleting a no longer needed snapshot fails. + fs::remove_file(&new_snapshot_path).ok(); + } + } else { + // Black and Ruff have different formatting. Write out a snapshot that covers the differences + // today. + let mut snapshot = String::new(); + write!(snapshot, "{}", Header::new("Input")).unwrap(); + write!(snapshot, "{}", CodeFrame::new("py", &content)).unwrap(); + + write!(snapshot, "{}", Header::new("Black Differences")).unwrap(); + + let diff = TextDiff::from_lines(expected_output.as_str(), formatted_code) + .unified_diff() + .header("Black", "Ruff") + .to_string(); + + write!(snapshot, "{}", CodeFrame::new("diff", &diff)).unwrap(); + + write!(snapshot, "{}", Header::new("Ruff Output")).unwrap(); + write!(snapshot, "{}", CodeFrame::new("py", &formatted_code)).unwrap(); + + write!(snapshot, "{}", Header::new("Black Output")).unwrap(); + write!(snapshot, "{}", CodeFrame::new("py", &expected_output)).unwrap(); + + insta::with_settings!({ + omit_expression => true, + input_file => input_path, + prepend_module_to_snapshot => false, + }, { + insta::assert_snapshot!(snapshot); + }); + } + }; + + insta::glob!("../resources", "test/fixtures/black/**/*.py", test_file); +} + +#[test] +fn format() { + let test_file = |input_path: &Path| { + let content = fs::read_to_string(input_path).unwrap(); + + let options = PyFormatOptions::default(); + let printed = format_module(&content, options.clone()).expect("Formatting to succeed"); + let formatted_code = printed.as_code(); + + ensure_stability_when_formatting_twice(formatted_code, options, input_path); + + let mut snapshot = format!("## Input\n{}", CodeFrame::new("py", &content)); + + let options_path = input_path.with_extension("options.json"); + if let Ok(options_file) = fs::File::open(options_path) { + let reader = BufReader::new(options_file); + let options: Vec = + serde_json::from_reader(reader).expect("Options to be a valid Json file"); + + writeln!(snapshot, "## Outputs").unwrap(); + + for (i, options) in options.into_iter().enumerate() { + let printed = + format_module(&content, options.clone()).expect("Formatting to succeed"); + let formatted_code = printed.as_code(); + + ensure_stability_when_formatting_twice(formatted_code, options.clone(), input_path); + + writeln!( + snapshot, + "### Output {}\n{}{}", + i + 1, + CodeFrame::new("", &DisplayPyOptions(&options)), + CodeFrame::new("py", &formatted_code) + ) + .unwrap(); + } + } else { + let options = PyFormatOptions::default(); + let printed = format_module(&content, options.clone()).expect("Formatting to succeed"); + let formatted_code = printed.as_code(); + + ensure_stability_when_formatting_twice(formatted_code, options, input_path); + + writeln!( + snapshot, + "## Output\n{}", + CodeFrame::new("py", &formatted_code) + ) + .unwrap(); + } + + insta::with_settings!({ + omit_expression => true, + input_file => input_path, + prepend_module_to_snapshot => false, + }, { + insta::assert_snapshot!(snapshot); + }); + }; + + insta::glob!("../resources", "test/fixtures/ruff/**/*.py", test_file); +} + +/// Format another time and make sure that there are no changes anymore +fn ensure_stability_when_formatting_twice( + formatted_code: &str, + options: PyFormatOptions, + input_path: &Path, +) { + let reformatted = match format_module(formatted_code, options) { + Ok(reformatted) => reformatted, + Err(err) => { + panic!( + "Expected formatted code of {} to be valid syntax: {err}:\ + \n---\n{formatted_code}---\n", + input_path.display() + ); + } + }; + + if reformatted.as_code() != formatted_code { + let diff = TextDiff::from_lines(formatted_code, reformatted.as_code()) + .unified_diff() + .header("Formatted once", "Formatted twice") + .to_string(); + panic!( + r#"Reformatting the formatted code of {} a second time resulted in formatting changes. +--- +{diff}--- + +Formatted once: +--- +{formatted_code}--- + +Formatted twice: +--- +{}---"#, + input_path.display(), + reformatted.as_code(), + ); + } +} + +struct Header<'a> { + title: &'a str, +} + +impl<'a> Header<'a> { + fn new(title: &'a str) -> Self { + Self { title } + } +} + +impl std::fmt::Display for Header<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + writeln!(f, "## {}", self.title)?; + writeln!(f) + } +} + +struct CodeFrame<'a> { + language: &'a str, + code: &'a dyn std::fmt::Display, +} + +impl<'a> CodeFrame<'a> { + fn new(language: &'a str, code: &'a dyn std::fmt::Display) -> Self { + Self { language, code } + } +} + +impl std::fmt::Display for CodeFrame<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + writeln!(f, "```{}", self.language)?; + write!(f, "{}", self.code)?; + writeln!(f, "```")?; + writeln!(f) + } +} + +struct DisplayPyOptions<'a>(&'a PyFormatOptions); + +impl fmt::Display for DisplayPyOptions<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + writeln!( + f, + r#"indent-style = {indent_style} +line-width = {line_width} +quote-style = {quote_style:?} +magic-trailing-comma = {magic_trailing_comma:?}"#, + indent_style = self.0.indent_style(), + line_width = self.0.line_width().value(), + quote_style = self.0.quote_style(), + magic_trailing_comma = self.0.magic_trailing_comma() + ) + } +} diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@conditional_expression.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@conditional_expression.py.snap new file mode 100644 index 0000000000..57ef1898fb --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@conditional_expression.py.snap @@ -0,0 +1,327 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/conditional_expression.py +--- +## Input + +```py +long_kwargs_single_line = my_function( + foo="test, this is a sample value", + bar=some_long_value_name_foo_bar_baz if some_boolean_variable else some_fallback_value_foo_bar_baz, + baz="hello, this is a another value", +) + +multiline_kwargs_indented = my_function( + foo="test, this is a sample value", + bar=some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz, + baz="hello, this is a another value", +) + +imploding_kwargs = my_function( + foo="test, this is a sample value", + bar=a + if foo + else b, + baz="hello, this is a another value", +) + +imploding_line = ( + 1 + if 1 + 1 == 2 + else 0 +) + +exploding_line = "hello this is a slightly long string" if some_long_value_name_foo_bar_baz else "this one is a little shorter" + +positional_argument_test(some_long_value_name_foo_bar_baz if some_boolean_variable else some_fallback_value_foo_bar_baz) + +def weird_default_argument(x=some_long_value_name_foo_bar_baz + if SOME_CONSTANT + else some_fallback_value_foo_bar_baz): + pass + +nested = "hello this is a slightly long string" if (some_long_value_name_foo_bar_baz if + nesting_test_expressions else some_fallback_value_foo_bar_baz) \ + else "this one is a little shorter" + +generator_expression = ( + some_long_value_name_foo_bar_baz if some_boolean_variable else some_fallback_value_foo_bar_baz for some_boolean_variable in some_iterable +) + + +def limit_offset_sql(self, low_mark, high_mark): + """Return LIMIT/OFFSET SQL clause.""" + limit, offset = self._get_limit_offset_params(low_mark, high_mark) + return " ".join( + sql + for sql in ( + "LIMIT %d" % limit if limit else None, + ("OFFSET %d" % offset) if offset else None, + ) + if sql + ) + + +def something(): + clone._iterable_class = ( + NamedValuesListIterable + if named + else FlatValuesListIterable + if flat + else ValuesListIterable + ) +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,20 +1,16 @@ + long_kwargs_single_line = my_function( + foo="test, this is a sample value", +- bar=( +- some_long_value_name_foo_bar_baz +- if some_boolean_variable +- else some_fallback_value_foo_bar_baz +- ), ++ bar=some_long_value_name_foo_bar_baz ++ if some_boolean_variable ++ else some_fallback_value_foo_bar_baz, + baz="hello, this is a another value", + ) + + multiline_kwargs_indented = my_function( + foo="test, this is a sample value", +- bar=( +- some_long_value_name_foo_bar_baz +- if some_boolean_variable +- else some_fallback_value_foo_bar_baz +- ), ++ bar=some_long_value_name_foo_bar_baz ++ if some_boolean_variable ++ else some_fallback_value_foo_bar_baz, + baz="hello, this is a another value", + ) + +@@ -40,11 +36,9 @@ + + + def weird_default_argument( +- x=( +- some_long_value_name_foo_bar_baz +- if SOME_CONSTANT +- else some_fallback_value_foo_bar_baz +- ), ++ x=some_long_value_name_foo_bar_baz ++ if SOME_CONSTANT ++ else some_fallback_value_foo_bar_baz, + ): + pass + +@@ -59,26 +53,14 @@ + else "this one is a little shorter" + ) + +-generator_expression = ( +- ( +- some_long_value_name_foo_bar_baz +- if some_boolean_variable +- else some_fallback_value_foo_bar_baz +- ) +- for some_boolean_variable in some_iterable +-) ++generator_expression = (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) + + + def limit_offset_sql(self, low_mark, high_mark): + """Return LIMIT/OFFSET SQL clause.""" + limit, offset = self._get_limit_offset_params(low_mark, high_mark) + return " ".join( +- sql +- for sql in ( +- "LIMIT %d" % limit if limit else None, +- ("OFFSET %d" % offset) if offset else None, +- ) +- if sql ++ (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) + ) + + +``` + +## Ruff Output + +```py +long_kwargs_single_line = my_function( + foo="test, this is a sample value", + bar=some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz, + baz="hello, this is a another value", +) + +multiline_kwargs_indented = my_function( + foo="test, this is a sample value", + bar=some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz, + baz="hello, this is a another value", +) + +imploding_kwargs = my_function( + foo="test, this is a sample value", + bar=a if foo else b, + baz="hello, this is a another value", +) + +imploding_line = 1 if 1 + 1 == 2 else 0 + +exploding_line = ( + "hello this is a slightly long string" + if some_long_value_name_foo_bar_baz + else "this one is a little shorter" +) + +positional_argument_test( + some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz +) + + +def weird_default_argument( + x=some_long_value_name_foo_bar_baz + if SOME_CONSTANT + else some_fallback_value_foo_bar_baz, +): + pass + + +nested = ( + "hello this is a slightly long string" + if ( + some_long_value_name_foo_bar_baz + if nesting_test_expressions + else some_fallback_value_foo_bar_baz + ) + else "this one is a little shorter" +) + +generator_expression = (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) + + +def limit_offset_sql(self, low_mark, high_mark): + """Return LIMIT/OFFSET SQL clause.""" + limit, offset = self._get_limit_offset_params(low_mark, high_mark) + return " ".join( + (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) + ) + + +def something(): + clone._iterable_class = ( + NamedValuesListIterable + if named + else FlatValuesListIterable if flat else ValuesListIterable + ) +``` + +## Black Output + +```py +long_kwargs_single_line = my_function( + foo="test, this is a sample value", + bar=( + some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz + ), + baz="hello, this is a another value", +) + +multiline_kwargs_indented = my_function( + foo="test, this is a sample value", + bar=( + some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz + ), + baz="hello, this is a another value", +) + +imploding_kwargs = my_function( + foo="test, this is a sample value", + bar=a if foo else b, + baz="hello, this is a another value", +) + +imploding_line = 1 if 1 + 1 == 2 else 0 + +exploding_line = ( + "hello this is a slightly long string" + if some_long_value_name_foo_bar_baz + else "this one is a little shorter" +) + +positional_argument_test( + some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz +) + + +def weird_default_argument( + x=( + some_long_value_name_foo_bar_baz + if SOME_CONSTANT + else some_fallback_value_foo_bar_baz + ), +): + pass + + +nested = ( + "hello this is a slightly long string" + if ( + some_long_value_name_foo_bar_baz + if nesting_test_expressions + else some_fallback_value_foo_bar_baz + ) + else "this one is a little shorter" +) + +generator_expression = ( + ( + some_long_value_name_foo_bar_baz + if some_boolean_variable + else some_fallback_value_foo_bar_baz + ) + for some_boolean_variable in some_iterable +) + + +def limit_offset_sql(self, low_mark, high_mark): + """Return LIMIT/OFFSET SQL clause.""" + limit, offset = self._get_limit_offset_params(low_mark, high_mark) + return " ".join( + sql + for sql in ( + "LIMIT %d" % limit if limit else None, + ("OFFSET %d" % offset) if offset else None, + ) + if sql + ) + + +def something(): + clone._iterable_class = ( + NamedValuesListIterable + if named + else FlatValuesListIterable if flat else ValuesListIterable + ) +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__blackd_diff.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__blackd_diff.py.snap new file mode 100644 index 0000000000..835d6e4d60 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__blackd_diff.py.snap @@ -0,0 +1,55 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/blackd_diff.py +--- +## Input + +```py +def abc (): + return ["hello", "world", + "!"] + +print( "Incorrect formatting" +) +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,6 +1,5 @@ +-def abc (): +- return ["hello", "world", +- "!"] ++def abc(): ++ return ["hello", "world", "!"] + +-print( "Incorrect formatting" +-) ++ ++print("Incorrect formatting") +``` + +## Ruff Output + +```py +def abc(): + return ["hello", "world", "!"] + + +print("Incorrect formatting") +``` + +## Black Output + +```py +def abc (): + return ["hello", "world", + "!"] + +print( "Incorrect formatting" +) +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__debug_visitor.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__debug_visitor.py.snap new file mode 100644 index 0000000000..3dba6a699c --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__debug_visitor.py.snap @@ -0,0 +1,165 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/debug_visitor.py +--- +## Input + +```py +@dataclass +class DebugVisitor(Visitor[T]): + tree_depth: int = 0 + + def visit_default(self, node: LN) -> Iterator[T]: + indent = ' ' * (2 * self.tree_depth) + if isinstance(node, Node): + _type = type_repr(node.type) + out(f'{indent}{_type}', fg='yellow') + self.tree_depth += 1 + for child in node.children: + yield from self.visit(child) + + self.tree_depth -= 1 + out(f'{indent}/{_type}', fg='yellow', bold=False) + else: + _type = token.tok_name.get(node.type, str(node.type)) + out(f'{indent}{_type}', fg='blue', nl=False) + if node.prefix: + # We don't have to handle prefixes for `Node` objects since + # that delegates to the first child anyway. + out(f' {node.prefix!r}', fg='green', bold=False, nl=False) + out(f' {node.value!r}', fg='blue', bold=False) + + @classmethod + def show(cls, code: str) -> None: + """Pretty-prints a given string of `code`. + + Convenience method for debugging. + """ + v: DebugVisitor[None] = DebugVisitor() + list(v.visit(lib2to3_parse(code))) +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -3,24 +3,29 @@ + tree_depth: int = 0 + + def visit_default(self, node: LN) -> Iterator[T]: +- indent = ' ' * (2 * self.tree_depth) ++ indent = " " * (2 * self.tree_depth) + if isinstance(node, Node): + _type = type_repr(node.type) +- out(f'{indent}{_type}', fg='yellow') ++ out(f"NOT_YET_IMPLEMENTED_ExprJoinedStr", fg="yellow") + self.tree_depth += 1 + for child in node.children: +- yield from self.visit(child) ++ NOT_YET_IMPLEMENTED_ExprYieldFrom + + self.tree_depth -= 1 +- out(f'{indent}/{_type}', fg='yellow', bold=False) ++ out(f"NOT_YET_IMPLEMENTED_ExprJoinedStr", fg="yellow", bold=False) + else: + _type = token.tok_name.get(node.type, str(node.type)) +- out(f'{indent}{_type}', fg='blue', nl=False) ++ out(f"NOT_YET_IMPLEMENTED_ExprJoinedStr", fg="blue", nl=False) + if node.prefix: + # We don't have to handle prefixes for `Node` objects since + # that delegates to the first child anyway. +- out(f' {node.prefix!r}', fg='green', bold=False, nl=False) +- out(f' {node.value!r}', fg='blue', bold=False) ++ out( ++ f"NOT_YET_IMPLEMENTED_ExprJoinedStr", ++ fg="green", ++ bold=False, ++ nl=False, ++ ) ++ out(f"NOT_YET_IMPLEMENTED_ExprJoinedStr", fg="blue", bold=False) + + @classmethod + def show(cls, code: str) -> None: +``` + +## Ruff Output + +```py +@dataclass +class DebugVisitor(Visitor[T]): + tree_depth: int = 0 + + def visit_default(self, node: LN) -> Iterator[T]: + indent = " " * (2 * self.tree_depth) + if isinstance(node, Node): + _type = type_repr(node.type) + out(f"NOT_YET_IMPLEMENTED_ExprJoinedStr", fg="yellow") + self.tree_depth += 1 + for child in node.children: + NOT_YET_IMPLEMENTED_ExprYieldFrom + + self.tree_depth -= 1 + out(f"NOT_YET_IMPLEMENTED_ExprJoinedStr", fg="yellow", bold=False) + else: + _type = token.tok_name.get(node.type, str(node.type)) + out(f"NOT_YET_IMPLEMENTED_ExprJoinedStr", fg="blue", nl=False) + if node.prefix: + # We don't have to handle prefixes for `Node` objects since + # that delegates to the first child anyway. + out( + f"NOT_YET_IMPLEMENTED_ExprJoinedStr", + fg="green", + bold=False, + nl=False, + ) + out(f"NOT_YET_IMPLEMENTED_ExprJoinedStr", fg="blue", bold=False) + + @classmethod + def show(cls, code: str) -> None: + """Pretty-prints a given string of `code`. + + Convenience method for debugging. + """ + v: DebugVisitor[None] = DebugVisitor() + list(v.visit(lib2to3_parse(code))) +``` + +## Black Output + +```py +@dataclass +class DebugVisitor(Visitor[T]): + tree_depth: int = 0 + + def visit_default(self, node: LN) -> Iterator[T]: + indent = ' ' * (2 * self.tree_depth) + if isinstance(node, Node): + _type = type_repr(node.type) + out(f'{indent}{_type}', fg='yellow') + self.tree_depth += 1 + for child in node.children: + yield from self.visit(child) + + self.tree_depth -= 1 + out(f'{indent}/{_type}', fg='yellow', bold=False) + else: + _type = token.tok_name.get(node.type, str(node.type)) + out(f'{indent}{_type}', fg='blue', nl=False) + if node.prefix: + # We don't have to handle prefixes for `Node` objects since + # that delegates to the first child anyway. + out(f' {node.prefix!r}', fg='green', bold=False, nl=False) + out(f' {node.value!r}', fg='blue', bold=False) + + @classmethod + def show(cls, code: str) -> None: + """Pretty-prints a given string of `code`. + + Convenience method for debugging. + """ + v: DebugVisitor[None] = DebugVisitor() + list(v.visit(lib2to3_parse(code))) +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__decorators.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__decorators.py.snap new file mode 100644 index 0000000000..7793a98149 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__decorators.py.snap @@ -0,0 +1,576 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/decorators.py +--- +## Input + +```py +# This file doesn't use the standard decomposition. +# Decorator syntax test cases are separated by double # comments. +# Those before the 'output' comment are valid under the old syntax. +# Those after the 'ouput' comment require PEP614 relaxed syntax. +# Do not remove the double # separator before the first test case, it allows +# the comment before the test case to be ignored. + +## + +@decorator +def f(): + ... + +## + +@decorator() +def f(): + ... + +## + +@decorator(arg) +def f(): + ... + +## + +@decorator(kwarg=0) +def f(): + ... + +## + +@decorator(*args) +def f(): + ... + +## + +@decorator(**kwargs) +def f(): + ... + +## + +@decorator(*args, **kwargs) +def f(): + ... + +## + +@decorator(*args, **kwargs,) +def f(): + ... + +## + +@dotted.decorator +def f(): + ... + +## + +@dotted.decorator(arg) +def f(): + ... + +## + +@dotted.decorator(kwarg=0) +def f(): + ... + +## + +@dotted.decorator(*args) +def f(): + ... + +## + +@dotted.decorator(**kwargs) +def f(): + ... + +## + +@dotted.decorator(*args, **kwargs) +def f(): + ... + +## + +@dotted.decorator(*args, **kwargs,) +def f(): + ... + +## + +@double.dotted.decorator +def f(): + ... + +## + +@double.dotted.decorator(arg) +def f(): + ... + +## + +@double.dotted.decorator(kwarg=0) +def f(): + ... + +## + +@double.dotted.decorator(*args) +def f(): + ... + +## + +@double.dotted.decorator(**kwargs) +def f(): + ... + +## + +@double.dotted.decorator(*args, **kwargs) +def f(): + ... + +## + +@double.dotted.decorator(*args, **kwargs,) +def f(): + ... + +## + +@_(sequence["decorator"]) +def f(): + ... + +## + +@eval("sequence['decorator']") +def f(): + ... +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,29 +1,182 @@ ++# This file doesn't use the standard decomposition. ++# Decorator syntax test cases are separated by double # comments. ++# Those before the 'output' comment are valid under the old syntax. ++# Those after the 'ouput' comment require PEP614 relaxed syntax. ++# Do not remove the double # separator before the first test case, it allows ++# the comment before the test case to be ignored. ++ ++## ++ ++@decorator ++def f(): ++ ... ++ ++ ++## ++ ++@decorator() ++def f(): ++ ... ++ ++ ++## ++ ++@decorator(arg) ++def f(): ++ ... ++ ++ ++## ++ ++@decorator(kwarg=0) ++def f(): ++ ... ++ ++ ++## ++ ++@decorator(*args) ++def f(): ++ ... ++ ++ + ## + +-@decorator()() ++@decorator(**kwargs) + def f(): + ... + ++ + ## + +-@(decorator) ++@decorator(*args, **kwargs) + def f(): + ... + ++ + ## + +-@sequence["decorator"] ++@decorator( ++ *args, ++ **kwargs, ++) + def f(): + ... + ++ + ## + +-@decorator[List[str]] ++@dotted.decorator + def f(): + ... + ++ + ## + +-@var := decorator ++@dotted.decorator(arg) ++def f(): ++ ... ++ ++ ++## ++ ++@dotted.decorator(kwarg=0) ++def f(): ++ ... ++ ++ ++## ++ ++@dotted.decorator(*args) ++def f(): ++ ... ++ ++ ++## ++ ++@dotted.decorator(**kwargs) ++def f(): ++ ... ++ ++ ++## ++ ++@dotted.decorator(*args, **kwargs) ++def f(): ++ ... ++ ++ ++## ++ ++@dotted.decorator( ++ *args, ++ **kwargs, ++) ++def f(): ++ ... ++ ++ ++## ++ ++@double.dotted.decorator ++def f(): ++ ... ++ ++ ++## ++ ++@double.dotted.decorator(arg) ++def f(): ++ ... ++ ++ ++## ++ ++@double.dotted.decorator(kwarg=0) ++def f(): ++ ... ++ ++ ++## ++ ++@double.dotted.decorator(*args) ++def f(): ++ ... ++ ++ ++## ++ ++@double.dotted.decorator(**kwargs) ++def f(): ++ ... ++ ++ ++## ++ ++@double.dotted.decorator(*args, **kwargs) ++def f(): ++ ... ++ ++ ++## ++ ++@double.dotted.decorator( ++ *args, ++ **kwargs, ++) ++def f(): ++ ... ++ ++ ++## ++ ++@_(sequence["decorator"]) ++def f(): ++ ... ++ ++ ++## ++ ++@eval("sequence['decorator']") + def f(): + ... +``` + +## Ruff Output + +```py +# This file doesn't use the standard decomposition. +# Decorator syntax test cases are separated by double # comments. +# Those before the 'output' comment are valid under the old syntax. +# Those after the 'ouput' comment require PEP614 relaxed syntax. +# Do not remove the double # separator before the first test case, it allows +# the comment before the test case to be ignored. + +## + +@decorator +def f(): + ... + + +## + +@decorator() +def f(): + ... + + +## + +@decorator(arg) +def f(): + ... + + +## + +@decorator(kwarg=0) +def f(): + ... + + +## + +@decorator(*args) +def f(): + ... + + +## + +@decorator(**kwargs) +def f(): + ... + + +## + +@decorator(*args, **kwargs) +def f(): + ... + + +## + +@decorator( + *args, + **kwargs, +) +def f(): + ... + + +## + +@dotted.decorator +def f(): + ... + + +## + +@dotted.decorator(arg) +def f(): + ... + + +## + +@dotted.decorator(kwarg=0) +def f(): + ... + + +## + +@dotted.decorator(*args) +def f(): + ... + + +## + +@dotted.decorator(**kwargs) +def f(): + ... + + +## + +@dotted.decorator(*args, **kwargs) +def f(): + ... + + +## + +@dotted.decorator( + *args, + **kwargs, +) +def f(): + ... + + +## + +@double.dotted.decorator +def f(): + ... + + +## + +@double.dotted.decorator(arg) +def f(): + ... + + +## + +@double.dotted.decorator(kwarg=0) +def f(): + ... + + +## + +@double.dotted.decorator(*args) +def f(): + ... + + +## + +@double.dotted.decorator(**kwargs) +def f(): + ... + + +## + +@double.dotted.decorator(*args, **kwargs) +def f(): + ... + + +## + +@double.dotted.decorator( + *args, + **kwargs, +) +def f(): + ... + + +## + +@_(sequence["decorator"]) +def f(): + ... + + +## + +@eval("sequence['decorator']") +def f(): + ... +``` + +## Black Output + +```py +## + +@decorator()() +def f(): + ... + +## + +@(decorator) +def f(): + ... + +## + +@sequence["decorator"] +def f(): + ... + +## + +@decorator[List[str]] +def f(): + ... + +## + +@var := decorator +def f(): + ... +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__docstring_no_string_normalization.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__docstring_no_string_normalization.py.snap new file mode 100644 index 0000000000..80c7ba60ec --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__docstring_no_string_normalization.py.snap @@ -0,0 +1,556 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/docstring_no_string_normalization.py +--- +## Input + +```py +class ALonelyClass: + ''' + A multiline class docstring. + ''' + def AnEquallyLonelyMethod(self): + ''' + A multiline method docstring''' + pass + + +def one_function(): + '''This is a docstring with a single line of text.''' + pass + + +def shockingly_the_quotes_are_normalized(): + '''This is a multiline docstring. + This is a multiline docstring. + This is a multiline docstring. + ''' + pass + + +def foo(): + """This is a docstring with + some lines of text here + """ + return + + +def baz(): + '''"This" is a string with some + embedded "quotes"''' + return + + +def poit(): + """ + Lorem ipsum dolor sit amet. + + Consectetur adipiscing elit: + - sed do eiusmod tempor incididunt ut labore + - dolore magna aliqua + - enim ad minim veniam + - quis nostrud exercitation ullamco laboris nisi + - aliquip ex ea commodo consequat + """ + pass + + +def under_indent(): + """ + These lines are indented in a way that does not +make sense. + """ + pass + + +def over_indent(): + """ + This has a shallow indent + - But some lines are deeper + - And the closing quote is too deep + """ + pass + + +def single_line(): + """But with a newline after it! + + """ + pass + + +def this(): + r""" + 'hey ho' + """ + + +def that(): + """ "hey yah" """ + + +def and_that(): + """ + "hey yah" """ + + +def and_this(): + ''' + "hey yah"''' + + +def believe_it_or_not_this_is_in_the_py_stdlib(): ''' +"hey yah"''' + + +def shockingly_the_quotes_are_normalized_v2(): + ''' + Docstring Docstring Docstring + ''' + pass + + +def backslash_space(): + '\ ' + + +def multiline_backslash_1(): + ''' + hey\there\ + \ ''' + + +def multiline_backslash_2(): + ''' + hey there \ ''' + + +def multiline_backslash_3(): + ''' + already escaped \\ ''' +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,73 +1,75 @@ + class ALonelyClass: +- ''' ++ """ + A multiline class docstring. +- ''' ++ """ + + def AnEquallyLonelyMethod(self): +- ''' +- A multiline method docstring''' ++ """ ++ A multiline method docstring""" + pass + + + def one_function(): +- '''This is a docstring with a single line of text.''' ++ """This is a docstring with a single line of text.""" + pass + + + def shockingly_the_quotes_are_normalized(): +- '''This is a multiline docstring. ++ """This is a multiline docstring. + This is a multiline docstring. + This is a multiline docstring. +- ''' ++ """ + pass + + + def foo(): +- """This is a docstring with +- some lines of text here +- """ ++ """This is a docstring with ++ some lines of text here ++ """ + return + + + def baz(): + '''"This" is a string with some +- embedded "quotes"''' ++ embedded "quotes"''' + return + + + def poit(): + """ +- Lorem ipsum dolor sit amet. ++ Lorem ipsum dolor sit amet. + +- Consectetur adipiscing elit: +- - sed do eiusmod tempor incididunt ut labore +- - dolore magna aliqua +- - enim ad minim veniam +- - quis nostrud exercitation ullamco laboris nisi +- - aliquip ex ea commodo consequat +- """ ++ Consectetur adipiscing elit: ++ - sed do eiusmod tempor incididunt ut labore ++ - dolore magna aliqua ++ - enim ad minim veniam ++ - quis nostrud exercitation ullamco laboris nisi ++ - aliquip ex ea commodo consequat ++ """ + pass + + + def under_indent(): + """ +- These lines are indented in a way that does not +- make sense. +- """ ++ These lines are indented in a way that does not ++make sense. ++ """ + pass + + + def over_indent(): + """ +- This has a shallow indent +- - But some lines are deeper +- - And the closing quote is too deep ++ This has a shallow indent ++ - But some lines are deeper ++ - And the closing quote is too deep + """ + pass + + + def single_line(): +- """But with a newline after it!""" ++ """But with a newline after it! ++ ++ """ + pass + + +@@ -83,41 +85,41 @@ + + def and_that(): + """ +- "hey yah" """ ++ "hey yah" """ + + + def and_this(): +- ''' +- "hey yah"''' ++ ''' ++ "hey yah"''' + + + def believe_it_or_not_this_is_in_the_py_stdlib(): +- ''' +- "hey yah"''' ++ ''' ++"hey yah"''' + + + def shockingly_the_quotes_are_normalized_v2(): +- ''' ++ """ + Docstring Docstring Docstring +- ''' ++ """ + pass + + + def backslash_space(): +- '\ ' ++ "\ " + + + def multiline_backslash_1(): +- ''' ++ """ + hey\there\ +- \ ''' ++ \ """ + + + def multiline_backslash_2(): +- ''' +- hey there \ ''' ++ """ ++ hey there \ """ + + + def multiline_backslash_3(): +- ''' +- already escaped \\''' ++ """ ++ already escaped \\ """ +``` + +## Ruff Output + +```py +class ALonelyClass: + """ + A multiline class docstring. + """ + + def AnEquallyLonelyMethod(self): + """ + A multiline method docstring""" + pass + + +def one_function(): + """This is a docstring with a single line of text.""" + pass + + +def shockingly_the_quotes_are_normalized(): + """This is a multiline docstring. + This is a multiline docstring. + This is a multiline docstring. + """ + pass + + +def foo(): + """This is a docstring with + some lines of text here + """ + return + + +def baz(): + '''"This" is a string with some + embedded "quotes"''' + return + + +def poit(): + """ + Lorem ipsum dolor sit amet. + + Consectetur adipiscing elit: + - sed do eiusmod tempor incididunt ut labore + - dolore magna aliqua + - enim ad minim veniam + - quis nostrud exercitation ullamco laboris nisi + - aliquip ex ea commodo consequat + """ + pass + + +def under_indent(): + """ + These lines are indented in a way that does not +make sense. + """ + pass + + +def over_indent(): + """ + This has a shallow indent + - But some lines are deeper + - And the closing quote is too deep + """ + pass + + +def single_line(): + """But with a newline after it! + + """ + pass + + +def this(): + r""" + 'hey ho' + """ + + +def that(): + """ "hey yah" """ + + +def and_that(): + """ + "hey yah" """ + + +def and_this(): + ''' + "hey yah"''' + + +def believe_it_or_not_this_is_in_the_py_stdlib(): + ''' +"hey yah"''' + + +def shockingly_the_quotes_are_normalized_v2(): + """ + Docstring Docstring Docstring + """ + pass + + +def backslash_space(): + "\ " + + +def multiline_backslash_1(): + """ + hey\there\ + \ """ + + +def multiline_backslash_2(): + """ + hey there \ """ + + +def multiline_backslash_3(): + """ + already escaped \\ """ +``` + +## Black Output + +```py +class ALonelyClass: + ''' + A multiline class docstring. + ''' + + def AnEquallyLonelyMethod(self): + ''' + A multiline method docstring''' + pass + + +def one_function(): + '''This is a docstring with a single line of text.''' + pass + + +def shockingly_the_quotes_are_normalized(): + '''This is a multiline docstring. + This is a multiline docstring. + This is a multiline docstring. + ''' + pass + + +def foo(): + """This is a docstring with + some lines of text here + """ + return + + +def baz(): + '''"This" is a string with some + embedded "quotes"''' + return + + +def poit(): + """ + Lorem ipsum dolor sit amet. + + Consectetur adipiscing elit: + - sed do eiusmod tempor incididunt ut labore + - dolore magna aliqua + - enim ad minim veniam + - quis nostrud exercitation ullamco laboris nisi + - aliquip ex ea commodo consequat + """ + pass + + +def under_indent(): + """ + These lines are indented in a way that does not + make sense. + """ + pass + + +def over_indent(): + """ + This has a shallow indent + - But some lines are deeper + - And the closing quote is too deep + """ + pass + + +def single_line(): + """But with a newline after it!""" + pass + + +def this(): + r""" + 'hey ho' + """ + + +def that(): + """ "hey yah" """ + + +def and_that(): + """ + "hey yah" """ + + +def and_this(): + ''' + "hey yah"''' + + +def believe_it_or_not_this_is_in_the_py_stdlib(): + ''' + "hey yah"''' + + +def shockingly_the_quotes_are_normalized_v2(): + ''' + Docstring Docstring Docstring + ''' + pass + + +def backslash_space(): + '\ ' + + +def multiline_backslash_1(): + ''' + hey\there\ + \ ''' + + +def multiline_backslash_2(): + ''' + hey there \ ''' + + +def multiline_backslash_3(): + ''' + already escaped \\''' +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__docstring_preview_no_string_normalization.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__docstring_preview_no_string_normalization.py.snap new file mode 100644 index 0000000000..0a617572c9 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__docstring_preview_no_string_normalization.py.snap @@ -0,0 +1,68 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/docstring_preview_no_string_normalization.py +--- +## Input + +```py +def do_not_touch_this_prefix(): + R"""There was a bug where docstring prefixes would be normalized even with -S.""" + + +def do_not_touch_this_prefix2(): + FR'There was a bug where docstring prefixes would be normalized even with -S.' + + +def do_not_touch_this_prefix3(): + u'''There was a bug where docstring prefixes would be normalized even with -S.''' +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -3,8 +3,8 @@ + + + def do_not_touch_this_prefix2(): +- FR'There was a bug where docstring prefixes would be normalized even with -S.' ++ f"NOT_YET_IMPLEMENTED_ExprJoinedStr" + + + def do_not_touch_this_prefix3(): +- u'''There was a bug where docstring prefixes would be normalized even with -S.''' ++ """There was a bug where docstring prefixes would be normalized even with -S.""" +``` + +## Ruff Output + +```py +def do_not_touch_this_prefix(): + R"""There was a bug where docstring prefixes would be normalized even with -S.""" + + +def do_not_touch_this_prefix2(): + f"NOT_YET_IMPLEMENTED_ExprJoinedStr" + + +def do_not_touch_this_prefix3(): + """There was a bug where docstring prefixes would be normalized even with -S.""" +``` + +## Black Output + +```py +def do_not_touch_this_prefix(): + R"""There was a bug where docstring prefixes would be normalized even with -S.""" + + +def do_not_touch_this_prefix2(): + FR'There was a bug where docstring prefixes would be normalized even with -S.' + + +def do_not_touch_this_prefix3(): + u'''There was a bug where docstring prefixes would be normalized even with -S.''' +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__force_pyi.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__force_pyi.py.snap new file mode 100644 index 0000000000..b2ea142296 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__force_pyi.py.snap @@ -0,0 +1,218 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/force_pyi.py +--- +## Input + +```py +from typing import Union + +@bird +def zoo(): ... + +class A: ... +@bar +class B: + def BMethod(self) -> None: ... + @overload + def BMethod(self, arg : List[str]) -> None: ... + +class C: ... +@hmm +class D: ... +class E: ... + +@baz +def foo() -> None: + ... + +class F (A , C): ... +def spam() -> None: ... + +@overload +def spam(arg: str) -> str: ... + +var : int = 1 + +def eggs() -> Union[str, int]: ... +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,32 +1,58 @@ + from typing import Union + ++ + @bird +-def zoo(): ... ++def zoo(): ++ ... + +-class A: ... + ++class A: ++ ... ++ ++ + @bar + class B: +- def BMethod(self) -> None: ... ++ def BMethod(self) -> None: ++ ... ++ + @overload +- def BMethod(self, arg: List[str]) -> None: ... ++ def BMethod(self, arg: List[str]) -> None: ++ ... ++ ++ ++class C: ++ ... + +-class C: ... + + @hmm +-class D: ... ++class D: ++ ... ++ ++ ++class E: ++ ... + +-class E: ... + + @baz +-def foo() -> None: ... ++def foo() -> None: ++ ... + +-class F(A, C): ... + +-def spam() -> None: ... ++class F(A, C): ++ ... ++ ++ ++def spam() -> None: ++ ... ++ ++ + @overload +-def spam(arg: str) -> str: ... ++def spam(arg: str) -> str: ++ ... ++ + + var: int = 1 + +-def eggs() -> Union[str, int]: ... ++ ++def eggs() -> Union[str, int]: ++ ... +``` + +## Ruff Output + +```py +from typing import Union + + +@bird +def zoo(): + ... + + +class A: + ... + + +@bar +class B: + def BMethod(self) -> None: + ... + + @overload + def BMethod(self, arg: List[str]) -> None: + ... + + +class C: + ... + + +@hmm +class D: + ... + + +class E: + ... + + +@baz +def foo() -> None: + ... + + +class F(A, C): + ... + + +def spam() -> None: + ... + + +@overload +def spam(arg: str) -> str: + ... + + +var: int = 1 + + +def eggs() -> Union[str, int]: + ... +``` + +## Black Output + +```py +from typing import Union + +@bird +def zoo(): ... + +class A: ... + +@bar +class B: + def BMethod(self) -> None: ... + @overload + def BMethod(self, arg: List[str]) -> None: ... + +class C: ... + +@hmm +class D: ... + +class E: ... + +@baz +def foo() -> None: ... + +class F(A, C): ... + +def spam() -> None: ... +@overload +def spam(arg: str) -> str: ... + +var: int = 1 + +def eggs() -> Union[str, int]: ... +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__long_strings_flag_disabled.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__long_strings_flag_disabled.py.snap new file mode 100644 index 0000000000..fe648573cc --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__long_strings_flag_disabled.py.snap @@ -0,0 +1,957 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/long_strings_flag_disabled.py +--- +## Input + +```py +x = "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." + +x += "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." + +y = "Short string" + +print( + "This is a really long string inside of a print statement with extra arguments attached at the end of it.", + x, + y, + z, +) + +print( + "This is a really long string inside of a print statement with no extra arguments attached at the end of it." +) + +D1 = { + "The First": "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + "The Second": "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D2 = { + 1.0: "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + 2.0: "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D3 = { + x: "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + y: "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D4 = { + "A long and ridiculous {}".format( + string_key + ): "This is a really really really long string that has to go i,side of a dictionary. It is soooo bad.", + some_func( + "calling", "some", "stuff" + ): "This is a really really really long string that has to go inside of a dictionary. It is {soooo} bad (#{x}).".format( + sooo="soooo", x=2 + ), + "A %s %s" + % ( + "formatted", + "string", + ): "This is a really really really long string that has to go inside of a dictionary. It is %s bad (#%d)." + % ("soooo", 2), +} + +func_with_keywords( + my_arg, + my_kwarg="Long keyword strings also need to be wrapped, but they will probably need to be handled a little bit differently.", +) + +bad_split1 = ( + "But what should happen when code has already been formatted but in the wrong way? Like" + " with a space at the end instead of the beginning. Or what about when it is split too soon?" +) + +bad_split2 = ( + "But what should happen when code has already " + "been formatted but in the wrong way? Like " + "with a space at the end instead of the " + "beginning. Or what about when it is split too " + "soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split." +) + +bad_split3 = ( + "What if we have inline comments on " # First Comment + "each line of a bad split? In that " # Second Comment + "case, we should just leave it alone." # Third Comment +) + +bad_split_func1( + "But what should happen when code has already " + "been formatted but in the wrong way? Like " + "with a space at the end instead of the " + "beginning. Or what about when it is split too " + "soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split.", + xxx, + yyy, + zzz, +) + +bad_split_func2( + xxx, + yyy, + zzz, + long_string_kwarg="But what should happen when code has already been formatted but in the wrong way? Like " + "with a space at the end instead of the beginning. Or what about when it is split too " + "soon?", +) + +bad_split_func3( + ( + "But what should happen when code has already " + r"been formatted but in the wrong way? Like " + "with a space at the end instead of the " + r"beginning. Or what about when it is split too " + r"soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split." + ), + xxx, + yyy, + zzz, +) + +raw_string = r"This is a long raw string. When re-formatting this string, black needs to make sure it prepends the 'r' onto the new string." + +fmt_string1 = "We also need to be sure to preserve any and all {} which may or may not be attached to the string in question.".format( + "method calls" +) + +fmt_string2 = "But what about when the string is {} but {}".format( + "short", + "the method call is really really really really really really really really long?", +) + +old_fmt_string1 = ( + "While we are on the topic of %s, we should also note that old-style formatting must also be preserved, since some %s still uses it." + % ("formatting", "code") +) + +old_fmt_string2 = "This is a %s %s %s %s" % ( + "really really really really really", + "old", + "way to format strings!", + "Use f-strings instead!", +) + +old_fmt_string3 = ( + "Whereas only the strings after the percent sign were long in the last example, this example uses a long initial string as well. This is another %s %s %s %s" + % ( + "really really really really really", + "old", + "way to format strings!", + "Use f-strings instead!", + ) +) + +fstring = f"f-strings definitely make things more {difficult} than they need to be for {{black}}. But boy they sure are handy. The problem is that some lines will need to have the 'f' whereas others do not. This {line}, for example, needs one." + +fstring_with_no_fexprs = f"Some regular string that needs to get split certainly but is NOT an fstring by any means whatsoever." + +comment_string = "Long lines with inline comments should have their comments appended to the reformatted string's enclosing right parentheses." # This comment gets thrown to the top. + +arg_comment_string = print( + "Long lines with inline comments which are apart of (and not the only member of) an argument list should have their comments appended to the reformatted string's enclosing left parentheses.", # This comment stays on the bottom. + "Arg #2", + "Arg #3", + "Arg #4", + "Arg #5", +) + +pragma_comment_string1 = "Lines which end with an inline pragma comment of the form `# : <...>` should be left alone." # noqa: E501 + +pragma_comment_string2 = "Lines which end with an inline pragma comment of the form `# : <...>` should be left alone." # noqa + +"""This is a really really really long triple quote string and it should not be touched.""" + +triple_quote_string = """This is a really really really long triple quote string assignment and it should not be touched.""" + +assert ( + some_type_of_boolean_expression +), "Followed by a really really really long string that is used to provide context to the AssertionError exception." + +assert ( + some_type_of_boolean_expression +), "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic string {}.".format( + "formatting" +) + +assert some_type_of_boolean_expression, ( + "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic string %s." + % "formatting" +) + +assert some_type_of_boolean_expression, ( + "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic %s %s." + % ("string", "formatting") +) + +some_function_call( + "With a reallly generic name and with a really really long string that is, at some point down the line, " + + added + + " to a variable and then added to another string." +) + +some_function_call( + "With a reallly generic name and with a really really long string that is, at some point down the line, " + + added + + " to a variable and then added to another string. But then what happens when the final string is also supppppperrrrr long?! Well then that second (realllllllly long) string should be split too.", + "and a second argument", + and_a_third, +) + +return "A really really really really really really really really really really really really really long {} {}".format( + "return", "value" +) + +func_with_bad_comma( + "This is a really long string argument to a function that has a trailing comma which should NOT be there.", +) + +func_with_bad_comma( + "This is a really long string argument to a function that has a trailing comma which should NOT be there.", # comment after comma +) + +func_with_bad_comma( + ( + "This is a really long string argument to a function that has a trailing comma" + " which should NOT be there." + ), +) + +func_with_bad_comma( + ( + "This is a really long string argument to a function that has a trailing comma" + " which should NOT be there." + ), # comment after comma +) + +func_with_bad_parens_that_wont_fit_in_one_line( + ("short string that should have parens stripped"), x, y, z +) + +func_with_bad_parens_that_wont_fit_in_one_line( + x, y, ("short string that should have parens stripped"), z +) + +func_with_bad_parens( + ("short string that should have parens stripped"), + x, + y, + z, +) + +func_with_bad_parens( + x, + y, + ("short string that should have parens stripped"), + z, +) + +annotated_variable: Final = ( + "This is a large " + + STRING + + " that has been " + + CONCATENATED + + "using the '+' operator." +) +annotated_variable: Final = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." +annotated_variable: Literal[ + "fakse_literal" +] = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." + +backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\" +backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\\\" +backslashes = "This is a really 'long' string with \"embedded double quotes\" and 'single' quotes that also handles checking for an odd number of backslashes \\\", like this...\\\\\\" + +short_string = "Hi" " there." + +func_call(short_string=("Hi" " there.")) + +raw_strings = r"Don't" " get" r" merged" " unless they are all raw." + + +def foo(): + yield "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." + + +x = f"This is a {{really}} long string that needs to be split without a doubt (i.e. most definitely). In short, this {string} that can't possibly be {{expected}} to fit all together on one line. In {fact} it may even take up three or more lines... like four or five... but probably just four." + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # type: ignore + " of it." +) + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # noqa + " of it." +) + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # pylint: disable=some-pylint-check + " of it." +) +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -143,9 +143,9 @@ + ) + ) + +-fstring = f"f-strings definitely make things more {difficult} than they need to be for {{black}}. But boy they sure are handy. The problem is that some lines will need to have the 'f' whereas others do not. This {line}, for example, needs one." ++fstring = f"NOT_YET_IMPLEMENTED_ExprJoinedStr" + +-fstring_with_no_fexprs = f"Some regular string that needs to get split certainly but is NOT an fstring by any means whatsoever." ++fstring_with_no_fexprs = f"NOT_YET_IMPLEMENTED_ExprJoinedStr" + + comment_string = "Long lines with inline comments should have their comments appended to the reformatted string's enclosing right parentheses." # This comment gets thrown to the top. + +@@ -165,25 +165,13 @@ + + triple_quote_string = """This is a really really really long triple quote string assignment and it should not be touched.""" + +-assert ( +- some_type_of_boolean_expression +-), "Followed by a really really really long string that is used to provide context to the AssertionError exception." ++NOT_YET_IMPLEMENTED_StmtAssert + +-assert ( +- some_type_of_boolean_expression +-), "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic string {}.".format( +- "formatting" +-) ++NOT_YET_IMPLEMENTED_StmtAssert + +-assert some_type_of_boolean_expression, ( +- "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic string %s." +- % "formatting" +-) ++NOT_YET_IMPLEMENTED_StmtAssert + +-assert some_type_of_boolean_expression, ( +- "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic %s %s." +- % ("string", "formatting") +-) ++NOT_YET_IMPLEMENTED_StmtAssert + + some_function_call( + "With a reallly generic name and with a really really long string that is, at some point down the line, " +@@ -221,8 +209,8 @@ + func_with_bad_comma( + ( + "This is a really long string argument to a function that has a trailing comma" +- " which should NOT be there." +- ), # comment after comma ++ " which should NOT be there." # comment after comma ++ ), + ) + + func_with_bad_parens_that_wont_fit_in_one_line( +@@ -271,10 +259,10 @@ + + + def foo(): +- yield "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." ++ NOT_YET_IMPLEMENTED_ExprYield + + +-x = f"This is a {{really}} long string that needs to be split without a doubt (i.e. most definitely). In short, this {string} that can't possibly be {{expected}} to fit all together on one line. In {fact} it may even take up three or more lines... like four or five... but probably just four." ++x = f"NOT_YET_IMPLEMENTED_ExprJoinedStr" + + long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # type: ignore +``` + +## Ruff Output + +```py +x = "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." + +x += "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." + +y = "Short string" + +print( + "This is a really long string inside of a print statement with extra arguments attached at the end of it.", + x, + y, + z, +) + +print( + "This is a really long string inside of a print statement with no extra arguments attached at the end of it." +) + +D1 = { + "The First": "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + "The Second": "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D2 = { + 1.0: "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + 2.0: "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D3 = { + x: "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + y: "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D4 = { + "A long and ridiculous {}".format( + string_key + ): "This is a really really really long string that has to go i,side of a dictionary. It is soooo bad.", + some_func( + "calling", "some", "stuff" + ): "This is a really really really long string that has to go inside of a dictionary. It is {soooo} bad (#{x}).".format( + sooo="soooo", x=2 + ), + "A %s %s" + % ( + "formatted", + "string", + ): "This is a really really really long string that has to go inside of a dictionary. It is %s bad (#%d)." + % ("soooo", 2), +} + +func_with_keywords( + my_arg, + my_kwarg="Long keyword strings also need to be wrapped, but they will probably need to be handled a little bit differently.", +) + +bad_split1 = ( + "But what should happen when code has already been formatted but in the wrong way? Like" + " with a space at the end instead of the beginning. Or what about when it is split too soon?" +) + +bad_split2 = ( + "But what should happen when code has already " + "been formatted but in the wrong way? Like " + "with a space at the end instead of the " + "beginning. Or what about when it is split too " + "soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split." +) + +bad_split3 = ( + "What if we have inline comments on " # First Comment + "each line of a bad split? In that " # Second Comment + "case, we should just leave it alone." # Third Comment +) + +bad_split_func1( + "But what should happen when code has already " + "been formatted but in the wrong way? Like " + "with a space at the end instead of the " + "beginning. Or what about when it is split too " + "soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split.", + xxx, + yyy, + zzz, +) + +bad_split_func2( + xxx, + yyy, + zzz, + long_string_kwarg="But what should happen when code has already been formatted but in the wrong way? Like " + "with a space at the end instead of the beginning. Or what about when it is split too " + "soon?", +) + +bad_split_func3( + ( + "But what should happen when code has already " + r"been formatted but in the wrong way? Like " + "with a space at the end instead of the " + r"beginning. Or what about when it is split too " + r"soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split." + ), + xxx, + yyy, + zzz, +) + +raw_string = r"This is a long raw string. When re-formatting this string, black needs to make sure it prepends the 'r' onto the new string." + +fmt_string1 = "We also need to be sure to preserve any and all {} which may or may not be attached to the string in question.".format( + "method calls" +) + +fmt_string2 = "But what about when the string is {} but {}".format( + "short", + "the method call is really really really really really really really really long?", +) + +old_fmt_string1 = ( + "While we are on the topic of %s, we should also note that old-style formatting must also be preserved, since some %s still uses it." + % ("formatting", "code") +) + +old_fmt_string2 = "This is a %s %s %s %s" % ( + "really really really really really", + "old", + "way to format strings!", + "Use f-strings instead!", +) + +old_fmt_string3 = ( + "Whereas only the strings after the percent sign were long in the last example, this example uses a long initial string as well. This is another %s %s %s %s" + % ( + "really really really really really", + "old", + "way to format strings!", + "Use f-strings instead!", + ) +) + +fstring = f"NOT_YET_IMPLEMENTED_ExprJoinedStr" + +fstring_with_no_fexprs = f"NOT_YET_IMPLEMENTED_ExprJoinedStr" + +comment_string = "Long lines with inline comments should have their comments appended to the reformatted string's enclosing right parentheses." # This comment gets thrown to the top. + +arg_comment_string = print( + "Long lines with inline comments which are apart of (and not the only member of) an argument list should have their comments appended to the reformatted string's enclosing left parentheses.", # This comment stays on the bottom. + "Arg #2", + "Arg #3", + "Arg #4", + "Arg #5", +) + +pragma_comment_string1 = "Lines which end with an inline pragma comment of the form `# : <...>` should be left alone." # noqa: E501 + +pragma_comment_string2 = "Lines which end with an inline pragma comment of the form `# : <...>` should be left alone." # noqa + +"""This is a really really really long triple quote string and it should not be touched.""" + +triple_quote_string = """This is a really really really long triple quote string assignment and it should not be touched.""" + +NOT_YET_IMPLEMENTED_StmtAssert + +NOT_YET_IMPLEMENTED_StmtAssert + +NOT_YET_IMPLEMENTED_StmtAssert + +NOT_YET_IMPLEMENTED_StmtAssert + +some_function_call( + "With a reallly generic name and with a really really long string that is, at some point down the line, " + + added + + " to a variable and then added to another string." +) + +some_function_call( + "With a reallly generic name and with a really really long string that is, at some point down the line, " + + added + + " to a variable and then added to another string. But then what happens when the final string is also supppppperrrrr long?! Well then that second (realllllllly long) string should be split too.", + "and a second argument", + and_a_third, +) + +return "A really really really really really really really really really really really really really long {} {}".format( + "return", "value" +) + +func_with_bad_comma( + "This is a really long string argument to a function that has a trailing comma which should NOT be there.", +) + +func_with_bad_comma( + "This is a really long string argument to a function that has a trailing comma which should NOT be there.", # comment after comma +) + +func_with_bad_comma( + ( + "This is a really long string argument to a function that has a trailing comma" + " which should NOT be there." + ), +) + +func_with_bad_comma( + ( + "This is a really long string argument to a function that has a trailing comma" + " which should NOT be there." # comment after comma + ), +) + +func_with_bad_parens_that_wont_fit_in_one_line( + ("short string that should have parens stripped"), x, y, z +) + +func_with_bad_parens_that_wont_fit_in_one_line( + x, y, ("short string that should have parens stripped"), z +) + +func_with_bad_parens( + ("short string that should have parens stripped"), + x, + y, + z, +) + +func_with_bad_parens( + x, + y, + ("short string that should have parens stripped"), + z, +) + +annotated_variable: Final = ( + "This is a large " + + STRING + + " that has been " + + CONCATENATED + + "using the '+' operator." +) +annotated_variable: Final = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." +annotated_variable: Literal[ + "fakse_literal" +] = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." + +backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\" +backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\\\" +backslashes = "This is a really 'long' string with \"embedded double quotes\" and 'single' quotes that also handles checking for an odd number of backslashes \\\", like this...\\\\\\" + +short_string = "Hi" " there." + +func_call(short_string=("Hi" " there.")) + +raw_strings = r"Don't" " get" r" merged" " unless they are all raw." + + +def foo(): + NOT_YET_IMPLEMENTED_ExprYield + + +x = f"NOT_YET_IMPLEMENTED_ExprJoinedStr" + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # type: ignore + " of it." +) + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # noqa + " of it." +) + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # pylint: disable=some-pylint-check + " of it." +) +``` + +## Black Output + +```py +x = "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." + +x += "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." + +y = "Short string" + +print( + "This is a really long string inside of a print statement with extra arguments attached at the end of it.", + x, + y, + z, +) + +print( + "This is a really long string inside of a print statement with no extra arguments attached at the end of it." +) + +D1 = { + "The First": "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + "The Second": "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D2 = { + 1.0: "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + 2.0: "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D3 = { + x: "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + y: "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D4 = { + "A long and ridiculous {}".format( + string_key + ): "This is a really really really long string that has to go i,side of a dictionary. It is soooo bad.", + some_func( + "calling", "some", "stuff" + ): "This is a really really really long string that has to go inside of a dictionary. It is {soooo} bad (#{x}).".format( + sooo="soooo", x=2 + ), + "A %s %s" + % ( + "formatted", + "string", + ): "This is a really really really long string that has to go inside of a dictionary. It is %s bad (#%d)." + % ("soooo", 2), +} + +func_with_keywords( + my_arg, + my_kwarg="Long keyword strings also need to be wrapped, but they will probably need to be handled a little bit differently.", +) + +bad_split1 = ( + "But what should happen when code has already been formatted but in the wrong way? Like" + " with a space at the end instead of the beginning. Or what about when it is split too soon?" +) + +bad_split2 = ( + "But what should happen when code has already " + "been formatted but in the wrong way? Like " + "with a space at the end instead of the " + "beginning. Or what about when it is split too " + "soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split." +) + +bad_split3 = ( + "What if we have inline comments on " # First Comment + "each line of a bad split? In that " # Second Comment + "case, we should just leave it alone." # Third Comment +) + +bad_split_func1( + "But what should happen when code has already " + "been formatted but in the wrong way? Like " + "with a space at the end instead of the " + "beginning. Or what about when it is split too " + "soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split.", + xxx, + yyy, + zzz, +) + +bad_split_func2( + xxx, + yyy, + zzz, + long_string_kwarg="But what should happen when code has already been formatted but in the wrong way? Like " + "with a space at the end instead of the beginning. Or what about when it is split too " + "soon?", +) + +bad_split_func3( + ( + "But what should happen when code has already " + r"been formatted but in the wrong way? Like " + "with a space at the end instead of the " + r"beginning. Or what about when it is split too " + r"soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split." + ), + xxx, + yyy, + zzz, +) + +raw_string = r"This is a long raw string. When re-formatting this string, black needs to make sure it prepends the 'r' onto the new string." + +fmt_string1 = "We also need to be sure to preserve any and all {} which may or may not be attached to the string in question.".format( + "method calls" +) + +fmt_string2 = "But what about when the string is {} but {}".format( + "short", + "the method call is really really really really really really really really long?", +) + +old_fmt_string1 = ( + "While we are on the topic of %s, we should also note that old-style formatting must also be preserved, since some %s still uses it." + % ("formatting", "code") +) + +old_fmt_string2 = "This is a %s %s %s %s" % ( + "really really really really really", + "old", + "way to format strings!", + "Use f-strings instead!", +) + +old_fmt_string3 = ( + "Whereas only the strings after the percent sign were long in the last example, this example uses a long initial string as well. This is another %s %s %s %s" + % ( + "really really really really really", + "old", + "way to format strings!", + "Use f-strings instead!", + ) +) + +fstring = f"f-strings definitely make things more {difficult} than they need to be for {{black}}. But boy they sure are handy. The problem is that some lines will need to have the 'f' whereas others do not. This {line}, for example, needs one." + +fstring_with_no_fexprs = f"Some regular string that needs to get split certainly but is NOT an fstring by any means whatsoever." + +comment_string = "Long lines with inline comments should have their comments appended to the reformatted string's enclosing right parentheses." # This comment gets thrown to the top. + +arg_comment_string = print( + "Long lines with inline comments which are apart of (and not the only member of) an argument list should have their comments appended to the reformatted string's enclosing left parentheses.", # This comment stays on the bottom. + "Arg #2", + "Arg #3", + "Arg #4", + "Arg #5", +) + +pragma_comment_string1 = "Lines which end with an inline pragma comment of the form `# : <...>` should be left alone." # noqa: E501 + +pragma_comment_string2 = "Lines which end with an inline pragma comment of the form `# : <...>` should be left alone." # noqa + +"""This is a really really really long triple quote string and it should not be touched.""" + +triple_quote_string = """This is a really really really long triple quote string assignment and it should not be touched.""" + +assert ( + some_type_of_boolean_expression +), "Followed by a really really really long string that is used to provide context to the AssertionError exception." + +assert ( + some_type_of_boolean_expression +), "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic string {}.".format( + "formatting" +) + +assert some_type_of_boolean_expression, ( + "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic string %s." + % "formatting" +) + +assert some_type_of_boolean_expression, ( + "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic %s %s." + % ("string", "formatting") +) + +some_function_call( + "With a reallly generic name and with a really really long string that is, at some point down the line, " + + added + + " to a variable and then added to another string." +) + +some_function_call( + "With a reallly generic name and with a really really long string that is, at some point down the line, " + + added + + " to a variable and then added to another string. But then what happens when the final string is also supppppperrrrr long?! Well then that second (realllllllly long) string should be split too.", + "and a second argument", + and_a_third, +) + +return "A really really really really really really really really really really really really really long {} {}".format( + "return", "value" +) + +func_with_bad_comma( + "This is a really long string argument to a function that has a trailing comma which should NOT be there.", +) + +func_with_bad_comma( + "This is a really long string argument to a function that has a trailing comma which should NOT be there.", # comment after comma +) + +func_with_bad_comma( + ( + "This is a really long string argument to a function that has a trailing comma" + " which should NOT be there." + ), +) + +func_with_bad_comma( + ( + "This is a really long string argument to a function that has a trailing comma" + " which should NOT be there." + ), # comment after comma +) + +func_with_bad_parens_that_wont_fit_in_one_line( + ("short string that should have parens stripped"), x, y, z +) + +func_with_bad_parens_that_wont_fit_in_one_line( + x, y, ("short string that should have parens stripped"), z +) + +func_with_bad_parens( + ("short string that should have parens stripped"), + x, + y, + z, +) + +func_with_bad_parens( + x, + y, + ("short string that should have parens stripped"), + z, +) + +annotated_variable: Final = ( + "This is a large " + + STRING + + " that has been " + + CONCATENATED + + "using the '+' operator." +) +annotated_variable: Final = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." +annotated_variable: Literal[ + "fakse_literal" +] = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." + +backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\" +backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\\\" +backslashes = "This is a really 'long' string with \"embedded double quotes\" and 'single' quotes that also handles checking for an odd number of backslashes \\\", like this...\\\\\\" + +short_string = "Hi" " there." + +func_call(short_string=("Hi" " there.")) + +raw_strings = r"Don't" " get" r" merged" " unless they are all raw." + + +def foo(): + yield "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." + + +x = f"This is a {{really}} long string that needs to be split without a doubt (i.e. most definitely). In short, this {string} that can't possibly be {{expected}} to fit all together on one line. In {fact} it may even take up three or more lines... like four or five... but probably just four." + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # type: ignore + " of it." +) + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # noqa + " of it." +) + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # pylint: disable=some-pylint-check + " of it." +) +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__power_op_newline.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__power_op_newline.py.snap new file mode 100644 index 0000000000..991e030ac9 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__power_op_newline.py.snap @@ -0,0 +1,44 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/power_op_newline.py +--- +## Input + +```py +importA;()<<0**0# +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,6 +1,2 @@ + importA +-( +- () +- << 0 +- ** 0 +-) # ++() << 0**0 # +``` + +## Ruff Output + +```py +importA +() << 0**0 # +``` + +## Black Output + +```py +importA +( + () + << 0 + ** 0 +) # +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__string_quotes.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__string_quotes.py.snap new file mode 100644 index 0000000000..f2d2835ccc --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__string_quotes.py.snap @@ -0,0 +1,244 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/miscellaneous/string_quotes.py +--- +## Input + +```py +'''''' +'\'' +'"' +"'" +"\"" +"Hello" +"Don't do that" +'Here is a "' +'What\'s the deal here?' +"What's the deal \"here\"?" +"And \"here\"?" +"""Strings with "" in them""" +'''Strings with "" in them''' +'''Here's a "''' +'''Here's a " ''' +'''Just a normal triple +quote''' +f"just a normal {f} string" +f'''This is a triple-quoted {f}-string''' +f'MOAR {" ".join([])}' +f"MOAR {' '.join([])}" +r"raw string ftw" +r'Date d\'expiration:(.*)' +r'Tricky "quote' +r'Not-so-tricky \"quote' +rf'{yay}' +'\n\ +The \"quick\"\n\ +brown fox\n\ +jumps over\n\ +the \'lazy\' dog.\n\ +' +re.compile(r'[\\"]') +"x = ''; y = \"\"" +"x = '''; y = \"\"" +"x = ''''; y = \"\"" +"x = '' ''; y = \"\"" +"x = ''; y = \"\"\"" +"x = '''; y = \"\"\"\"" +"x = ''''; y = \"\"\"\"\"" +"x = '' ''; y = \"\"\"\"\"" +'unnecessary \"\"escaping' +"unnecessary \'\'escaping" +'\\""' +"\\''" +'Lots of \\\\\\\\\'quotes\'' +f'{y * " "} \'{z}\'' +f'{{y * " "}} \'{z}\'' +f'\'{z}\' {y * " "}' +f'{y * x} \'{z}\'' +'\'{z}\' {y * " "}' +'{y * x} \'{z}\'' + +# We must bail out if changing the quotes would introduce backslashes in f-string +# expressions. xref: https://github.com/psf/black/issues/2348 +f"\"{b}\"{' ' * (long-len(b)+1)}: \"{sts}\",\n" +f"\"{a}\"{'hello' * b}\"{c}\"" +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -15,16 +15,21 @@ + """Here's a " """ + """Just a normal triple + quote""" +-f"just a normal {f} string" +-f"""This is a triple-quoted {f}-string""" +-f'MOAR {" ".join([])}' +-f"MOAR {' '.join([])}" ++f"NOT_YET_IMPLEMENTED_ExprJoinedStr" ++f"NOT_YET_IMPLEMENTED_ExprJoinedStr" ++f"NOT_YET_IMPLEMENTED_ExprJoinedStr" ++f"NOT_YET_IMPLEMENTED_ExprJoinedStr" + r"raw string ftw" +-r"Date d\'expiration:(.*)" ++r"Date d'expiration:(.*)" + r'Tricky "quote' +-r"Not-so-tricky \"quote" +-rf"{yay}" +-"\nThe \"quick\"\nbrown fox\njumps over\nthe 'lazy' dog.\n" ++r'Not-so-tricky "quote' ++f"NOT_YET_IMPLEMENTED_ExprJoinedStr" ++"\n\ ++The \"quick\"\n\ ++brown fox\n\ ++jumps over\n\ ++the 'lazy' dog.\n\ ++" + re.compile(r'[\\"]') + "x = ''; y = \"\"" + "x = '''; y = \"\"" +@@ -39,14 +44,14 @@ + '\\""' + "\\''" + "Lots of \\\\\\\\'quotes'" +-f'{y * " "} \'{z}\'' +-f"{{y * \" \"}} '{z}'" +-f'\'{z}\' {y * " "}' +-f"{y * x} '{z}'" ++f"NOT_YET_IMPLEMENTED_ExprJoinedStr" ++f"NOT_YET_IMPLEMENTED_ExprJoinedStr" ++f"NOT_YET_IMPLEMENTED_ExprJoinedStr" ++f"NOT_YET_IMPLEMENTED_ExprJoinedStr" + "'{z}' {y * \" \"}" + "{y * x} '{z}'" + + # We must bail out if changing the quotes would introduce backslashes in f-string + # expressions. xref: https://github.com/psf/black/issues/2348 +-f"\"{b}\"{' ' * (long-len(b)+1)}: \"{sts}\",\n" +-f"\"{a}\"{'hello' * b}\"{c}\"" ++f"NOT_YET_IMPLEMENTED_ExprJoinedStr" ++f"NOT_YET_IMPLEMENTED_ExprJoinedStr" +``` + +## Ruff Output + +```py +"""""" +"'" +'"' +"'" +'"' +"Hello" +"Don't do that" +'Here is a "' +"What's the deal here?" +'What\'s the deal "here"?' +'And "here"?' +"""Strings with "" in them""" +"""Strings with "" in them""" +'''Here's a "''' +"""Here's a " """ +"""Just a normal triple +quote""" +f"NOT_YET_IMPLEMENTED_ExprJoinedStr" +f"NOT_YET_IMPLEMENTED_ExprJoinedStr" +f"NOT_YET_IMPLEMENTED_ExprJoinedStr" +f"NOT_YET_IMPLEMENTED_ExprJoinedStr" +r"raw string ftw" +r"Date d'expiration:(.*)" +r'Tricky "quote' +r'Not-so-tricky "quote' +f"NOT_YET_IMPLEMENTED_ExprJoinedStr" +"\n\ +The \"quick\"\n\ +brown fox\n\ +jumps over\n\ +the 'lazy' dog.\n\ +" +re.compile(r'[\\"]') +"x = ''; y = \"\"" +"x = '''; y = \"\"" +"x = ''''; y = \"\"" +"x = '' ''; y = \"\"" +'x = \'\'; y = """' +'x = \'\'\'; y = """"' +'x = \'\'\'\'; y = """""' +'x = \'\' \'\'; y = """""' +'unnecessary ""escaping' +"unnecessary ''escaping" +'\\""' +"\\''" +"Lots of \\\\\\\\'quotes'" +f"NOT_YET_IMPLEMENTED_ExprJoinedStr" +f"NOT_YET_IMPLEMENTED_ExprJoinedStr" +f"NOT_YET_IMPLEMENTED_ExprJoinedStr" +f"NOT_YET_IMPLEMENTED_ExprJoinedStr" +"'{z}' {y * \" \"}" +"{y * x} '{z}'" + +# We must bail out if changing the quotes would introduce backslashes in f-string +# expressions. xref: https://github.com/psf/black/issues/2348 +f"NOT_YET_IMPLEMENTED_ExprJoinedStr" +f"NOT_YET_IMPLEMENTED_ExprJoinedStr" +``` + +## Black Output + +```py +"""""" +"'" +'"' +"'" +'"' +"Hello" +"Don't do that" +'Here is a "' +"What's the deal here?" +'What\'s the deal "here"?' +'And "here"?' +"""Strings with "" in them""" +"""Strings with "" in them""" +'''Here's a "''' +"""Here's a " """ +"""Just a normal triple +quote""" +f"just a normal {f} string" +f"""This is a triple-quoted {f}-string""" +f'MOAR {" ".join([])}' +f"MOAR {' '.join([])}" +r"raw string ftw" +r"Date d\'expiration:(.*)" +r'Tricky "quote' +r"Not-so-tricky \"quote" +rf"{yay}" +"\nThe \"quick\"\nbrown fox\njumps over\nthe 'lazy' dog.\n" +re.compile(r'[\\"]') +"x = ''; y = \"\"" +"x = '''; y = \"\"" +"x = ''''; y = \"\"" +"x = '' ''; y = \"\"" +'x = \'\'; y = """' +'x = \'\'\'; y = """"' +'x = \'\'\'\'; y = """""' +'x = \'\' \'\'; y = """""' +'unnecessary ""escaping' +"unnecessary ''escaping" +'\\""' +"\\''" +"Lots of \\\\\\\\'quotes'" +f'{y * " "} \'{z}\'' +f"{{y * \" \"}} '{z}'" +f'\'{z}\' {y * " "}' +f"{y * x} '{z}'" +"'{z}' {y * \" \"}" +"{y * x} '{z}'" + +# We must bail out if changing the quotes would introduce backslashes in f-string +# expressions. xref: https://github.com/psf/black/issues/2348 +f"\"{b}\"{' ' * (long-len(b)+1)}: \"{sts}\",\n" +f"\"{a}\"{'hello' * b}\"{c}\"" +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_complex.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_complex.py.snap new file mode 100644 index 0000000000..ce5a833fd2 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_complex.py.snap @@ -0,0 +1,549 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_complex.py +--- +## Input + +```py +# Cases sampled from Lib/test/test_patma.py + +# case black_test_patma_098 +match x: + case -0j: + y = 0 +# case black_test_patma_142 +match x: + case bytes(z): + y = 0 +# case black_test_patma_073 +match x: + case 0 if 0: + y = 0 + case 0 if 1: + y = 1 +# case black_test_patma_006 +match 3: + case 0 | 1 | 2 | 3: + x = True +# case black_test_patma_049 +match x: + case [0, 1] | [1, 0]: + y = 0 +# case black_check_sequence_then_mapping +match x: + case [*_]: + return "seq" + case {}: + return "map" +# case black_test_patma_035 +match x: + case {0: [1, 2, {}]}: + y = 0 + case {0: [1, 2, {}] | True} | {1: [[]]} | {0: [1, 2, {}]} | [] | "X" | {}: + y = 1 + case []: + y = 2 +# case black_test_patma_107 +match x: + case 0.25 + 1.75j: + y = 0 +# case black_test_patma_097 +match x: + case -0j: + y = 0 +# case black_test_patma_007 +match 4: + case 0 | 1 | 2 | 3: + x = True +# case black_test_patma_154 +match x: + case 0 if x: + y = 0 +# case black_test_patma_134 +match x: + case {1: 0}: + y = 0 + case {0: 0}: + y = 1 + case {**z}: + y = 2 +# case black_test_patma_185 +match Seq(): + case [*_]: + y = 0 +# case black_test_patma_063 +match x: + case 1: + y = 0 + case 1: + y = 1 +# case black_test_patma_248 +match x: + case {"foo": bar}: + y = bar +# case black_test_patma_019 +match (0, 1, 2): + case [0, 1, *x, 2]: + y = 0 +# case black_test_patma_052 +match x: + case [0]: + y = 0 + case [1, 0] if (x := x[:0]): + y = 1 + case [1, 0]: + y = 2 +# case black_test_patma_191 +match w: + case [x, y, *_]: + z = 0 +# case black_test_patma_110 +match x: + case -0.25 - 1.75j: + y = 0 +# case black_test_patma_151 +match (x,): + case [y]: + z = 0 +# case black_test_patma_114 +match x: + case A.B.C.D: + y = 0 +# case black_test_patma_232 +match x: + case None: + y = 0 +# case black_test_patma_058 +match x: + case 0: + y = 0 +# case black_test_patma_233 +match x: + case False: + y = 0 +# case black_test_patma_078 +match x: + case []: + y = 0 + case [""]: + y = 1 + case "": + y = 2 +# case black_test_patma_156 +match x: + case z: + y = 0 +# case black_test_patma_189 +match w: + case [x, y, *rest]: + z = 0 +# case black_test_patma_042 +match x: + case (0 as z) | (1 as z) | (2 as z) if z == x % 2: + y = 0 +# case black_test_patma_034 +match x: + case {0: [1, 2, {}]}: + y = 0 + case {0: [1, 2, {}] | False} | {1: [[]]} | {0: [1, 2, {}]} | [] | "X" | {}: + y = 1 + case []: + y = 2 +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,144 +1,60 @@ + # Cases sampled from Lib/test/test_patma.py + + # case black_test_patma_098 +-match x: +- case -0j: +- y = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_142 +-match x: +- case bytes(z): +- y = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_073 +-match x: +- case 0 if 0: +- y = 0 +- case 0 if 1: +- y = 1 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_006 +-match 3: +- case 0 | 1 | 2 | 3: +- x = True ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_049 +-match x: +- case [0, 1] | [1, 0]: +- y = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_check_sequence_then_mapping +-match x: +- case [*_]: +- return "seq" +- case {}: +- return "map" ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_035 +-match x: +- case {0: [1, 2, {}]}: +- y = 0 +- case {0: [1, 2, {}] | True} | {1: [[]]} | {0: [1, 2, {}]} | [] | "X" | {}: +- y = 1 +- case []: +- y = 2 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_107 +-match x: +- case 0.25 + 1.75j: +- y = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_097 +-match x: +- case -0j: +- y = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_007 +-match 4: +- case 0 | 1 | 2 | 3: +- x = True ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_154 +-match x: +- case 0 if x: +- y = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_134 +-match x: +- case {1: 0}: +- y = 0 +- case {0: 0}: +- y = 1 +- case {**z}: +- y = 2 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_185 +-match Seq(): +- case [*_]: +- y = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_063 +-match x: +- case 1: +- y = 0 +- case 1: +- y = 1 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_248 +-match x: +- case {"foo": bar}: +- y = bar ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_019 +-match (0, 1, 2): +- case [0, 1, *x, 2]: +- y = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_052 +-match x: +- case [0]: +- y = 0 +- case [1, 0] if (x := x[:0]): +- y = 1 +- case [1, 0]: +- y = 2 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_191 +-match w: +- case [x, y, *_]: +- z = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_110 +-match x: +- case -0.25 - 1.75j: +- y = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_151 +-match (x,): +- case [y]: +- z = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_114 +-match x: +- case A.B.C.D: +- y = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_232 +-match x: +- case None: +- y = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_058 +-match x: +- case 0: +- y = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_233 +-match x: +- case False: +- y = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_078 +-match x: +- case []: +- y = 0 +- case [""]: +- y = 1 +- case "": +- y = 2 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_156 +-match x: +- case z: +- y = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_189 +-match w: +- case [x, y, *rest]: +- z = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_042 +-match x: +- case (0 as z) | (1 as z) | (2 as z) if z == x % 2: +- y = 0 ++NOT_YET_IMPLEMENTED_StmtMatch + # case black_test_patma_034 +-match x: +- case {0: [1, 2, {}]}: +- y = 0 +- case {0: [1, 2, {}] | False} | {1: [[]]} | {0: [1, 2, {}]} | [] | "X" | {}: +- y = 1 +- case []: +- y = 2 ++NOT_YET_IMPLEMENTED_StmtMatch +``` + +## Ruff Output + +```py +# Cases sampled from Lib/test/test_patma.py + +# case black_test_patma_098 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_142 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_073 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_006 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_049 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_check_sequence_then_mapping +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_035 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_107 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_097 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_007 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_154 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_134 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_185 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_063 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_248 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_019 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_052 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_191 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_110 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_151 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_114 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_232 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_058 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_233 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_078 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_156 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_189 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_042 +NOT_YET_IMPLEMENTED_StmtMatch +# case black_test_patma_034 +NOT_YET_IMPLEMENTED_StmtMatch +``` + +## Black Output + +```py +# Cases sampled from Lib/test/test_patma.py + +# case black_test_patma_098 +match x: + case -0j: + y = 0 +# case black_test_patma_142 +match x: + case bytes(z): + y = 0 +# case black_test_patma_073 +match x: + case 0 if 0: + y = 0 + case 0 if 1: + y = 1 +# case black_test_patma_006 +match 3: + case 0 | 1 | 2 | 3: + x = True +# case black_test_patma_049 +match x: + case [0, 1] | [1, 0]: + y = 0 +# case black_check_sequence_then_mapping +match x: + case [*_]: + return "seq" + case {}: + return "map" +# case black_test_patma_035 +match x: + case {0: [1, 2, {}]}: + y = 0 + case {0: [1, 2, {}] | True} | {1: [[]]} | {0: [1, 2, {}]} | [] | "X" | {}: + y = 1 + case []: + y = 2 +# case black_test_patma_107 +match x: + case 0.25 + 1.75j: + y = 0 +# case black_test_patma_097 +match x: + case -0j: + y = 0 +# case black_test_patma_007 +match 4: + case 0 | 1 | 2 | 3: + x = True +# case black_test_patma_154 +match x: + case 0 if x: + y = 0 +# case black_test_patma_134 +match x: + case {1: 0}: + y = 0 + case {0: 0}: + y = 1 + case {**z}: + y = 2 +# case black_test_patma_185 +match Seq(): + case [*_]: + y = 0 +# case black_test_patma_063 +match x: + case 1: + y = 0 + case 1: + y = 1 +# case black_test_patma_248 +match x: + case {"foo": bar}: + y = bar +# case black_test_patma_019 +match (0, 1, 2): + case [0, 1, *x, 2]: + y = 0 +# case black_test_patma_052 +match x: + case [0]: + y = 0 + case [1, 0] if (x := x[:0]): + y = 1 + case [1, 0]: + y = 2 +# case black_test_patma_191 +match w: + case [x, y, *_]: + z = 0 +# case black_test_patma_110 +match x: + case -0.25 - 1.75j: + y = 0 +# case black_test_patma_151 +match (x,): + case [y]: + z = 0 +# case black_test_patma_114 +match x: + case A.B.C.D: + y = 0 +# case black_test_patma_232 +match x: + case None: + y = 0 +# case black_test_patma_058 +match x: + case 0: + y = 0 +# case black_test_patma_233 +match x: + case False: + y = 0 +# case black_test_patma_078 +match x: + case []: + y = 0 + case [""]: + y = 1 + case "": + y = 2 +# case black_test_patma_156 +match x: + case z: + y = 0 +# case black_test_patma_189 +match w: + case [x, y, *rest]: + z = 0 +# case black_test_patma_042 +match x: + case (0 as z) | (1 as z) | (2 as z) if z == x % 2: + y = 0 +# case black_test_patma_034 +match x: + case {0: [1, 2, {}]}: + y = 0 + case {0: [1, 2, {}] | False} | {1: [[]]} | {0: [1, 2, {}]} | [] | "X" | {}: + y = 1 + case []: + y = 2 +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_extras.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_extras.py.snap new file mode 100644 index 0000000000..23dd576815 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_extras.py.snap @@ -0,0 +1,441 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_extras.py +--- +## Input + +```py +import match + +match something: + case [a as b]: + print(b) + case [a as b, c, d, e as f]: + print(f) + case Point(a as b): + print(b) + case Point(int() as x, int() as y): + print(x, y) + + +match = 1 +case: int = re.match(something) + +match re.match(case): + case type("match", match): + pass + case match: + pass + + +def func(match: case, case: match) -> case: + match Something(): + case func(match, case): + ... + case another: + ... + + +match maybe, multiple: + case perhaps, 5: + pass + case perhaps, 6,: + pass + + +match more := (than, one), indeed,: + case _, (5, 6): + pass + case [[5], (6)], [7],: + pass + case _: + pass + + +match a, *b, c: + case [*_]: + assert "seq" == _ + case {}: + assert "map" == b + + +match match( + case, + match( + match, case, match, looooooooooooooooooooooooooooooooooooong, match, case, match + ), + case, +): + case case( + match=case, + case=re.match( + loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong + ), + ): + pass + + case [a as match]: + pass + + case case: + pass + + +match match: + case case: + pass + + +match a, *b(), c: + case d, *f, g: + pass + + +match something: + case { + "key": key as key_1, + "password": PASS.ONE | PASS.TWO | PASS.THREE as password, + }: + pass + case {"maybe": something(complicated as this) as that}: + pass + + +match something: + case 1 as a: + pass + + case 2 as b, 3 as c: + pass + + case 4 as d, (5 as e), (6 | 7 as g), *h: + pass + + +match bar1: + case Foo(aa=Callable() as aa, bb=int()): + print(bar1.aa, bar1.bb) + case _: + print("no match", "\n") + + +match bar1: + case Foo( + normal=x, perhaps=[list, {"x": d, "y": 1.0}] as y, otherwise=something, q=t as u + ): + pass +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,119 +1,43 @@ + import match + +-match something: +- case [a as b]: +- print(b) +- case [a as b, c, d, e as f]: +- print(f) +- case Point(a as b): +- print(b) +- case Point(int() as x, int() as y): +- print(x, y) ++NOT_YET_IMPLEMENTED_StmtMatch + + + match = 1 + case: int = re.match(something) + +-match re.match(case): +- case type("match", match): +- pass +- case match: +- pass ++NOT_YET_IMPLEMENTED_StmtMatch + + + def func(match: case, case: match) -> case: +- match Something(): +- case func(match, case): +- ... +- case another: +- ... ++ NOT_YET_IMPLEMENTED_StmtMatch + + +-match maybe, multiple: +- case perhaps, 5: +- pass +- case perhaps, 6,: +- pass ++NOT_YET_IMPLEMENTED_StmtMatch + + +-match more := (than, one), indeed,: +- case _, (5, 6): +- pass +- case [[5], (6)], [7],: +- pass +- case _: +- pass ++NOT_YET_IMPLEMENTED_StmtMatch + + +-match a, *b, c: +- case [*_]: +- assert "seq" == _ +- case {}: +- assert "map" == b ++NOT_YET_IMPLEMENTED_StmtMatch + + +-match match( +- case, +- match( +- match, case, match, looooooooooooooooooooooooooooooooooooong, match, case, match +- ), +- case, +-): +- case case( +- match=case, +- case=re.match( +- loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong +- ), +- ): +- pass ++NOT_YET_IMPLEMENTED_StmtMatch + +- case [a as match]: +- pass + +- case case: +- pass +- +- +-match match: +- case case: +- pass +- ++NOT_YET_IMPLEMENTED_StmtMatch + +-match a, *b(), c: +- case d, *f, g: +- pass + +- +-match something: +- case { +- "key": key as key_1, +- "password": PASS.ONE | PASS.TWO | PASS.THREE as password, +- }: +- pass +- case {"maybe": something(complicated as this) as that}: +- pass ++NOT_YET_IMPLEMENTED_StmtMatch + + +-match something: +- case 1 as a: +- pass ++NOT_YET_IMPLEMENTED_StmtMatch + +- case 2 as b, 3 as c: +- pass + +- case 4 as d, (5 as e), (6 | 7 as g), *h: +- pass ++NOT_YET_IMPLEMENTED_StmtMatch + + +-match bar1: +- case Foo(aa=Callable() as aa, bb=int()): +- print(bar1.aa, bar1.bb) +- case _: +- print("no match", "\n") ++NOT_YET_IMPLEMENTED_StmtMatch + + +-match bar1: +- case Foo( +- normal=x, perhaps=[list, {"x": d, "y": 1.0}] as y, otherwise=something, q=t as u +- ): +- pass ++NOT_YET_IMPLEMENTED_StmtMatch +``` + +## Ruff Output + +```py +import match + +NOT_YET_IMPLEMENTED_StmtMatch + + +match = 1 +case: int = re.match(something) + +NOT_YET_IMPLEMENTED_StmtMatch + + +def func(match: case, case: match) -> case: + NOT_YET_IMPLEMENTED_StmtMatch + + +NOT_YET_IMPLEMENTED_StmtMatch + + +NOT_YET_IMPLEMENTED_StmtMatch + + +NOT_YET_IMPLEMENTED_StmtMatch + + +NOT_YET_IMPLEMENTED_StmtMatch + + +NOT_YET_IMPLEMENTED_StmtMatch + + +NOT_YET_IMPLEMENTED_StmtMatch + + +NOT_YET_IMPLEMENTED_StmtMatch + + +NOT_YET_IMPLEMENTED_StmtMatch + + +NOT_YET_IMPLEMENTED_StmtMatch + + +NOT_YET_IMPLEMENTED_StmtMatch +``` + +## Black Output + +```py +import match + +match something: + case [a as b]: + print(b) + case [a as b, c, d, e as f]: + print(f) + case Point(a as b): + print(b) + case Point(int() as x, int() as y): + print(x, y) + + +match = 1 +case: int = re.match(something) + +match re.match(case): + case type("match", match): + pass + case match: + pass + + +def func(match: case, case: match) -> case: + match Something(): + case func(match, case): + ... + case another: + ... + + +match maybe, multiple: + case perhaps, 5: + pass + case perhaps, 6,: + pass + + +match more := (than, one), indeed,: + case _, (5, 6): + pass + case [[5], (6)], [7],: + pass + case _: + pass + + +match a, *b, c: + case [*_]: + assert "seq" == _ + case {}: + assert "map" == b + + +match match( + case, + match( + match, case, match, looooooooooooooooooooooooooooooooooooong, match, case, match + ), + case, +): + case case( + match=case, + case=re.match( + loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong + ), + ): + pass + + case [a as match]: + pass + + case case: + pass + + +match match: + case case: + pass + + +match a, *b(), c: + case d, *f, g: + pass + + +match something: + case { + "key": key as key_1, + "password": PASS.ONE | PASS.TWO | PASS.THREE as password, + }: + pass + case {"maybe": something(complicated as this) as that}: + pass + + +match something: + case 1 as a: + pass + + case 2 as b, 3 as c: + pass + + case 4 as d, (5 as e), (6 | 7 as g), *h: + pass + + +match bar1: + case Foo(aa=Callable() as aa, bb=int()): + print(bar1.aa, bar1.bb) + case _: + print("no match", "\n") + + +match bar1: + case Foo( + normal=x, perhaps=[list, {"x": d, "y": 1.0}] as y, otherwise=something, q=t as u + ): + pass +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_generic.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_generic.py.snap new file mode 100644 index 0000000000..6e8d106770 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_generic.py.snap @@ -0,0 +1,420 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_generic.py +--- +## Input + +```py +re.match() +match = a +with match() as match: + match = f"{match}" + +re.match() +match = a +with match() as match: + match = f"{match}" + + +def get_grammars(target_versions: Set[TargetVersion]) -> List[Grammar]: + if not target_versions: + # No target_version specified, so try all grammars. + return [ + # Python 3.7+ + pygram.python_grammar_no_print_statement_no_exec_statement_async_keywords, + # Python 3.0-3.6 + pygram.python_grammar_no_print_statement_no_exec_statement, + # Python 2.7 with future print_function import + pygram.python_grammar_no_print_statement, + # Python 2.7 + pygram.python_grammar, + ] + + match match: + case case: + match match: + case case: + pass + + if all(version.is_python2() for version in target_versions): + # Python 2-only code, so try Python 2 grammars. + return [ + # Python 2.7 with future print_function import + pygram.python_grammar_no_print_statement, + # Python 2.7 + pygram.python_grammar, + ] + + re.match() + match = a + with match() as match: + match = f"{match}" + + def test_patma_139(self): + x = False + match x: + case bool(z): + y = 0 + self.assertIs(x, False) + self.assertEqual(y, 0) + self.assertIs(z, x) + + # Python 3-compatible code, so only try Python 3 grammar. + grammars = [] + if supports_feature(target_versions, Feature.PATTERN_MATCHING): + # Python 3.10+ + grammars.append(pygram.python_grammar_soft_keywords) + # If we have to parse both, try to parse async as a keyword first + if not supports_feature( + target_versions, Feature.ASYNC_IDENTIFIERS + ) and not supports_feature(target_versions, Feature.PATTERN_MATCHING): + # Python 3.7-3.9 + grammars.append( + pygram.python_grammar_no_print_statement_no_exec_statement_async_keywords + ) + if not supports_feature(target_versions, Feature.ASYNC_KEYWORDS): + # Python 3.0-3.6 + grammars.append(pygram.python_grammar_no_print_statement_no_exec_statement) + + def test_patma_155(self): + x = 0 + y = None + match x: + case 1e1000: + y = 0 + self.assertEqual(x, 0) + self.assertIs(y, None) + + x = range(3) + match x: + case [y, case as x, z]: + w = 0 + + # At least one of the above branches must have been taken, because every Python + # version has exactly one of the two 'ASYNC_*' flags + return grammars + + +def lib2to3_parse(src_txt: str, target_versions: Iterable[TargetVersion] = ()) -> Node: + """Given a string with source, return the lib2to3 Node.""" + if not src_txt.endswith("\n"): + src_txt += "\n" + + grammars = get_grammars(set(target_versions)) + + +re.match() +match = a +with match() as match: + match = f"{match}" + +re.match() +match = a +with match() as match: + match = f"{match}" +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,12 +1,12 @@ + re.match() + match = a + with match() as match: +- match = f"{match}" ++ match = f"NOT_YET_IMPLEMENTED_ExprJoinedStr" + + re.match() + match = a + with match() as match: +- match = f"{match}" ++ match = f"NOT_YET_IMPLEMENTED_ExprJoinedStr" + + + def get_grammars(target_versions: Set[TargetVersion]) -> List[Grammar]: +@@ -23,13 +23,11 @@ + pygram.python_grammar, + ] + +- match match: +- case case: +- match match: +- case case: +- pass ++ NOT_YET_IMPLEMENTED_StmtMatch + +- if all(version.is_python2() for version in target_versions): ++ if all( ++ (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) ++ ): + # Python 2-only code, so try Python 2 grammars. + return [ + # Python 2.7 with future print_function import +@@ -41,13 +39,11 @@ + re.match() + match = a + with match() as match: +- match = f"{match}" ++ match = f"NOT_YET_IMPLEMENTED_ExprJoinedStr" + + def test_patma_139(self): + x = False +- match x: +- case bool(z): +- y = 0 ++ NOT_YET_IMPLEMENTED_StmtMatch + self.assertIs(x, False) + self.assertEqual(y, 0) + self.assertIs(z, x) +@@ -72,16 +68,12 @@ + def test_patma_155(self): + x = 0 + y = None +- match x: +- case 1e1000: +- y = 0 ++ NOT_YET_IMPLEMENTED_StmtMatch + self.assertEqual(x, 0) + self.assertIs(y, None) + + x = range(3) +- match x: +- case [y, case as x, z]: +- w = 0 ++ NOT_YET_IMPLEMENTED_StmtMatch + + # At least one of the above branches must have been taken, because every Python + # version has exactly one of the two 'ASYNC_*' flags +@@ -99,9 +91,9 @@ + re.match() + match = a + with match() as match: +- match = f"{match}" ++ match = f"NOT_YET_IMPLEMENTED_ExprJoinedStr" + + re.match() + match = a + with match() as match: +- match = f"{match}" ++ match = f"NOT_YET_IMPLEMENTED_ExprJoinedStr" +``` + +## Ruff Output + +```py +re.match() +match = a +with match() as match: + match = f"NOT_YET_IMPLEMENTED_ExprJoinedStr" + +re.match() +match = a +with match() as match: + match = f"NOT_YET_IMPLEMENTED_ExprJoinedStr" + + +def get_grammars(target_versions: Set[TargetVersion]) -> List[Grammar]: + if not target_versions: + # No target_version specified, so try all grammars. + return [ + # Python 3.7+ + pygram.python_grammar_no_print_statement_no_exec_statement_async_keywords, + # Python 3.0-3.6 + pygram.python_grammar_no_print_statement_no_exec_statement, + # Python 2.7 with future print_function import + pygram.python_grammar_no_print_statement, + # Python 2.7 + pygram.python_grammar, + ] + + NOT_YET_IMPLEMENTED_StmtMatch + + if all( + (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) + ): + # Python 2-only code, so try Python 2 grammars. + return [ + # Python 2.7 with future print_function import + pygram.python_grammar_no_print_statement, + # Python 2.7 + pygram.python_grammar, + ] + + re.match() + match = a + with match() as match: + match = f"NOT_YET_IMPLEMENTED_ExprJoinedStr" + + def test_patma_139(self): + x = False + NOT_YET_IMPLEMENTED_StmtMatch + self.assertIs(x, False) + self.assertEqual(y, 0) + self.assertIs(z, x) + + # Python 3-compatible code, so only try Python 3 grammar. + grammars = [] + if supports_feature(target_versions, Feature.PATTERN_MATCHING): + # Python 3.10+ + grammars.append(pygram.python_grammar_soft_keywords) + # If we have to parse both, try to parse async as a keyword first + if not supports_feature( + target_versions, Feature.ASYNC_IDENTIFIERS + ) and not supports_feature(target_versions, Feature.PATTERN_MATCHING): + # Python 3.7-3.9 + grammars.append( + pygram.python_grammar_no_print_statement_no_exec_statement_async_keywords + ) + if not supports_feature(target_versions, Feature.ASYNC_KEYWORDS): + # Python 3.0-3.6 + grammars.append(pygram.python_grammar_no_print_statement_no_exec_statement) + + def test_patma_155(self): + x = 0 + y = None + NOT_YET_IMPLEMENTED_StmtMatch + self.assertEqual(x, 0) + self.assertIs(y, None) + + x = range(3) + NOT_YET_IMPLEMENTED_StmtMatch + + # At least one of the above branches must have been taken, because every Python + # version has exactly one of the two 'ASYNC_*' flags + return grammars + + +def lib2to3_parse(src_txt: str, target_versions: Iterable[TargetVersion] = ()) -> Node: + """Given a string with source, return the lib2to3 Node.""" + if not src_txt.endswith("\n"): + src_txt += "\n" + + grammars = get_grammars(set(target_versions)) + + +re.match() +match = a +with match() as match: + match = f"NOT_YET_IMPLEMENTED_ExprJoinedStr" + +re.match() +match = a +with match() as match: + match = f"NOT_YET_IMPLEMENTED_ExprJoinedStr" +``` + +## Black Output + +```py +re.match() +match = a +with match() as match: + match = f"{match}" + +re.match() +match = a +with match() as match: + match = f"{match}" + + +def get_grammars(target_versions: Set[TargetVersion]) -> List[Grammar]: + if not target_versions: + # No target_version specified, so try all grammars. + return [ + # Python 3.7+ + pygram.python_grammar_no_print_statement_no_exec_statement_async_keywords, + # Python 3.0-3.6 + pygram.python_grammar_no_print_statement_no_exec_statement, + # Python 2.7 with future print_function import + pygram.python_grammar_no_print_statement, + # Python 2.7 + pygram.python_grammar, + ] + + match match: + case case: + match match: + case case: + pass + + if all(version.is_python2() for version in target_versions): + # Python 2-only code, so try Python 2 grammars. + return [ + # Python 2.7 with future print_function import + pygram.python_grammar_no_print_statement, + # Python 2.7 + pygram.python_grammar, + ] + + re.match() + match = a + with match() as match: + match = f"{match}" + + def test_patma_139(self): + x = False + match x: + case bool(z): + y = 0 + self.assertIs(x, False) + self.assertEqual(y, 0) + self.assertIs(z, x) + + # Python 3-compatible code, so only try Python 3 grammar. + grammars = [] + if supports_feature(target_versions, Feature.PATTERN_MATCHING): + # Python 3.10+ + grammars.append(pygram.python_grammar_soft_keywords) + # If we have to parse both, try to parse async as a keyword first + if not supports_feature( + target_versions, Feature.ASYNC_IDENTIFIERS + ) and not supports_feature(target_versions, Feature.PATTERN_MATCHING): + # Python 3.7-3.9 + grammars.append( + pygram.python_grammar_no_print_statement_no_exec_statement_async_keywords + ) + if not supports_feature(target_versions, Feature.ASYNC_KEYWORDS): + # Python 3.0-3.6 + grammars.append(pygram.python_grammar_no_print_statement_no_exec_statement) + + def test_patma_155(self): + x = 0 + y = None + match x: + case 1e1000: + y = 0 + self.assertEqual(x, 0) + self.assertIs(y, None) + + x = range(3) + match x: + case [y, case as x, z]: + w = 0 + + # At least one of the above branches must have been taken, because every Python + # version has exactly one of the two 'ASYNC_*' flags + return grammars + + +def lib2to3_parse(src_txt: str, target_versions: Iterable[TargetVersion] = ()) -> Node: + """Given a string with source, return the lib2to3 Node.""" + if not src_txt.endswith("\n"): + src_txt += "\n" + + grammars = get_grammars(set(target_versions)) + + +re.match() +match = a +with match() as match: + match = f"{match}" + +re.match() +match = a +with match() as match: + match = f"{match}" +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_simple.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_simple.py.snap new file mode 100644 index 0000000000..a165da5f84 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_simple.py.snap @@ -0,0 +1,343 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_simple.py +--- +## Input + +```py +# Cases sampled from PEP 636 examples + +match command.split(): + case [action, obj]: + ... # interpret action, obj + +match command.split(): + case [action]: + ... # interpret single-verb action + case [action, obj]: + ... # interpret action, obj + +match command.split(): + case ["quit"]: + print("Goodbye!") + quit_game() + case ["look"]: + current_room.describe() + case ["get", obj]: + character.get(obj, current_room) + case ["go", direction]: + current_room = current_room.neighbor(direction) + # The rest of your commands go here + +match command.split(): + case ["drop", *objects]: + for obj in objects: + character.drop(obj, current_room) + # The rest of your commands go here + +match command.split(): + case ["quit"]: + pass + case ["go", direction]: + print("Going:", direction) + case ["drop", *objects]: + print("Dropping: ", *objects) + case _: + print(f"Sorry, I couldn't understand {command!r}") + +match command.split(): + case ["north"] | ["go", "north"]: + current_room = current_room.neighbor("north") + case ["get", obj] | ["pick", "up", obj] | ["pick", obj, "up"]: + ... # Code for picking up the given object + +match command.split(): + case ["go", ("north" | "south" | "east" | "west")]: + current_room = current_room.neighbor(...) + # how do I know which direction to go? + +match command.split(): + case ["go", ("north" | "south" | "east" | "west") as direction]: + current_room = current_room.neighbor(direction) + +match command.split(): + case ["go", direction] if direction in current_room.exits: + current_room = current_room.neighbor(direction) + case ["go", _]: + print("Sorry, you can't go that way") + +match event.get(): + case Click(position=(x, y)): + handle_click_at(x, y) + case KeyPress(key_name="Q") | Quit(): + game.quit() + case KeyPress(key_name="up arrow"): + game.go_north() + case KeyPress(): + pass # Ignore other keystrokes + case other_event: + raise ValueError(f"Unrecognized event: {other_event}") + +match event.get(): + case Click((x, y), button=Button.LEFT): # This is a left click + handle_click_at(x, y) + case Click(): + pass # ignore other clicks + + +def where_is(point): + match point: + case Point(x=0, y=0): + print("Origin") + case Point(x=0, y=y): + print(f"Y={y}") + case Point(x=x, y=0): + print(f"X={x}") + case Point(): + print("Somewhere else") + case _: + print("Not a point") +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,92 +1,27 @@ + # Cases sampled from PEP 636 examples + +-match command.split(): +- case [action, obj]: +- ... # interpret action, obj ++NOT_YET_IMPLEMENTED_StmtMatch + +-match command.split(): +- case [action]: +- ... # interpret single-verb action +- case [action, obj]: +- ... # interpret action, obj ++NOT_YET_IMPLEMENTED_StmtMatch + +-match command.split(): +- case ["quit"]: +- print("Goodbye!") +- quit_game() +- case ["look"]: +- current_room.describe() +- case ["get", obj]: +- character.get(obj, current_room) +- case ["go", direction]: +- current_room = current_room.neighbor(direction) +- # The rest of your commands go here ++NOT_YET_IMPLEMENTED_StmtMatch + +-match command.split(): +- case ["drop", *objects]: +- for obj in objects: +- character.drop(obj, current_room) +- # The rest of your commands go here ++NOT_YET_IMPLEMENTED_StmtMatch + +-match command.split(): +- case ["quit"]: +- pass +- case ["go", direction]: +- print("Going:", direction) +- case ["drop", *objects]: +- print("Dropping: ", *objects) +- case _: +- print(f"Sorry, I couldn't understand {command!r}") ++NOT_YET_IMPLEMENTED_StmtMatch + +-match command.split(): +- case ["north"] | ["go", "north"]: +- current_room = current_room.neighbor("north") +- case ["get", obj] | ["pick", "up", obj] | ["pick", obj, "up"]: +- ... # Code for picking up the given object ++NOT_YET_IMPLEMENTED_StmtMatch + +-match command.split(): +- case ["go", ("north" | "south" | "east" | "west")]: +- current_room = current_room.neighbor(...) +- # how do I know which direction to go? ++NOT_YET_IMPLEMENTED_StmtMatch + +-match command.split(): +- case ["go", ("north" | "south" | "east" | "west") as direction]: +- current_room = current_room.neighbor(direction) ++NOT_YET_IMPLEMENTED_StmtMatch + +-match command.split(): +- case ["go", direction] if direction in current_room.exits: +- current_room = current_room.neighbor(direction) +- case ["go", _]: +- print("Sorry, you can't go that way") ++NOT_YET_IMPLEMENTED_StmtMatch + +-match event.get(): +- case Click(position=(x, y)): +- handle_click_at(x, y) +- case KeyPress(key_name="Q") | Quit(): +- game.quit() +- case KeyPress(key_name="up arrow"): +- game.go_north() +- case KeyPress(): +- pass # Ignore other keystrokes +- case other_event: +- raise ValueError(f"Unrecognized event: {other_event}") ++NOT_YET_IMPLEMENTED_StmtMatch + +-match event.get(): +- case Click((x, y), button=Button.LEFT): # This is a left click +- handle_click_at(x, y) +- case Click(): +- pass # ignore other clicks ++NOT_YET_IMPLEMENTED_StmtMatch + + + def where_is(point): +- match point: +- case Point(x=0, y=0): +- print("Origin") +- case Point(x=0, y=y): +- print(f"Y={y}") +- case Point(x=x, y=0): +- print(f"X={x}") +- case Point(): +- print("Somewhere else") +- case _: +- print("Not a point") ++ NOT_YET_IMPLEMENTED_StmtMatch +``` + +## Ruff Output + +```py +# Cases sampled from PEP 636 examples + +NOT_YET_IMPLEMENTED_StmtMatch + +NOT_YET_IMPLEMENTED_StmtMatch + +NOT_YET_IMPLEMENTED_StmtMatch + +NOT_YET_IMPLEMENTED_StmtMatch + +NOT_YET_IMPLEMENTED_StmtMatch + +NOT_YET_IMPLEMENTED_StmtMatch + +NOT_YET_IMPLEMENTED_StmtMatch + +NOT_YET_IMPLEMENTED_StmtMatch + +NOT_YET_IMPLEMENTED_StmtMatch + +NOT_YET_IMPLEMENTED_StmtMatch + +NOT_YET_IMPLEMENTED_StmtMatch + + +def where_is(point): + NOT_YET_IMPLEMENTED_StmtMatch +``` + +## Black Output + +```py +# Cases sampled from PEP 636 examples + +match command.split(): + case [action, obj]: + ... # interpret action, obj + +match command.split(): + case [action]: + ... # interpret single-verb action + case [action, obj]: + ... # interpret action, obj + +match command.split(): + case ["quit"]: + print("Goodbye!") + quit_game() + case ["look"]: + current_room.describe() + case ["get", obj]: + character.get(obj, current_room) + case ["go", direction]: + current_room = current_room.neighbor(direction) + # The rest of your commands go here + +match command.split(): + case ["drop", *objects]: + for obj in objects: + character.drop(obj, current_room) + # The rest of your commands go here + +match command.split(): + case ["quit"]: + pass + case ["go", direction]: + print("Going:", direction) + case ["drop", *objects]: + print("Dropping: ", *objects) + case _: + print(f"Sorry, I couldn't understand {command!r}") + +match command.split(): + case ["north"] | ["go", "north"]: + current_room = current_room.neighbor("north") + case ["get", obj] | ["pick", "up", obj] | ["pick", obj, "up"]: + ... # Code for picking up the given object + +match command.split(): + case ["go", ("north" | "south" | "east" | "west")]: + current_room = current_room.neighbor(...) + # how do I know which direction to go? + +match command.split(): + case ["go", ("north" | "south" | "east" | "west") as direction]: + current_room = current_room.neighbor(direction) + +match command.split(): + case ["go", direction] if direction in current_room.exits: + current_room = current_room.neighbor(direction) + case ["go", _]: + print("Sorry, you can't go that way") + +match event.get(): + case Click(position=(x, y)): + handle_click_at(x, y) + case KeyPress(key_name="Q") | Quit(): + game.quit() + case KeyPress(key_name="up arrow"): + game.go_north() + case KeyPress(): + pass # Ignore other keystrokes + case other_event: + raise ValueError(f"Unrecognized event: {other_event}") + +match event.get(): + case Click((x, y), button=Button.LEFT): # This is a left click + handle_click_at(x, y) + case Click(): + pass # ignore other clicks + + +def where_is(point): + match point: + case Point(x=0, y=0): + print("Origin") + case Point(x=0, y=y): + print(f"Y={y}") + case Point(x=x, y=0): + print(f"X={x}") + case Point(): + print("Somewhere else") + case _: + print("Not a point") +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_style.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_style.py.snap new file mode 100644 index 0000000000..56fe93fb72 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_style.py.snap @@ -0,0 +1,186 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pattern_matching_style.py +--- +## Input + +```py +match something: + case b(): print(1+1) + case c( + very_complex=True, + perhaps_even_loooooooooooooooooooooooooooooooooooooong=- 1 + ): print(1) + case c( + very_complex=True, + perhaps_even_loooooooooooooooooooooooooooooooooooooong=-1, + ): print(2) + case a: pass + +match( + arg # comment +) + +match( +) + +match( + + +) + +case( + arg # comment +) + +case( +) + +case( + + +) + + +re.match( + something # fast +) +re.match( + + + +) +match match( + + +): + case case( + arg, # comment + ): + pass +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,35 +1,24 @@ +-match something: +- case b(): +- print(1 + 1) +- case c( +- very_complex=True, perhaps_even_loooooooooooooooooooooooooooooooooooooong=-1 +- ): +- print(1) +- case c( +- very_complex=True, +- perhaps_even_loooooooooooooooooooooooooooooooooooooong=-1, +- ): +- print(2) +- case a: +- pass ++NOT_YET_IMPLEMENTED_StmtMatch + +-match(arg) # comment ++match( ++ arg # comment ++) + + match() + + match() + +-case(arg) # comment ++case( ++ arg # comment ++) + + case() + + case() + + +-re.match(something) # fast ++re.match( ++ something # fast ++) + re.match() +-match match(): +- case case( +- arg, # comment +- ): +- pass ++NOT_YET_IMPLEMENTED_StmtMatch +``` + +## Ruff Output + +```py +NOT_YET_IMPLEMENTED_StmtMatch + +match( + arg # comment +) + +match() + +match() + +case( + arg # comment +) + +case() + +case() + + +re.match( + something # fast +) +re.match() +NOT_YET_IMPLEMENTED_StmtMatch +``` + +## Black Output + +```py +match something: + case b(): + print(1 + 1) + case c( + very_complex=True, perhaps_even_loooooooooooooooooooooooooooooooooooooong=-1 + ): + print(1) + case c( + very_complex=True, + perhaps_even_loooooooooooooooooooooooooooooooooooooong=-1, + ): + print(2) + case a: + pass + +match(arg) # comment + +match() + +match() + +case(arg) # comment + +case() + +case() + + +re.match(something) # fast +re.match() +match match(): + case case( + arg, # comment + ): + pass +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pep_572_py310.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pep_572_py310.py.snap new file mode 100644 index 0000000000..1204c05491 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pep_572_py310.py.snap @@ -0,0 +1,106 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_310/pep_572_py310.py +--- +## Input + +```py +# Unparenthesized walruses are now allowed in indices since Python 3.10. +x[a:=0] +x[a:=0, b:=1] +x[5, b:=0] + +# Walruses are allowed inside generator expressions on function calls since 3.10. +if any(match := pattern_error.match(s) for s in buffer): + if match.group(2) == data_not_available: + # Error OK to ignore. + pass + +f(a := b + c for c in range(10)) +f((a := b + c for c in range(10)), x) +f(y=(a := b + c for c in range(10))) +f(x, (a := b + c for c in range(10)), y=z, **q) +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,15 +1,20 @@ + # Unparenthesized walruses are now allowed in indices since Python 3.10. +-x[a:=0] +-x[a:=0, b:=1] +-x[5, b:=0] ++x[a := 0] ++x[a := 0, b := 1] ++x[5, b := 0] + + # Walruses are allowed inside generator expressions on function calls since 3.10. +-if any(match := pattern_error.match(s) for s in buffer): ++if any((NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in [])): + if match.group(2) == data_not_available: + # Error OK to ignore. + pass + +-f(a := b + c for c in range(10)) +-f((a := b + c for c in range(10)), x) +-f(y=(a := b + c for c in range(10))) +-f(x, (a := b + c for c in range(10)), y=z, **q) ++f((NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in [])) ++f((NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []), x) ++f(y=(NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in [])) ++f( ++ x, ++ (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []), ++ y=z, ++ **q, ++) +``` + +## Ruff Output + +```py +# Unparenthesized walruses are now allowed in indices since Python 3.10. +x[a := 0] +x[a := 0, b := 1] +x[5, b := 0] + +# Walruses are allowed inside generator expressions on function calls since 3.10. +if any((NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in [])): + if match.group(2) == data_not_available: + # Error OK to ignore. + pass + +f((NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in [])) +f((NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []), x) +f(y=(NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in [])) +f( + x, + (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []), + y=z, + **q, +) +``` + +## Black Output + +```py +# Unparenthesized walruses are now allowed in indices since Python 3.10. +x[a:=0] +x[a:=0, b:=1] +x[5, b:=0] + +# Walruses are allowed inside generator expressions on function calls since 3.10. +if any(match := pattern_error.match(s) for s in buffer): + if match.group(2) == data_not_available: + # Error OK to ignore. + pass + +f(a := b + c for c in range(10)) +f((a := b + c for c in range(10)), x) +f(y=(a := b + c for c in range(10))) +f(x, (a := b + c for c in range(10)), y=z, **q) +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__remove_newline_after_match.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__remove_newline_after_match.py.snap new file mode 100644 index 0000000000..6099825e88 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__remove_newline_after_match.py.snap @@ -0,0 +1,76 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_310/remove_newline_after_match.py +--- +## Input + +```py +def http_status(status): + + match status: + + case 400: + + return "Bad request" + + case 401: + + return "Unauthorized" + + case 403: + + return "Forbidden" + + case 404: + + return "Not found" +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,13 +1,2 @@ + def http_status(status): +- match status: +- case 400: +- return "Bad request" +- +- case 401: +- return "Unauthorized" +- +- case 403: +- return "Forbidden" +- +- case 404: +- return "Not found" ++ NOT_YET_IMPLEMENTED_StmtMatch +``` + +## Ruff Output + +```py +def http_status(status): + NOT_YET_IMPLEMENTED_StmtMatch +``` + +## Black Output + +```py +def http_status(status): + match status: + case 400: + return "Bad request" + + case 401: + return "Unauthorized" + + case 403: + return "Forbidden" + + case 404: + return "Not found" +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__starred_for_target.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__starred_for_target.py.snap new file mode 100644 index 0000000000..dad8c213dc --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__starred_for_target.py.snap @@ -0,0 +1,123 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_310/starred_for_target.py +--- +## Input + +```py +for x in *a, *b: + print(x) + +for x in a, b, *c: + print(x) + +for x in *a, b, c: + print(x) + +for x in *a, b, *c: + print(x) + +async for x in *a, *b: + print(x) + +async for x in *a, b, *c: + print(x) + +async for x in a, b, *c: + print(x) + +async for x in ( + *loooooooooooooooooooooong, + very, + *loooooooooooooooooooooooooooooooooooooooooooooooong, +): + print(x) +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -10,18 +10,10 @@ + for x in *a, b, *c: + print(x) + +-async for x in *a, *b: +- print(x) ++NOT_YET_IMPLEMENTED_StmtAsyncFor + +-async for x in *a, b, *c: +- print(x) ++NOT_YET_IMPLEMENTED_StmtAsyncFor + +-async for x in a, b, *c: +- print(x) ++NOT_YET_IMPLEMENTED_StmtAsyncFor + +-async for x in ( +- *loooooooooooooooooooooong, +- very, +- *loooooooooooooooooooooooooooooooooooooooooooooooong, +-): +- print(x) ++NOT_YET_IMPLEMENTED_StmtAsyncFor +``` + +## Ruff Output + +```py +for x in *a, *b: + print(x) + +for x in a, b, *c: + print(x) + +for x in *a, b, c: + print(x) + +for x in *a, b, *c: + print(x) + +NOT_YET_IMPLEMENTED_StmtAsyncFor + +NOT_YET_IMPLEMENTED_StmtAsyncFor + +NOT_YET_IMPLEMENTED_StmtAsyncFor + +NOT_YET_IMPLEMENTED_StmtAsyncFor +``` + +## Black Output + +```py +for x in *a, *b: + print(x) + +for x in a, b, *c: + print(x) + +for x in *a, b, c: + print(x) + +for x in *a, b, *c: + print(x) + +async for x in *a, *b: + print(x) + +async for x in *a, b, *c: + print(x) + +async for x in a, b, *c: + print(x) + +async for x in ( + *loooooooooooooooooooooong, + very, + *loooooooooooooooooooooooooooooooooooooooooooooooong, +): + print(x) +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_36__numeric_literals.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_36__numeric_literals.py.snap new file mode 100644 index 0000000000..4135458654 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_36__numeric_literals.py.snap @@ -0,0 +1,118 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_36/numeric_literals.py +--- +## Input + +```py +#!/usr/bin/env python3.6 + +x = 123456789 +x = 123456 +x = .1 +x = 1. +x = 1E+1 +x = 1E-1 +x = 1.000_000_01 +x = 123456789.123456789 +x = 123456789.123456789E123456789 +x = 123456789E123456789 +x = 123456789J +x = 123456789.123456789J +x = 0XB1ACC +x = 0B1011 +x = 0O777 +x = 0.000000006 +x = 10000 +x = 133333 +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -2,19 +2,19 @@ + + x = 123456789 + x = 123456 +-x = 0.1 +-x = 1.0 +-x = 1e1 +-x = 1e-1 ++x = .1 ++x = 1. ++x = 1E+1 ++x = 1E-1 + x = 1.000_000_01 + x = 123456789.123456789 +-x = 123456789.123456789e123456789 +-x = 123456789e123456789 +-x = 123456789j +-x = 123456789.123456789j +-x = 0xB1ACC +-x = 0b1011 +-x = 0o777 ++x = 123456789.123456789E123456789 ++x = 123456789E123456789 ++x = 123456789J ++x = 123456789.123456789J ++x = 0XB1ACC ++x = 0B1011 ++x = 0O777 + x = 0.000000006 + x = 10000 + x = 133333 +``` + +## Ruff Output + +```py +#!/usr/bin/env python3.6 + +x = 123456789 +x = 123456 +x = .1 +x = 1. +x = 1E+1 +x = 1E-1 +x = 1.000_000_01 +x = 123456789.123456789 +x = 123456789.123456789E123456789 +x = 123456789E123456789 +x = 123456789J +x = 123456789.123456789J +x = 0XB1ACC +x = 0B1011 +x = 0O777 +x = 0.000000006 +x = 10000 +x = 133333 +``` + +## Black Output + +```py +#!/usr/bin/env python3.6 + +x = 123456789 +x = 123456 +x = 0.1 +x = 1.0 +x = 1e1 +x = 1e-1 +x = 1.000_000_01 +x = 123456789.123456789 +x = 123456789.123456789e123456789 +x = 123456789e123456789 +x = 123456789j +x = 123456789.123456789j +x = 0xB1ACC +x = 0b1011 +x = 0o777 +x = 0.000000006 +x = 10000 +x = 133333 +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_36__numeric_literals_skip_underscores.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_36__numeric_literals_skip_underscores.py.snap new file mode 100644 index 0000000000..69d74787f3 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_36__numeric_literals_skip_underscores.py.snap @@ -0,0 +1,72 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_36/numeric_literals_skip_underscores.py +--- +## Input + +```py +#!/usr/bin/env python3.6 + +x = 123456789 +x = 1_2_3_4_5_6_7 +x = 1E+1 +x = 0xb1acc +x = 0.00_00_006 +x = 12_34_567J +x = .1_2 +x = 1_2. +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -2,9 +2,9 @@ + + x = 123456789 + x = 1_2_3_4_5_6_7 +-x = 1e1 +-x = 0xB1ACC ++x = 1E+1 ++x = 0xb1acc + x = 0.00_00_006 +-x = 12_34_567j +-x = 0.1_2 +-x = 1_2.0 ++x = 12_34_567J ++x = .1_2 ++x = 1_2. +``` + +## Ruff Output + +```py +#!/usr/bin/env python3.6 + +x = 123456789 +x = 1_2_3_4_5_6_7 +x = 1E+1 +x = 0xb1acc +x = 0.00_00_006 +x = 12_34_567J +x = .1_2 +x = 1_2. +``` + +## Black Output + +```py +#!/usr/bin/env python3.6 + +x = 123456789 +x = 1_2_3_4_5_6_7 +x = 1e1 +x = 0xB1ACC +x = 0.00_00_006 +x = 12_34_567j +x = 0.1_2 +x = 1_2.0 +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_37__python37.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_37__python37.py.snap new file mode 100644 index 0000000000..4a12cae071 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_37__python37.py.snap @@ -0,0 +1,142 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_37/python37.py +--- +## Input + +```py +#!/usr/bin/env python3.7 + + +def f(): + return (i * 2 async for i in arange(42)) + + +def g(): + return ( + something_long * something_long + async for something_long in async_generator(with_an_argument) + ) + + +async def func(): + if test: + out_batched = [ + i + async for i in aitertools._async_map( + self.async_inc, arange(8), batch_size=3 + ) + ] + + +def awaited_generator_value(n): + return (await awaitable for awaitable in awaitable_list) + + +def make_arange(n): + return (i * 2 for i in range(n) if await wrap(i)) +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -2,14 +2,11 @@ + + + def f(): +- return (i * 2 async for i in arange(42)) ++ return (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) + + + def g(): +- return ( +- something_long * something_long +- async for something_long in async_generator(with_an_argument) +- ) ++ return (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) + + + async def func(): +@@ -23,8 +20,8 @@ + + + def awaited_generator_value(n): +- return (await awaitable for awaitable in awaitable_list) ++ return (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) + + + def make_arange(n): +- return (i * 2 for i in range(n) if await wrap(i)) ++ return (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) +``` + +## Ruff Output + +```py +#!/usr/bin/env python3.7 + + +def f(): + return (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) + + +def g(): + return (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) + + +async def func(): + if test: + out_batched = [ + i + async for i in aitertools._async_map( + self.async_inc, arange(8), batch_size=3 + ) + ] + + +def awaited_generator_value(n): + return (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) + + +def make_arange(n): + return (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) +``` + +## Black Output + +```py +#!/usr/bin/env python3.7 + + +def f(): + return (i * 2 async for i in arange(42)) + + +def g(): + return ( + something_long * something_long + async for something_long in async_generator(with_an_argument) + ) + + +async def func(): + if test: + out_batched = [ + i + async for i in aitertools._async_map( + self.async_inc, arange(8), batch_size=3 + ) + ] + + +def awaited_generator_value(n): + return (await awaitable for awaitable in awaitable_list) + + +def make_arange(n): + return (i * 2 for i in range(n) if await wrap(i)) +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__pep_570.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__pep_570.py.snap new file mode 100644 index 0000000000..984c4bb1f9 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__pep_570.py.snap @@ -0,0 +1,174 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_38/pep_570.py +--- +## Input + +```py +def positional_only_arg(a, /): + pass + + +def all_markers(a, b, /, c, d, *, e, f): + pass + + +def all_markers_with_args_and_kwargs( + a_long_one, + b_long_one, + /, + c_long_one, + d_long_one, + *args, + e_long_one, + f_long_one, + **kwargs, +): + pass + + +def all_markers_with_defaults(a, b=1, /, c=2, d=3, *, e=4, f=5): + pass + + +def long_one_with_long_parameter_names( + but_all_of_them, + are_positional_only, + arguments_mmmmkay, + so_this_is_only_valid_after, + three_point_eight, + /, +): + pass + + +lambda a, /: a + +lambda a, b, /, c, d, *, e, f: a + +lambda a, b, /, c, d, *args, e, f, **kwargs: args + +lambda a, b=1, /, c=2, d=3, *, e=4, f=5: 1 +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -35,10 +35,10 @@ + pass + + +-lambda a, /: a ++lambda NOT_YET_IMPLEMENTED_lambda: True + +-lambda a, b, /, c, d, *, e, f: a ++lambda NOT_YET_IMPLEMENTED_lambda: True + +-lambda a, b, /, c, d, *args, e, f, **kwargs: args ++lambda NOT_YET_IMPLEMENTED_lambda: True + +-lambda a, b=1, /, c=2, d=3, *, e=4, f=5: 1 ++lambda NOT_YET_IMPLEMENTED_lambda: True +``` + +## Ruff Output + +```py +def positional_only_arg(a, /): + pass + + +def all_markers(a, b, /, c, d, *, e, f): + pass + + +def all_markers_with_args_and_kwargs( + a_long_one, + b_long_one, + /, + c_long_one, + d_long_one, + *args, + e_long_one, + f_long_one, + **kwargs, +): + pass + + +def all_markers_with_defaults(a, b=1, /, c=2, d=3, *, e=4, f=5): + pass + + +def long_one_with_long_parameter_names( + but_all_of_them, + are_positional_only, + arguments_mmmmkay, + so_this_is_only_valid_after, + three_point_eight, + /, +): + pass + + +lambda NOT_YET_IMPLEMENTED_lambda: True + +lambda NOT_YET_IMPLEMENTED_lambda: True + +lambda NOT_YET_IMPLEMENTED_lambda: True + +lambda NOT_YET_IMPLEMENTED_lambda: True +``` + +## Black Output + +```py +def positional_only_arg(a, /): + pass + + +def all_markers(a, b, /, c, d, *, e, f): + pass + + +def all_markers_with_args_and_kwargs( + a_long_one, + b_long_one, + /, + c_long_one, + d_long_one, + *args, + e_long_one, + f_long_one, + **kwargs, +): + pass + + +def all_markers_with_defaults(a, b=1, /, c=2, d=3, *, e=4, f=5): + pass + + +def long_one_with_long_parameter_names( + but_all_of_them, + are_positional_only, + arguments_mmmmkay, + so_this_is_only_valid_after, + three_point_eight, + /, +): + pass + + +lambda a, /: a + +lambda a, b, /, c, d, *, e, f: a + +lambda a, b, /, c, d, *args, e, f, **kwargs: args + +lambda a, b=1, /, c=2, d=3, *, e=4, f=5: 1 +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__pep_572.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__pep_572.py.snap new file mode 100644 index 0000000000..c411a16a6e --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__pep_572.py.snap @@ -0,0 +1,214 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_38/pep_572.py +--- +## Input + +```py +(a := 1) +(a := a) +if (match := pattern.search(data)) is None: + pass +if match := pattern.search(data): + pass +[y := f(x), y**2, y**3] +filtered_data = [y for x in data if (y := f(x)) is None] +(y := f(x)) +y0 = (y1 := f(x)) +foo(x=(y := f(x))) + + +def foo(answer=(p := 42)): + pass + + +def foo(answer: (p := 42) = 5): + pass + + +lambda: (x := 1) +(x := lambda: 1) +(x := lambda: (y := 1)) +lambda line: (m := re.match(pattern, line)) and m.group(1) +x = (y := 0) +(z := (y := (x := 0))) +(info := (name, phone, *rest)) +(x := 1, 2) +(total := total + tax) +len(lines := f.readlines()) +foo(x := 3, cat="vector") +foo(cat=(category := "vector")) +if any(len(longline := l) >= 100 for l in lines): + print(longline) +if env_base := os.environ.get("PYTHONUSERBASE", None): + return env_base +if self._is_special and (ans := self._check_nans(context=context)): + return ans +foo(b := 2, a=1) +foo((b := 2), a=1) +foo(c=(b := 2), a=1) + +while x := f(x): + pass +while x := f(x): + pass +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -2,7 +2,7 @@ + (a := a) + if (match := pattern.search(data)) is None: + pass +-if match := pattern.search(data): ++if (match := pattern.search(data)): + pass + [y := f(x), y**2, y**3] + filtered_data = [y for x in data if (y := f(x)) is None] +@@ -19,10 +19,10 @@ + pass + + +-lambda: (x := 1) +-(x := lambda: 1) +-(x := lambda: (y := 1)) +-lambda line: (m := re.match(pattern, line)) and m.group(1) ++lambda NOT_YET_IMPLEMENTED_lambda: True ++(x := lambda NOT_YET_IMPLEMENTED_lambda: True) ++(x := lambda NOT_YET_IMPLEMENTED_lambda: True) ++lambda NOT_YET_IMPLEMENTED_lambda: True + x = (y := 0) + (z := (y := (x := 0))) + (info := (name, phone, *rest)) +@@ -31,9 +31,9 @@ + len(lines := f.readlines()) + foo(x := 3, cat="vector") + foo(cat=(category := "vector")) +-if any(len(longline := l) >= 100 for l in lines): ++if any((NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in [])): + print(longline) +-if env_base := os.environ.get("PYTHONUSERBASE", None): ++if (env_base := os.environ.get("PYTHONUSERBASE", None)): + return env_base + if self._is_special and (ans := self._check_nans(context=context)): + return ans +@@ -41,7 +41,7 @@ + foo((b := 2), a=1) + foo(c=(b := 2), a=1) + +-while x := f(x): ++while (x := f(x)): + pass +-while x := f(x): ++while (x := f(x)): + pass +``` + +## Ruff Output + +```py +(a := 1) +(a := a) +if (match := pattern.search(data)) is None: + pass +if (match := pattern.search(data)): + pass +[y := f(x), y**2, y**3] +filtered_data = [y for x in data if (y := f(x)) is None] +(y := f(x)) +y0 = (y1 := f(x)) +foo(x=(y := f(x))) + + +def foo(answer=(p := 42)): + pass + + +def foo(answer: (p := 42) = 5): + pass + + +lambda NOT_YET_IMPLEMENTED_lambda: True +(x := lambda NOT_YET_IMPLEMENTED_lambda: True) +(x := lambda NOT_YET_IMPLEMENTED_lambda: True) +lambda NOT_YET_IMPLEMENTED_lambda: True +x = (y := 0) +(z := (y := (x := 0))) +(info := (name, phone, *rest)) +(x := 1, 2) +(total := total + tax) +len(lines := f.readlines()) +foo(x := 3, cat="vector") +foo(cat=(category := "vector")) +if any((NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in [])): + print(longline) +if (env_base := os.environ.get("PYTHONUSERBASE", None)): + return env_base +if self._is_special and (ans := self._check_nans(context=context)): + return ans +foo(b := 2, a=1) +foo((b := 2), a=1) +foo(c=(b := 2), a=1) + +while (x := f(x)): + pass +while (x := f(x)): + pass +``` + +## Black Output + +```py +(a := 1) +(a := a) +if (match := pattern.search(data)) is None: + pass +if match := pattern.search(data): + pass +[y := f(x), y**2, y**3] +filtered_data = [y for x in data if (y := f(x)) is None] +(y := f(x)) +y0 = (y1 := f(x)) +foo(x=(y := f(x))) + + +def foo(answer=(p := 42)): + pass + + +def foo(answer: (p := 42) = 5): + pass + + +lambda: (x := 1) +(x := lambda: 1) +(x := lambda: (y := 1)) +lambda line: (m := re.match(pattern, line)) and m.group(1) +x = (y := 0) +(z := (y := (x := 0))) +(info := (name, phone, *rest)) +(x := 1, 2) +(total := total + tax) +len(lines := f.readlines()) +foo(x := 3, cat="vector") +foo(cat=(category := "vector")) +if any(len(longline := l) >= 100 for l in lines): + print(longline) +if env_base := os.environ.get("PYTHONUSERBASE", None): + return env_base +if self._is_special and (ans := self._check_nans(context=context)): + return ans +foo(b := 2, a=1) +foo((b := 2), a=1) +foo(c=(b := 2), a=1) + +while x := f(x): + pass +while x := f(x): + pass +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__python38.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__python38.py.snap new file mode 100644 index 0000000000..f92c9acaf0 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__python38.py.snap @@ -0,0 +1,103 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_38/python38.py +--- +## Input + +```py +#!/usr/bin/env python3.8 + + +def starred_return(): + my_list = ["value2", "value3"] + return "value1", *my_list + + +def starred_yield(): + my_list = ["value2", "value3"] + yield "value1", *my_list + + +# all right hand side expressions allowed in regular assignments are now also allowed in +# annotated assignments +a : Tuple[ str, int] = "1", 2 +a: Tuple[int , ... ] = b, *c, d +def t(): + a : str = yield "a" +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -8,7 +8,7 @@ + + def starred_yield(): + my_list = ["value2", "value3"] +- yield "value1", *my_list ++ NOT_YET_IMPLEMENTED_ExprYield + + + # all right hand side expressions allowed in regular assignments are now also allowed in +@@ -18,4 +18,4 @@ + + + def t(): +- a: str = yield "a" ++ a: str = NOT_YET_IMPLEMENTED_ExprYield +``` + +## Ruff Output + +```py +#!/usr/bin/env python3.8 + + +def starred_return(): + my_list = ["value2", "value3"] + return "value1", *my_list + + +def starred_yield(): + my_list = ["value2", "value3"] + NOT_YET_IMPLEMENTED_ExprYield + + +# all right hand side expressions allowed in regular assignments are now also allowed in +# annotated assignments +a: Tuple[str, int] = "1", 2 +a: Tuple[int, ...] = b, *c, d + + +def t(): + a: str = NOT_YET_IMPLEMENTED_ExprYield +``` + +## Black Output + +```py +#!/usr/bin/env python3.8 + + +def starred_return(): + my_list = ["value2", "value3"] + return "value1", *my_list + + +def starred_yield(): + my_list = ["value2", "value3"] + yield "value1", *my_list + + +# all right hand side expressions allowed in regular assignments are now also allowed in +# annotated assignments +a: Tuple[str, int] = "1", 2 +a: Tuple[int, ...] = b, *c, d + + +def t(): + a: str = yield "a" +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_39__pep_572_py39.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_39__pep_572_py39.py.snap new file mode 100644 index 0000000000..4a2a8824aa --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_39__pep_572_py39.py.snap @@ -0,0 +1,58 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_39/pep_572_py39.py +--- +## Input + +```py +# Unparenthesized walruses are now allowed in set literals & set comprehensions +# since Python 3.9 +{x := 1, 2, 3} +{x4 := x**5 for x in range(7)} +# We better not remove the parentheses here (since it's a 3.10 feature) +x[(a := 1)] +x[(a := 1), (b := 3)] +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,7 +1,7 @@ + # Unparenthesized walruses are now allowed in set literals & set comprehensions + # since Python 3.9 + {x := 1, 2, 3} +-{x4 := x**5 for x in range(7)} ++{NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set} + # We better not remove the parentheses here (since it's a 3.10 feature) + x[(a := 1)] +-x[(a := 1), (b := 3)] ++x[((a := 1), (b := 3))] +``` + +## Ruff Output + +```py +# Unparenthesized walruses are now allowed in set literals & set comprehensions +# since Python 3.9 +{x := 1, 2, 3} +{NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set} +# We better not remove the parentheses here (since it's a 3.10 feature) +x[(a := 1)] +x[((a := 1), (b := 3))] +``` + +## Black Output + +```py +# Unparenthesized walruses are now allowed in set literals & set comprehensions +# since Python 3.9 +{x := 1, 2, 3} +{x4 := x**5 for x in range(7)} +# We better not remove the parentheses here (since it's a 3.10 feature) +x[(a := 1)] +x[(a := 1), (b := 3)] +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_39__python39.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_39__python39.py.snap new file mode 100644 index 0000000000..cef4b88f67 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_39__python39.py.snap @@ -0,0 +1,101 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_39/python39.py +--- +## Input + +```py +#!/usr/bin/env python3.9 + +@relaxed_decorator[0] +def f(): + ... + +@relaxed_decorator[extremely_long_name_that_definitely_will_not_fit_on_one_line_of_standard_length] +def f(): + ... + +@extremely_long_variable_name_that_doesnt_fit := complex.expression(with_long="arguments_value_that_wont_fit_at_the_end_of_the_line") +def f(): + ... +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,6 +1,5 @@ + #!/usr/bin/env python3.9 + +- + @relaxed_decorator[0] + def f(): + ... +@@ -13,8 +12,10 @@ + ... + + +-@extremely_long_variable_name_that_doesnt_fit := complex.expression( +- with_long="arguments_value_that_wont_fit_at_the_end_of_the_line" ++@( ++ extremely_long_variable_name_that_doesnt_fit := complex.expression( ++ with_long="arguments_value_that_wont_fit_at_the_end_of_the_line" ++ ) + ) + def f(): + ... +``` + +## Ruff Output + +```py +#!/usr/bin/env python3.9 + +@relaxed_decorator[0] +def f(): + ... + + +@relaxed_decorator[ + extremely_long_name_that_definitely_will_not_fit_on_one_line_of_standard_length +] +def f(): + ... + + +@( + extremely_long_variable_name_that_doesnt_fit := complex.expression( + with_long="arguments_value_that_wont_fit_at_the_end_of_the_line" + ) +) +def f(): + ... +``` + +## Black Output + +```py +#!/usr/bin/env python3.9 + + +@relaxed_decorator[0] +def f(): + ... + + +@relaxed_decorator[ + extremely_long_name_that_definitely_will_not_fit_on_one_line_of_standard_length +] +def f(): + ... + + +@extremely_long_variable_name_that_doesnt_fit := complex.expression( + with_long="arguments_value_that_wont_fit_at_the_end_of_the_line" +) +def f(): + ... +``` + + diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__attribute_access_on_number_literals_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__attribute_access_on_number_literals.py.snap similarity index 50% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__attribute_access_on_number_literals_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__attribute_access_on_number_literals.py.snap index 81c681bc72..86e302320a 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__attribute_access_on_number_literals_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__attribute_access_on_number_literals.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/attribute_access_on_number_literals.py --- ## Input @@ -35,14 +34,18 @@ y = 100(no) ```diff --- Black +++ Ruff -@@ -1,22 +1,22 @@ --x = (123456789).bit_count() --x = (123456).__abs__() +@@ -1,19 +1,19 @@ + x = (123456789).bit_count() + x = (123456).__abs__() -x = (0.1).is_integer() -x = (1.0).imag -x = (1e1).imag -x = (1e-1).real --x = (123456789.123456789).hex() ++x = (.1).is_integer() ++x = (1.).imag ++x = (1E+1).imag ++x = (1E-1).real + x = (123456789.123456789).hex() -x = (123456789.123456789e123456789).real -x = (123456789e123456789).conjugate() -x = 123456789j.real @@ -50,60 +53,46 @@ y = 100(no) -x = 0xB1ACC.conjugate() -x = 0b1011.conjugate() -x = 0o777.real --x = (0.000000006).hex() ++x = (123456789.123456789E123456789).real ++x = (123456789E123456789).conjugate() ++x = 123456789J.real ++x = 123456789.123456789J.__add__((0b1011).bit_length()) ++x = (0XB1ACC).conjugate() ++x = (0B1011).conjugate() ++x = (0O777).real + x = (0.000000006).hex() -x = -100.0000j -+x = NOT_IMPLEMENTED_call() -+x = NOT_IMPLEMENTED_call() -+x = NOT_IMPLEMENTED_call() -+x = 1. .NOT_IMPLEMENTED_attr -+x = 1E+1 .NOT_IMPLEMENTED_attr -+x = 1E-1 .NOT_IMPLEMENTED_attr -+x = NOT_IMPLEMENTED_call() -+x = 123456789.123456789E123456789 .NOT_IMPLEMENTED_attr -+x = NOT_IMPLEMENTED_call() -+x = 123456789J.NOT_IMPLEMENTED_attr -+x = NOT_IMPLEMENTED_call() -+x = NOT_IMPLEMENTED_call() -+x = NOT_IMPLEMENTED_call() -+x = 0O777 .NOT_IMPLEMENTED_attr -+x = NOT_IMPLEMENTED_call() -+x = NOT_YET_IMPLEMENTED_ExprUnaryOp ++x = -100.0000J --if (10).real: -+if 10 .NOT_IMPLEMENTED_attr: + if (10).real: ... - --y = 100[no] --y = 100(no) -+y = NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+y = NOT_IMPLEMENTED_call() ``` ## Ruff Output ```py -x = NOT_IMPLEMENTED_call() -x = NOT_IMPLEMENTED_call() -x = NOT_IMPLEMENTED_call() -x = 1. .NOT_IMPLEMENTED_attr -x = 1E+1 .NOT_IMPLEMENTED_attr -x = 1E-1 .NOT_IMPLEMENTED_attr -x = NOT_IMPLEMENTED_call() -x = 123456789.123456789E123456789 .NOT_IMPLEMENTED_attr -x = NOT_IMPLEMENTED_call() -x = 123456789J.NOT_IMPLEMENTED_attr -x = NOT_IMPLEMENTED_call() -x = NOT_IMPLEMENTED_call() -x = NOT_IMPLEMENTED_call() -x = 0O777 .NOT_IMPLEMENTED_attr -x = NOT_IMPLEMENTED_call() -x = NOT_YET_IMPLEMENTED_ExprUnaryOp +x = (123456789).bit_count() +x = (123456).__abs__() +x = (.1).is_integer() +x = (1.).imag +x = (1E+1).imag +x = (1E-1).real +x = (123456789.123456789).hex() +x = (123456789.123456789E123456789).real +x = (123456789E123456789).conjugate() +x = 123456789J.real +x = 123456789.123456789J.__add__((0b1011).bit_length()) +x = (0XB1ACC).conjugate() +x = (0B1011).conjugate() +x = (0O777).real +x = (0.000000006).hex() +x = -100.0000J -if 10 .NOT_IMPLEMENTED_attr: +if (10).real: ... -y = NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -y = NOT_IMPLEMENTED_call() +y = 100[no] +y = 100(no) ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__bracketmatch_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__bracketmatch.py.snap similarity index 66% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__bracketmatch_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__bracketmatch.py.snap index fd018bb9fa..8201dc55c6 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__bracketmatch_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__bracketmatch.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/bracketmatch.py --- ## Input @@ -19,22 +18,22 @@ lambda x=lambda y={1: 3}: y['x':lambda y: {1: 2}]: x ```diff --- Black +++ Ruff -@@ -1,4 +1,3 @@ --for ((x in {}) or {})["a"] in x: -- pass +@@ -1,4 +1,4 @@ + for ((x in {}) or {})["a"] in x: + pass -pem_spam = lambda l, spam={"x": 3}: not spam.get(l.strip()) -lambda x=lambda y={1: 3}: y["x" : lambda y: {1: 2}]: x -+NOT_YET_IMPLEMENTED_StmtFor -+pem_spam = lambda x: True -+lambda x: True ++pem_spam = lambda NOT_YET_IMPLEMENTED_lambda: True ++lambda NOT_YET_IMPLEMENTED_lambda: True ``` ## Ruff Output ```py -NOT_YET_IMPLEMENTED_StmtFor -pem_spam = lambda x: True -lambda x: True +for ((x in {}) or {})["a"] in x: + pass +pem_spam = lambda NOT_YET_IMPLEMENTED_lambda: True +lambda NOT_YET_IMPLEMENTED_lambda: True ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__class_methods_new_line_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__class_methods_new_line.py.snap similarity index 54% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__class_methods_new_line_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__class_methods_new_line.py.snap index ecb8824ed1..436b6923ff 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__class_methods_new_line_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__class_methods_new_line.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/class_methods_new_line.py --- ## Input @@ -113,259 +112,204 @@ class ClassWithDecoInitAndVarsAndDocstringWithInner2: ```diff --- Black +++ Ruff -@@ -1,165 +1,61 @@ --class ClassSimplest: -- pass -+NOT_YET_IMPLEMENTED_StmtClassDef +@@ -31,7 +31,6 @@ - --class ClassWithSingleField: -- a = 1 -+NOT_YET_IMPLEMENTED_StmtClassDef - - --class ClassWithJustTheDocstring: -- """Just a docstring.""" -+NOT_YET_IMPLEMENTED_StmtClassDef - - --class ClassWithInit: -- def __init__(self): -- pass -+NOT_YET_IMPLEMENTED_StmtClassDef - - --class ClassWithTheDocstringAndInit: -- """Just a docstring.""" -+NOT_YET_IMPLEMENTED_StmtClassDef - -- def __init__(self): -- pass - -+NOT_YET_IMPLEMENTED_StmtClassDef - --class ClassWithInitAndVars: -- cls_var = 100 - -- def __init__(self): -- pass -+NOT_YET_IMPLEMENTED_StmtClassDef - - --class ClassWithInitAndVarsAndDocstring: -- """Test class""" -+NOT_YET_IMPLEMENTED_StmtClassDef - -- cls_var = 100 - -- def __init__(self): -- pass -+NOT_YET_IMPLEMENTED_StmtClassDef - - --class ClassWithDecoInit: -- @deco -- def __init__(self): -- pass -+NOT_YET_IMPLEMENTED_StmtClassDef - - --class ClassWithDecoInitAndVars: -- cls_var = 100 -+NOT_YET_IMPLEMENTED_StmtClassDef - -- @deco -- def __init__(self): -- pass - -+NOT_YET_IMPLEMENTED_StmtClassDef - --class ClassWithDecoInitAndVarsAndDocstring: -- """Test class""" - -- cls_var = 100 -+NOT_YET_IMPLEMENTED_StmtClassDef - -- @deco -- def __init__(self): -- pass - -+NOT_YET_IMPLEMENTED_StmtClassDef - --class ClassSimplestWithInner: -- class Inner: -- pass - -+NOT_YET_IMPLEMENTED_StmtClassDef - --class ClassSimplestWithInnerWithDocstring: -- class Inner: -- """Just a docstring.""" - -- def __init__(self): -- pass -+NOT_YET_IMPLEMENTED_StmtClassDef - - --class ClassWithSingleFieldWithInner: -- a = 1 -+NOT_YET_IMPLEMENTED_StmtClassDef - -- class Inner: -- pass - -+NOT_YET_IMPLEMENTED_StmtClassDef - --class ClassWithJustTheDocstringWithInner: -- """Just a docstring.""" - -- class Inner: -- pass -+NOT_YET_IMPLEMENTED_StmtClassDef - - --class ClassWithInitWithInner: -- class Inner: -- pass + class ClassWithInitAndVarsAndDocstring: + """Test class""" - -- def __init__(self): -- pass -- -- --class ClassWithInitAndVarsWithInner: -- cls_var = 100 -- -- class Inner: -- pass -- -- def __init__(self): -- pass -- -- --class ClassWithInitAndVarsAndDocstringWithInner: -- """Test class""" -- -- cls_var = 100 -- -- class Inner: -- pass -- -- def __init__(self): -- pass -- -- --class ClassWithDecoInitWithInner: -- class Inner: -- pass -- -- @deco -- def __init__(self): -- pass -+NOT_YET_IMPLEMENTED_StmtClassDef + cls_var = 100 + def __init__(self): +@@ -54,7 +53,6 @@ --class ClassWithDecoInitAndVarsWithInner: -- cls_var = 100 + class ClassWithDecoInitAndVarsAndDocstring: + """Test class""" - -- class Inner: -- pass + cls_var = 100 + + @deco +@@ -109,7 +107,6 @@ + + class ClassWithInitAndVarsAndDocstringWithInner: + """Test class""" - -- @deco -- def __init__(self): -- pass + cls_var = 100 + + class Inner: +@@ -141,7 +138,6 @@ + + class ClassWithDecoInitAndVarsAndDocstringWithInner: + """Test class""" - -- --class ClassWithDecoInitAndVarsAndDocstringWithInner: -- """Test class""" -- -- cls_var = 100 -- -- class Inner: -- pass -- -- @deco -- def __init__(self): -- pass -- -- --class ClassWithDecoInitAndVarsAndDocstringWithInner2: -- """Test class""" -- -- class Inner: -- pass -- -- cls_var = 100 -- -- @deco -- def __init__(self): -- pass -+NOT_YET_IMPLEMENTED_StmtClassDef + cls_var = 100 + + class Inner: ``` ## Ruff Output ```py -NOT_YET_IMPLEMENTED_StmtClassDef +class ClassSimplest: + pass -NOT_YET_IMPLEMENTED_StmtClassDef +class ClassWithSingleField: + a = 1 -NOT_YET_IMPLEMENTED_StmtClassDef +class ClassWithJustTheDocstring: + """Just a docstring.""" -NOT_YET_IMPLEMENTED_StmtClassDef +class ClassWithInit: + def __init__(self): + pass -NOT_YET_IMPLEMENTED_StmtClassDef +class ClassWithTheDocstringAndInit: + """Just a docstring.""" + + def __init__(self): + pass -NOT_YET_IMPLEMENTED_StmtClassDef +class ClassWithInitAndVars: + cls_var = 100 + + def __init__(self): + pass -NOT_YET_IMPLEMENTED_StmtClassDef +class ClassWithInitAndVarsAndDocstring: + """Test class""" + cls_var = 100 + + def __init__(self): + pass -NOT_YET_IMPLEMENTED_StmtClassDef +class ClassWithDecoInit: + @deco + def __init__(self): + pass -NOT_YET_IMPLEMENTED_StmtClassDef +class ClassWithDecoInitAndVars: + cls_var = 100 + + @deco + def __init__(self): + pass -NOT_YET_IMPLEMENTED_StmtClassDef +class ClassWithDecoInitAndVarsAndDocstring: + """Test class""" + cls_var = 100 + + @deco + def __init__(self): + pass -NOT_YET_IMPLEMENTED_StmtClassDef +class ClassSimplestWithInner: + class Inner: + pass -NOT_YET_IMPLEMENTED_StmtClassDef +class ClassSimplestWithInnerWithDocstring: + class Inner: + """Just a docstring.""" + + def __init__(self): + pass -NOT_YET_IMPLEMENTED_StmtClassDef +class ClassWithSingleFieldWithInner: + a = 1 + + class Inner: + pass -NOT_YET_IMPLEMENTED_StmtClassDef +class ClassWithJustTheDocstringWithInner: + """Just a docstring.""" + + class Inner: + pass -NOT_YET_IMPLEMENTED_StmtClassDef +class ClassWithInitWithInner: + class Inner: + pass + + def __init__(self): + pass -NOT_YET_IMPLEMENTED_StmtClassDef +class ClassWithInitAndVarsWithInner: + cls_var = 100 + + class Inner: + pass + + def __init__(self): + pass -NOT_YET_IMPLEMENTED_StmtClassDef +class ClassWithInitAndVarsAndDocstringWithInner: + """Test class""" + cls_var = 100 + + class Inner: + pass + + def __init__(self): + pass -NOT_YET_IMPLEMENTED_StmtClassDef +class ClassWithDecoInitWithInner: + class Inner: + pass + + @deco + def __init__(self): + pass -NOT_YET_IMPLEMENTED_StmtClassDef +class ClassWithDecoInitAndVarsWithInner: + cls_var = 100 + + class Inner: + pass + + @deco + def __init__(self): + pass -NOT_YET_IMPLEMENTED_StmtClassDef +class ClassWithDecoInitAndVarsAndDocstringWithInner: + """Test class""" + cls_var = 100 + + class Inner: + pass + + @deco + def __init__(self): + pass -NOT_YET_IMPLEMENTED_StmtClassDef +class ClassWithDecoInitAndVarsAndDocstringWithInner2: + """Test class""" + + class Inner: + pass + + cls_var = 100 + + @deco + def __init__(self): + pass ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__collections_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__collections.py.snap similarity index 52% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__collections_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__collections.py.snap index 16be7db816..cf66667382 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__collections_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__collections.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/collections.py --- ## Input @@ -84,31 +83,9 @@ if True: ```diff --- Black +++ Ruff -@@ -1,99 +1,52 @@ --import core, time, a -+NOT_YET_IMPLEMENTED_StmtImport - --from . import A, B, C -+NOT_YET_IMPLEMENTED_StmtImportFrom - - # keeps existing trailing comma --from foo import ( -- bar, --) -+NOT_YET_IMPLEMENTED_StmtImportFrom - - # also keeps existing structure --from foo import ( -- baz, -- qux, --) -+NOT_YET_IMPLEMENTED_StmtImportFrom - - # `as` works as well --from foo import ( -- xyzzy as magic, --) -+NOT_YET_IMPLEMENTED_StmtImportFrom +@@ -18,23 +18,12 @@ + xyzzy as magic, + ) -a = { - 1, @@ -122,158 +99,119 @@ if True: - 2, - 3, -} --x = (1,) --y = (narf(),) ++c = {1, 2, 3} + x = (1,) + y = (narf(),) -nested = { - (1, 2, 3), - (4, 5, 6), -} --nested_no_trailing_comma = {(1, 2, 3), (4, 5, 6)} -+c = {1, 2, 3} -+x = (1, 2) -+y = (1, 2) -+nested = {(1, 2), (1, 2)} -+nested_no_trailing_comma = {(1, 2), (1, 2)} ++nested = {(1, 2, 3), (4, 5, 6)} + nested_no_trailing_comma = {(1, 2, 3), (4, 5, 6)} nested_long_lines = [ -- "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", -- "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", -- "cccccccccccccccccccccccccccccccccccccccc", -- (1, 2, 3), -- "dddddddddddddddddddddddddddddddddddddddd", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ (1, 2), -+ "NOT_YET_IMPLEMENTED_STRING", - ] --{ -- "oneple": (1,), --} --{"oneple": (1,)} --["ls", "lsoneple/%s" % (foo,)] --x = {"oneple": (1,)} --y = { -- "oneple": (1,), --} + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", +@@ -52,10 +41,7 @@ + y = { + "oneple": (1,), + } -assert False, ( - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa wraps %s" - % bar -) -+{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} -+{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} -+["NOT_YET_IMPLEMENTED_STRING", "NOT_YET_IMPLEMENTED_STRING" % (1, 2)] -+x = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} -+y = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} +NOT_YET_IMPLEMENTED_StmtAssert # looping over a 1-tuple should also not get wrapped --for x in (1,): -- pass --for (x,) in (1,), (2,), (3,): -- pass -+NOT_YET_IMPLEMENTED_StmtFor -+NOT_YET_IMPLEMENTED_StmtFor - --[ -- 1, -- 2, -- 3, --] -+[1, 2, 3] - --division_result_tuple = (6 / 2,) --print("foo %r", (foo.bar,)) -+division_result_tuple = (1, 2) -+NOT_IMPLEMENTED_call() - - if True: -- IGNORED_TYPES_FOR_ATTRIBUTE_CHECKING = ( -- Config.IGNORED_TYPES_FOR_ATTRIBUTE_CHECKING -- | {pylons.controllers.WSGIController} -- ) -+ IGNORED_TYPES_FOR_ATTRIBUTE_CHECKING = Config.NOT_IMPLEMENTED_attr | { -+ pylons.NOT_IMPLEMENTED_attr.NOT_IMPLEMENTED_attr, -+ } - - if True: -- ec2client.get_waiter("instance_stopped").wait( -- InstanceIds=[instance.id], -- WaiterConfig={ -- "Delay": 5, -- }, -- ) -- ec2client.get_waiter("instance_stopped").wait( -- InstanceIds=[instance.id], -- WaiterConfig={ -- "Delay": 5, -- }, -- ) -- ec2client.get_waiter("instance_stopped").wait( -- InstanceIds=[instance.id], -- WaiterConfig={ -- "Delay": 5, -- }, -- ) -+ NOT_IMPLEMENTED_call() -+ NOT_IMPLEMENTED_call() -+ NOT_IMPLEMENTED_call() + for x in (1,): ``` ## Ruff Output ```py -NOT_YET_IMPLEMENTED_StmtImport +import core, time, a -NOT_YET_IMPLEMENTED_StmtImportFrom +from . import A, B, C # keeps existing trailing comma -NOT_YET_IMPLEMENTED_StmtImportFrom +from foo import ( + bar, +) # also keeps existing structure -NOT_YET_IMPLEMENTED_StmtImportFrom +from foo import ( + baz, + qux, +) # `as` works as well -NOT_YET_IMPLEMENTED_StmtImportFrom +from foo import ( + xyzzy as magic, +) a = {1, 2, 3} b = {1, 2, 3} c = {1, 2, 3} -x = (1, 2) -y = (1, 2) -nested = {(1, 2), (1, 2)} -nested_no_trailing_comma = {(1, 2), (1, 2)} +x = (1,) +y = (narf(),) +nested = {(1, 2, 3), (4, 5, 6)} +nested_no_trailing_comma = {(1, 2, 3), (4, 5, 6)} nested_long_lines = [ - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - (1, 2), - "NOT_YET_IMPLEMENTED_STRING", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "cccccccccccccccccccccccccccccccccccccccc", + (1, 2, 3), + "dddddddddddddddddddddddddddddddddddddddd", ] -{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} -{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} -["NOT_YET_IMPLEMENTED_STRING", "NOT_YET_IMPLEMENTED_STRING" % (1, 2)] -x = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} -y = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} +{ + "oneple": (1,), +} +{"oneple": (1,)} +["ls", "lsoneple/%s" % (foo,)] +x = {"oneple": (1,)} +y = { + "oneple": (1,), +} NOT_YET_IMPLEMENTED_StmtAssert # looping over a 1-tuple should also not get wrapped -NOT_YET_IMPLEMENTED_StmtFor -NOT_YET_IMPLEMENTED_StmtFor +for x in (1,): + pass +for (x,) in (1,), (2,), (3,): + pass -[1, 2, 3] +[ + 1, + 2, + 3, +] -division_result_tuple = (1, 2) -NOT_IMPLEMENTED_call() +division_result_tuple = (6 / 2,) +print("foo %r", (foo.bar,)) if True: - IGNORED_TYPES_FOR_ATTRIBUTE_CHECKING = Config.NOT_IMPLEMENTED_attr | { - pylons.NOT_IMPLEMENTED_attr.NOT_IMPLEMENTED_attr, - } + IGNORED_TYPES_FOR_ATTRIBUTE_CHECKING = ( + Config.IGNORED_TYPES_FOR_ATTRIBUTE_CHECKING + | {pylons.controllers.WSGIController} + ) if True: - NOT_IMPLEMENTED_call() - NOT_IMPLEMENTED_call() - NOT_IMPLEMENTED_call() + ec2client.get_waiter("instance_stopped").wait( + InstanceIds=[instance.id], + WaiterConfig={ + "Delay": 5, + }, + ) + ec2client.get_waiter("instance_stopped").wait( + InstanceIds=[instance.id], + WaiterConfig={ + "Delay": 5, + }, + ) + ec2client.get_waiter("instance_stopped").wait( + InstanceIds=[instance.id], + WaiterConfig={ + "Delay": 5, + }, + ) ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comment_after_escaped_newline_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comment_after_escaped_newline.py.snap similarity index 91% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comment_after_escaped_newline_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comment_after_escaped_newline.py.snap index 92e670eafa..5a209c5759 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comment_after_escaped_newline_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comment_after_escaped_newline.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comment_after_escaped_newline.py --- ## Input diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments2.py.snap similarity index 58% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments2.py.snap index 90b9263196..a363f5c7fb 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments2.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments2.py --- ## Input @@ -178,254 +177,121 @@ instruction()#comment with bad spacing ```diff --- Black +++ Ruff -@@ -1,31 +1,27 @@ --from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( +@@ -1,8 +1,8 @@ + from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( - MyLovelyCompanyTeamProjectComponent, # NOT DRY --) --from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( ++ MyLovelyCompanyTeamProjectComponent # NOT DRY + ) + from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( - MyLovelyCompanyTeamProjectComponent as component, # DRY --) -+NOT_YET_IMPLEMENTED_StmtImportFrom -+NOT_YET_IMPLEMENTED_StmtImportFrom ++ MyLovelyCompanyTeamProjectComponent as component # DRY + ) # Please keep __all__ alphabetized within each category. - - __all__ = [ - # Super-special typing primitives. -- "Any", -- "Callable", -- "ClassVar", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", - # ABCs (from collections.abc). -- "AbstractSet", # collections.abc.Set. -- "ByteString", -- "Container", -+ "NOT_YET_IMPLEMENTED_STRING", # collections.abc.Set. -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", - # Concrete collection types. -- "Counter", -- "Deque", -- "Dict", -- "DefaultDict", -- "List", -- "Set", -- "FrozenSet", -- "NamedTuple", # Not really a type. -- "Generator", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", # Not really a type. -+ "NOT_YET_IMPLEMENTED_STRING", - ] - - not_shareables = [ -@@ -37,50 +33,51 @@ - # builtin types and objects - type, - object, -- object(), -- Exception(), -+ NOT_IMPLEMENTED_call(), -+ NOT_IMPLEMENTED_call(), - 42, - 100.0, -- "spam", -+ "NOT_YET_IMPLEMENTED_STRING", +@@ -45,7 +45,7 @@ # user-defined types and objects Cheese, -- Cheese("Wensleydale"), + Cheese("Wensleydale"), - SubBytes(b"spam"), -+ NOT_IMPLEMENTED_call(), -+ NOT_IMPLEMENTED_call(), ++ SubBytes(b"NOT_YET_IMPLEMENTED_BYTE_STRING"), ] --if "PYTHON" in os.environ: -- add_compiler(compiler_from_env()) -+if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: -+ NOT_IMPLEMENTED_call() - else: - # for compiler in compilers.values(): - # add_compiler(compiler) -- add_compiler(compilers[(7.0, 32)]) -+ NOT_IMPLEMENTED_call() - # add_compiler(compilers[(7.1, 64)]) - - + if "PYTHON" in os.environ: +@@ -60,8 +60,12 @@ # Comment before function. def inline_comments_in_brackets_ruin_everything(): if typedargslist: - parameters.children = [children[0], body, children[-1]] # (1 # )1 -- parameters.children = [ -- children[0], -+ parameters.NOT_IMPLEMENTED_attr = [ -+ NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], # (1 + parameters.children = [ ++ children[0], # (1 + body, -+ NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], # )1 ++ children[-1], # )1 + ] -+ parameters.NOT_IMPLEMENTED_attr = [ -+ NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], ++ parameters.children = [ + children[0], body, -- children[-1], # type: ignore -+ NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], # type: ignore - ] - else: -- parameters.children = [ -- parameters.children[0], # (2 what if this was actually long -+ parameters.NOT_IMPLEMENTED_attr = [ -+ NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], # (2 what if this was actually long + children[-1], # type: ignore +@@ -72,7 +76,11 @@ body, -- parameters.children[-1], # )2 -+ NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], # )2 + parameters.children[-1], # )2 ] - parameters.children = [parameters.what_if_this_was_actually_long.children[0], body, parameters.children[-1]] # type: ignore -- if ( -- self._proc is not None -- # has the child process finished? -- and self._returncode is None -- # the child process has finished, but the -- # transport hasn't been notified yet? -- and self._proc.poll() is None -- ): -+ parameters.NOT_IMPLEMENTED_attr = [ -+ NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], ++ parameters.children = [ ++ parameters.what_if_this_was_actually_long.children[0], + body, -+ NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], ++ parameters.children[-1], + ] # type: ignore -+ if NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: - pass - # no newline before or after - short = [ -@@ -91,75 +88,29 @@ - ] - - # no newline after -- call( -- arg1, -- arg2, -- """ --short --""", -- arg3=True, -- ) -+ NOT_IMPLEMENTED_call() - - ############################################################################ - -- call2( -- # short -- arg1, -- # but -- arg2, -- # multiline -- """ --short --""", -- # yup -- arg3=True, -- ) -- lcomp = [ + if ( + self._proc is not None + # has the child process finished? +@@ -115,7 +123,9 @@ + arg3=True, + ) + lcomp = [ - element for element in collection if element is not None # yup # yup # right -- ] -- lcomp2 = [ -- # hello -- element -- # yup -- for element in collection -- # right -- if element is not None -- ] -- lcomp3 = [ -- # This one is actually too long to fit in a single line. -- element.split("\n", 1)[0] -- # yup -- for element in collection.select_elements() -- # right -- if element is not None -- ] -+ NOT_IMPLEMENTED_call() -+ lcomp = [i for i in []] -+ lcomp2 = [i for i in []] -+ lcomp3 = [i for i in []] - while True: - if False: -- continue -+ NOT_YET_IMPLEMENTED_StmtContinue - - # and round and round we go - # and round and round we go - ++ element # yup ++ for element in collection # yup ++ if element is not None # right + ] + lcomp2 = [ + # hello +@@ -143,7 +153,10 @@ # let's return -- return Node( -- syms.simple_stmt, + return Node( + syms.simple_stmt, - [Node(statement, result), Leaf(token.NEWLINE, "\n")], # FIXME: \r\n? -- ) -+ return NOT_IMPLEMENTED_call() ++ [ ++ Node(statement, result), ++ Leaf(token.NEWLINE, "\n"), # FIXME: \r\n? ++ ], + ) --CONFIG_FILES = ( -- [ -- CONFIG_FILE, -- ] -- + SHARED_CONFIG_FILES -- + USER_CONFIG_FILES --) # type: Final -+CONFIG_FILES = [CONFIG_FILE] + SHARED_CONFIG_FILES + USER_CONFIG_FILES # type: Final +@@ -158,7 +171,10 @@ - --class Test: -- def _init_host(self, parsed) -> None: + class Test: + def _init_host(self, parsed) -> None: - if parsed.hostname is None or not parsed.hostname.strip(): # type: ignore -- pass -+NOT_YET_IMPLEMENTED_StmtClassDef ++ if ( ++ parsed.hostname is None # type: ignore ++ or not parsed.hostname.strip() ++ ): + pass - ####################### -@@ -167,7 +118,7 @@ - ####################### - - --instruction() # comment with bad spacing -+NOT_IMPLEMENTED_call() # comment with bad spacing - - # END COMMENTS - # MORE END COMMENTS ``` ## Ruff Output ```py -NOT_YET_IMPLEMENTED_StmtImportFrom -NOT_YET_IMPLEMENTED_StmtImportFrom +from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( + MyLovelyCompanyTeamProjectComponent # NOT DRY +) +from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( + MyLovelyCompanyTeamProjectComponent as component # DRY +) # Please keep __all__ alphabetized within each category. __all__ = [ # Super-special typing primitives. - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", + "Any", + "Callable", + "ClassVar", # ABCs (from collections.abc). - "NOT_YET_IMPLEMENTED_STRING", # collections.abc.Set. - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", + "AbstractSet", # collections.abc.Set. + "ByteString", + "Container", # Concrete collection types. - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", # Not really a type. - "NOT_YET_IMPLEMENTED_STRING", + "Counter", + "Deque", + "Dict", + "DefaultDict", + "List", + "Set", + "FrozenSet", + "NamedTuple", # Not really a type. + "Generator", ] not_shareables = [ @@ -437,51 +303,58 @@ not_shareables = [ # builtin types and objects type, object, - NOT_IMPLEMENTED_call(), - NOT_IMPLEMENTED_call(), + object(), + Exception(), 42, 100.0, - "NOT_YET_IMPLEMENTED_STRING", + "spam", # user-defined types and objects Cheese, - NOT_IMPLEMENTED_call(), - NOT_IMPLEMENTED_call(), + Cheese("Wensleydale"), + SubBytes(b"NOT_YET_IMPLEMENTED_BYTE_STRING"), ] -if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: - NOT_IMPLEMENTED_call() +if "PYTHON" in os.environ: + add_compiler(compiler_from_env()) else: # for compiler in compilers.values(): # add_compiler(compiler) - NOT_IMPLEMENTED_call() + add_compiler(compilers[(7.0, 32)]) # add_compiler(compilers[(7.1, 64)]) # Comment before function. def inline_comments_in_brackets_ruin_everything(): if typedargslist: - parameters.NOT_IMPLEMENTED_attr = [ - NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], # (1 + parameters.children = [ + children[0], # (1 body, - NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], # )1 + children[-1], # )1 ] - parameters.NOT_IMPLEMENTED_attr = [ - NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], + parameters.children = [ + children[0], body, - NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], # type: ignore + children[-1], # type: ignore ] else: - parameters.NOT_IMPLEMENTED_attr = [ - NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], # (2 what if this was actually long + parameters.children = [ + parameters.children[0], # (2 what if this was actually long body, - NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], # )2 + parameters.children[-1], # )2 ] - parameters.NOT_IMPLEMENTED_attr = [ - NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], + parameters.children = [ + parameters.what_if_this_was_actually_long.children[0], body, - NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], + parameters.children[-1], ] # type: ignore - if NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: + if ( + self._proc is not None + # has the child process finished? + and self._returncode is None + # the child process has finished, but the + # transport hasn't been notified yet? + and self._proc.poll() is None + ): pass # no newline before or after short = [ @@ -492,29 +365,83 @@ def inline_comments_in_brackets_ruin_everything(): ] # no newline after - NOT_IMPLEMENTED_call() + call( + arg1, + arg2, + """ +short +""", + arg3=True, + ) ############################################################################ - NOT_IMPLEMENTED_call() - lcomp = [i for i in []] - lcomp2 = [i for i in []] - lcomp3 = [i for i in []] + call2( + # short + arg1, + # but + arg2, + # multiline + """ +short +""", + # yup + arg3=True, + ) + lcomp = [ + element # yup + for element in collection # yup + if element is not None # right + ] + lcomp2 = [ + # hello + element + # yup + for element in collection + # right + if element is not None + ] + lcomp3 = [ + # This one is actually too long to fit in a single line. + element.split("\n", 1)[0] + # yup + for element in collection.select_elements() + # right + if element is not None + ] while True: if False: - NOT_YET_IMPLEMENTED_StmtContinue + continue # and round and round we go # and round and round we go # let's return - return NOT_IMPLEMENTED_call() + return Node( + syms.simple_stmt, + [ + Node(statement, result), + Leaf(token.NEWLINE, "\n"), # FIXME: \r\n? + ], + ) -CONFIG_FILES = [CONFIG_FILE] + SHARED_CONFIG_FILES + USER_CONFIG_FILES # type: Final +CONFIG_FILES = ( + [ + CONFIG_FILE, + ] + + SHARED_CONFIG_FILES + + USER_CONFIG_FILES +) # type: Final -NOT_YET_IMPLEMENTED_StmtClassDef +class Test: + def _init_host(self, parsed) -> None: + if ( + parsed.hostname is None # type: ignore + or not parsed.hostname.strip() + ): + pass ####################### @@ -522,7 +449,7 @@ NOT_YET_IMPLEMENTED_StmtClassDef ####################### -NOT_IMPLEMENTED_call() # comment with bad spacing +instruction() # comment with bad spacing # END COMMENTS # MORE END COMMENTS diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments4_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments4.py.snap similarity index 67% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments4_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments4.py.snap index 9d741d8b92..b7752423a8 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments4_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments4.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments4.py --- ## Input @@ -107,82 +106,26 @@ def foo3(list_a, list_b): ```diff --- Black +++ Ruff -@@ -1,94 +1,22 @@ --from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( -- MyLovelyCompanyTeamProjectComponent, # NOT DRY --) --from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( -- MyLovelyCompanyTeamProjectComponent as component, # DRY --) -+NOT_YET_IMPLEMENTED_StmtImportFrom -+NOT_YET_IMPLEMENTED_StmtImportFrom - - --class C: -- @pytest.mark.parametrize( -- ("post_data", "message"), -- [ -- # metadata_version errors. -- ( -- {}, -- "None is an invalid value for Metadata-Version. Error: This field is" -- " required. see" -- " https://packaging.python.org/specifications/core-metadata", -- ), -- ( -- {"metadata_version": "-1"}, -- "'-1' is an invalid value for Metadata-Version. Error: Unknown Metadata" -- " Version see" -- " https://packaging.python.org/specifications/core-metadata", -- ), -- # name errors. -- ( -- {"metadata_version": "1.2"}, -- "'' is an invalid value for Name. Error: This field is required. see" -- " https://packaging.python.org/specifications/core-metadata", -- ), -- ( -- {"metadata_version": "1.2", "name": "foo-"}, -- "'foo-' is an invalid value for Name. Error: Must start and end with a" -- " letter or numeral and contain only ascii numeric and '.', '_' and" -- " '-'. see https://packaging.python.org/specifications/core-metadata", -- ), -- # version errors. -- ( -- {"metadata_version": "1.2", "name": "example"}, -- "'' is an invalid value for Version. Error: This field is required. see" -- " https://packaging.python.org/specifications/core-metadata", -- ), -- ( -- {"metadata_version": "1.2", "name": "example", "version": "dog"}, -- "'dog' is an invalid value for Version. Error: Must start and end with" -- " a letter or numeral and contain only ascii numeric and '.', '_' and" -- " '-'. see https://packaging.python.org/specifications/core-metadata", -- ), -- ], -- ) -- def test_fails_invalid_post_data( -- self, pyramid_config, db_request, post_data, message -- ): -- pyramid_config.testing_securitypolicy(userid=1) -- db_request.POST = MultiDict(post_data) -+NOT_YET_IMPLEMENTED_StmtClassDef - +@@ -58,37 +58,28 @@ def foo(list_a, list_b): -- results = ( + results = ( - User.query.filter(User.foo == "bar") - .filter( # Because foo. -- db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b)) ++ User.query.filter(User.foo == "bar").filter( # Because foo. + db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b)) - ) - .filter(User.xyz.is_(None)) -- # Another comment about the filtering on is_quux goes here. ++ ).filter(User.xyz.is_(None)). + # Another comment about the filtering on is_quux goes here. - .filter(db.not_(User.is_pending.astext.cast(db.Boolean).is_(True))) - .order_by(User.created_at.desc()) - .with_for_update(key_share=True) - .all() -- ) -+ results = NOT_IMPLEMENTED_call() ++ filter(db.not_(User.is_pending.astext.cast(db.Boolean).is_(True))).order_by( ++ User.created_at.desc() ++ ).with_for_update(key_share=True).all() + ) return results @@ -195,7 +138,9 @@ def foo3(list_a, list_b): - ) - .filter(User.xyz.is_(None)) - ) -+ return NOT_IMPLEMENTED_call() ++ return User.query.filter(User.foo == "bar").filter( ++ db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b)) ++ ).filter(User.xyz.is_(None)) def foo3(list_a, list_b): @@ -203,37 +148,101 @@ def foo3(list_a, list_b): # Standalone comment but weirdly placed. - User.query.filter(User.foo == "bar") - .filter( -- db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b)) ++ User.query.filter(User.foo == "bar").filter( + db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b)) - ) - .filter(User.xyz.is_(None)) -+ NOT_IMPLEMENTED_call() ++ ).filter(User.xyz.is_(None)) ) ``` ## Ruff Output ```py -NOT_YET_IMPLEMENTED_StmtImportFrom -NOT_YET_IMPLEMENTED_StmtImportFrom +from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( + MyLovelyCompanyTeamProjectComponent, # NOT DRY +) +from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( + MyLovelyCompanyTeamProjectComponent as component, # DRY +) -NOT_YET_IMPLEMENTED_StmtClassDef +class C: + @pytest.mark.parametrize( + ("post_data", "message"), + [ + # metadata_version errors. + ( + {}, + "None is an invalid value for Metadata-Version. Error: This field is" + " required. see" + " https://packaging.python.org/specifications/core-metadata", + ), + ( + {"metadata_version": "-1"}, + "'-1' is an invalid value for Metadata-Version. Error: Unknown Metadata" + " Version see" + " https://packaging.python.org/specifications/core-metadata", + ), + # name errors. + ( + {"metadata_version": "1.2"}, + "'' is an invalid value for Name. Error: This field is required. see" + " https://packaging.python.org/specifications/core-metadata", + ), + ( + {"metadata_version": "1.2", "name": "foo-"}, + "'foo-' is an invalid value for Name. Error: Must start and end with a" + " letter or numeral and contain only ascii numeric and '.', '_' and" + " '-'. see https://packaging.python.org/specifications/core-metadata", + ), + # version errors. + ( + {"metadata_version": "1.2", "name": "example"}, + "'' is an invalid value for Version. Error: This field is required. see" + " https://packaging.python.org/specifications/core-metadata", + ), + ( + {"metadata_version": "1.2", "name": "example", "version": "dog"}, + "'dog' is an invalid value for Version. Error: Must start and end with" + " a letter or numeral and contain only ascii numeric and '.', '_' and" + " '-'. see https://packaging.python.org/specifications/core-metadata", + ), + ], + ) + def test_fails_invalid_post_data( + self, pyramid_config, db_request, post_data, message + ): + pyramid_config.testing_securitypolicy(userid=1) + db_request.POST = MultiDict(post_data) def foo(list_a, list_b): - results = NOT_IMPLEMENTED_call() + results = ( + User.query.filter(User.foo == "bar").filter( # Because foo. + db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b)) + ).filter(User.xyz.is_(None)). + # Another comment about the filtering on is_quux goes here. + filter(db.not_(User.is_pending.astext.cast(db.Boolean).is_(True))).order_by( + User.created_at.desc() + ).with_for_update(key_share=True).all() + ) return results def foo2(list_a, list_b): # Standalone comment reasonably placed. - return NOT_IMPLEMENTED_call() + return User.query.filter(User.foo == "bar").filter( + db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b)) + ).filter(User.xyz.is_(None)) def foo3(list_a, list_b): return ( # Standalone comment but weirdly placed. - NOT_IMPLEMENTED_call() + User.query.filter(User.foo == "bar").filter( + db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b)) + ).filter(User.xyz.is_(None)) ) ``` diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments6_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments6.py.snap similarity index 79% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments6_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments6.py.snap index 3009fba804..6be2a77bc1 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments6_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments6.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments6.py --- ## Input @@ -131,85 +130,60 @@ aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*ite ```diff --- Black +++ Ruff -@@ -1,4 +1,4 @@ --from typing import Any, Tuple -+NOT_YET_IMPLEMENTED_StmtImportFrom - - - def f( -@@ -49,15 +49,10 @@ +@@ -49,9 +49,7 @@ element = 0 # type: int another_element = 1 # type: float another_element_with_long_name = 2 # type: int - another_really_really_long_element_with_a_unnecessarily_long_name_to_describe_what_it_does_enterprise_style = ( - 3 - ) # type: int -- an_element_with_a_long_value = calls() or more_calls() and more() # type: bool + another_really_really_long_element_with_a_unnecessarily_long_name_to_describe_what_it_does_enterprise_style = 3 # type: int -+ an_element_with_a_long_value = NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 # type: bool + an_element_with_a_long_value = calls() or more_calls() and more() # type: bool -- tup = ( -- another_element, -- another_really_really_long_element_with_a_unnecessarily_long_name_to_describe_what_it_does_enterprise_style, -- ) # type: Tuple[int, int] -+ tup = (1, 2) # type: Tuple[int, int] + tup = ( +@@ -100,19 +98,30 @@ + ) - a = ( - element -@@ -84,35 +79,22 @@ - - - def func( -- a=some_list[0], # type: int -+ a=NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], # type: int - ): # type: () -> int -- c = call( -- 0.0123, -- 0.0456, -- 0.0789, -- 0.0123, -- 0.0456, -- 0.0789, -- 0.0123, -- 0.0456, -- 0.0789, -- a[-1], # type: ignore -- ) -+ c = NOT_IMPLEMENTED_call() - -- c = call( + c = call( - "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa" # type: ignore -- ) -+ c = NOT_IMPLEMENTED_call() ++ "aaaaaaaa", ++ "aaaaaaaa", ++ "aaaaaaaa", ++ "aaaaaaaa", ++ "aaaaaaaa", ++ "aaaaaaaa", ++ "aaaaaaaa", # type: ignore + ) -result = ( # aaa - "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -) -+result = "NOT_YET_IMPLEMENTED_STRING" # aaa ++result = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # aaa -AAAAAAAAAAAAA = [AAAAAAAAAAAAA] + SHARED_AAAAAAAAAAAAA + USER_AAAAAAAAAAAAA + AAAAAAAAAAAAA # type: ignore +AAAAAAAAAAAAA = ( -+ [AAAAAAAAAAAAA] -+ + SHARED_AAAAAAAAAAAAA -+ + USER_AAAAAAAAAAAAA -+ + AAAAAAAAAAAAA ++ [AAAAAAAAAAAAA] + SHARED_AAAAAAAAAAAAA + USER_AAAAAAAAAAAAA + AAAAAAAAAAAAA +) # type: ignore --call_to_some_function_asdf( -- foo, + call_to_some_function_asdf( + foo, - [AAAAAAAAAAAAAAAAAAAAAAA, AAAAAAAAAAAAAAAAAAAAAAA, AAAAAAAAAAAAAAAAAAAAAAA, BBBBBBBBBBBB], # type: ignore --) -+NOT_IMPLEMENTED_call() ++ [ ++ AAAAAAAAAAAAAAAAAAAAAAA, ++ AAAAAAAAAAAAAAAAAAAAAAA, ++ AAAAAAAAAAAAAAAAAAAAAAA, ++ BBBBBBBBBBBB, ++ ], # type: ignore + ) --aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*items))) # type: ignore[arg-type] -+(1, 2) = NOT_IMPLEMENTED_call() # type: ignore[arg-type] + aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*items))) # type: ignore[arg-type] ``` ## Ruff Output ```py -NOT_YET_IMPLEMENTED_StmtImportFrom +from typing import Any, Tuple def f( @@ -261,9 +235,12 @@ def f( another_element = 1 # type: float another_element_with_long_name = 2 # type: int another_really_really_long_element_with_a_unnecessarily_long_name_to_describe_what_it_does_enterprise_style = 3 # type: int - an_element_with_a_long_value = NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 # type: bool + an_element_with_a_long_value = calls() or more_calls() and more() # type: bool - tup = (1, 2) # type: Tuple[int, int] + tup = ( + another_element, + another_really_really_long_element_with_a_unnecessarily_long_name_to_describe_what_it_does_enterprise_style, + ) # type: Tuple[int, int] a = ( element @@ -290,25 +267,49 @@ def f( def func( - a=NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key], # type: int + a=some_list[0], # type: int ): # type: () -> int - c = NOT_IMPLEMENTED_call() + c = call( + 0.0123, + 0.0456, + 0.0789, + 0.0123, + 0.0456, + 0.0789, + 0.0123, + 0.0456, + 0.0789, + a[-1], # type: ignore + ) - c = NOT_IMPLEMENTED_call() + c = call( + "aaaaaaaa", + "aaaaaaaa", + "aaaaaaaa", + "aaaaaaaa", + "aaaaaaaa", + "aaaaaaaa", + "aaaaaaaa", # type: ignore + ) -result = "NOT_YET_IMPLEMENTED_STRING" # aaa +result = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # aaa AAAAAAAAAAAAA = ( - [AAAAAAAAAAAAA] - + SHARED_AAAAAAAAAAAAA - + USER_AAAAAAAAAAAAA - + AAAAAAAAAAAAA + [AAAAAAAAAAAAA] + SHARED_AAAAAAAAAAAAA + USER_AAAAAAAAAAAAA + AAAAAAAAAAAAA ) # type: ignore -NOT_IMPLEMENTED_call() +call_to_some_function_asdf( + foo, + [ + AAAAAAAAAAAAAAAAAAAAAAA, + AAAAAAAAAAAAAAAAAAAAAAA, + AAAAAAAAAAAAAAAAAAAAAAA, + BBBBBBBBBBBB, + ], # type: ignore +) -(1, 2) = NOT_IMPLEMENTED_call() # type: ignore[arg-type] +aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*items))) # type: ignore[arg-type] ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments9_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments9.py.snap similarity index 83% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments9_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments9.py.snap index cddd66b042..2399d84c1b 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments9_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments9.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments9.py --- ## Input @@ -152,65 +151,7 @@ def bar(): ```diff --- Black +++ Ruff -@@ -30,8 +30,7 @@ - - - # This comment should be split from the statement above by two lines. --class MyClass: -- pass -+NOT_YET_IMPLEMENTED_StmtClassDef - - - some = statement -@@ -39,17 +38,14 @@ - - - # This should be split from the above by two lines --class MyClassWithComplexLeadingComments: -- pass -+NOT_YET_IMPLEMENTED_StmtClassDef - - --class ClassWithDocstring: -- """A docstring.""" -+NOT_YET_IMPLEMENTED_StmtClassDef - - - # Leading comment after a class with just a docstring --class MyClassAfterAnotherClassWithDocstring: -- pass -+NOT_YET_IMPLEMENTED_StmtClassDef - - - some = statement -@@ -59,7 +55,7 @@ - @deco1 - # leading 2 - # leading 2 extra --@deco2(with_args=True) -+@NOT_IMPLEMENTED_call() - # leading 3 - @deco3 - # leading 4 -@@ -73,7 +69,7 @@ - # leading 1 - @deco1 - # leading 2 --@deco2(with_args=True) -+@NOT_IMPLEMENTED_call() - - # leading 3 that already has an empty line - @deco3 -@@ -88,7 +84,7 @@ - # leading 1 - @deco1 - # leading 2 --@deco2(with_args=True) -+@NOT_IMPLEMENTED_call() - # leading 3 - @deco3 - -@@ -106,7 +102,6 @@ +@@ -106,7 +106,6 @@ # Another leading comment def another_inline(): pass @@ -218,7 +159,7 @@ def bar(): else: # More leading comments def inline_after_else(): -@@ -121,18 +116,13 @@ +@@ -121,7 +120,6 @@ # Another leading comment def another_top_level_quote_inline_inline(): pass @@ -226,18 +167,6 @@ def bar(): else: # More leading comments def top_level_quote_inline_after_else(): - pass - - --class MyClass: -- # First method has no empty lines between bare class def. -- # More comments. -- def first_method(self): -- pass -+NOT_YET_IMPLEMENTED_StmtClassDef - - - # Regression test for https://github.com/psf/black/issues/3454. ``` ## Ruff Output @@ -275,7 +204,8 @@ some = statement # This comment should be split from the statement above by two lines. -NOT_YET_IMPLEMENTED_StmtClassDef +class MyClass: + pass some = statement @@ -283,14 +213,17 @@ some = statement # This should be split from the above by two lines -NOT_YET_IMPLEMENTED_StmtClassDef +class MyClassWithComplexLeadingComments: + pass -NOT_YET_IMPLEMENTED_StmtClassDef +class ClassWithDocstring: + """A docstring.""" # Leading comment after a class with just a docstring -NOT_YET_IMPLEMENTED_StmtClassDef +class MyClassAfterAnotherClassWithDocstring: + pass some = statement @@ -300,7 +233,7 @@ some = statement @deco1 # leading 2 # leading 2 extra -@NOT_IMPLEMENTED_call() +@deco2(with_args=True) # leading 3 @deco3 # leading 4 @@ -314,7 +247,7 @@ some = statement # leading 1 @deco1 # leading 2 -@NOT_IMPLEMENTED_call() +@deco2(with_args=True) # leading 3 that already has an empty line @deco3 @@ -329,7 +262,7 @@ some = statement # leading 1 @deco1 # leading 2 -@NOT_IMPLEMENTED_call() +@deco2(with_args=True) # leading 3 @deco3 @@ -367,7 +300,11 @@ else: pass -NOT_YET_IMPLEMENTED_StmtClassDef +class MyClass: + # First method has no empty lines between bare class def. + # More comments. + def first_method(self): + pass # Regression test for https://github.com/psf/black/issues/3454. diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_non_breaking_space_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments_non_breaking_space.py.snap similarity index 68% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_non_breaking_space_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments_non_breaking_space.py.snap index cddc816cdb..e8c73055d0 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_non_breaking_space_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments_non_breaking_space.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments_non_breaking_space.py --- ## Input @@ -32,51 +31,44 @@ def function(a:int=42): ```diff --- Black +++ Ruff -@@ -1,23 +1,15 @@ --from .config import ( -- ConfigTypeAttributes, -- Int, -- Path, # String, -- # DEFAULT_TYPE_ATTRIBUTES, --) -+NOT_YET_IMPLEMENTED_StmtImportFrom - - result = 1 # A simple comment --result = (1,) # Another one -+result = (1, 2) # Another one - - result = 1 #  type: ignore - result = 1 # This comment is talking about type: ignore --square = Square(4) #  type: Optional[Square] -+square = NOT_IMPLEMENTED_call() #  type: Optional[Square] +@@ -14,9 +14,9 @@ def function(a: int = 42): - """This docstring is already formatted - a - b -- """ -+ "NOT_YET_IMPLEMENTED_STRING" ++ """ This docstring is already formatted ++ a ++ b + """ # There's a NBSP + 3 spaces before # And 4 spaces on the next line - pass ``` ## Ruff Output ```py -NOT_YET_IMPLEMENTED_StmtImportFrom +from .config import ( + ConfigTypeAttributes, + Int, + Path, # String, + # DEFAULT_TYPE_ATTRIBUTES, +) result = 1 # A simple comment -result = (1, 2) # Another one +result = (1,) # Another one result = 1 #  type: ignore result = 1 # This comment is talking about type: ignore -square = NOT_IMPLEMENTED_call() #  type: Optional[Square] +square = Square(4) #  type: Optional[Square] def function(a: int = 42): - "NOT_YET_IMPLEMENTED_STRING" + """ This docstring is already formatted + a + b + """ # There's a NBSP + 3 spaces before # And 4 spaces on the next line pass diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__composition_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__composition.py.snap similarity index 76% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__composition_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__composition.py.snap index 86812da5ea..71b76f27cd 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__composition_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__composition.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/composition.py --- ## Input @@ -194,68 +193,20 @@ class C: ```diff --- Black +++ Ruff -@@ -1,181 +1 @@ --class C: -- def test(self) -> None: -- with patch("black.out", print): -- self.assertEqual( -- unstyle(str(report)), "1 file reformatted, 1 file failed to reformat." -- ) -- self.assertEqual( -- unstyle(str(report)), -- "1 file reformatted, 1 file left unchanged, 1 file failed to reformat.", -- ) -- self.assertEqual( -- unstyle(str(report)), -- "2 files reformatted, 1 file left unchanged, 1 file failed to" -- " reformat.", -- ) -- self.assertEqual( -- unstyle(str(report)), -- "2 files reformatted, 2 files left unchanged, 2 files failed to" -- " reformat.", -- ) -- for i in (a,): -- if ( -- # Rule 1 -- i % 2 == 0 -- # Rule 2 -- and i % 3 == 0 -- ): -- while ( -- # Just a comment -- call() -- # Another +@@ -28,8 +28,8 @@ + while ( + # Just a comment + call() ++ ): + # Another - ): -- print(i) -- xxxxxxxxxxxxxxxx = Yyyy2YyyyyYyyyyy( -- push_manager=context.request.resource_manager, -- max_items_to_push=num_items, -- batch_size=Yyyy2YyyyYyyyyYyyy.FULL_SIZE, -- ).push( -- # Only send the first n items. -- items=items[:num_items] -- ) -- return ( -- 'Utterly failed doctest test for %s\n File "%s", line %s, in %s\n\n%s' -- % (test.name, test.filename, lineno, lname, err) -- ) -- -- def omitting_trailers(self) -> None: -- get_collection( -- hey_this_is_a_very_long_call, it_has_funny_attributes, really=True -- )[OneLevelIndex] -- get_collection( -- hey_this_is_a_very_long_call, it_has_funny_attributes, really=True -- )[OneLevelIndex][TwoLevelIndex][ThreeLevelIndex][FourLevelIndex] -- d[0][1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20][21][ -- 22 -- ] -- assignment = ( -- some.rather.elaborate.rule() and another.rule.ending_with.index[123] -- ) -- -- def easy_asserts(self) -> None: + print(i) + xxxxxxxxxxxxxxxx = Yyyy2YyyyyYyyyyy( + push_manager=context.request.resource_manager, +@@ -59,101 +59,23 @@ + ) + + def easy_asserts(self) -> None: - assert { - key1: value1, - key2: value2, @@ -267,7 +218,8 @@ class C: - key8: value8, - key9: value9, - } == expected, "Not what we expected" -- ++ NOT_YET_IMPLEMENTED_StmtAssert + - assert expected == { - key1: value1, - key2: value2, @@ -279,7 +231,8 @@ class C: - key8: value8, - key9: value9, - }, "Not what we expected" -- ++ NOT_YET_IMPLEMENTED_StmtAssert + - assert expected == { - key1: value1, - key2: value2, @@ -291,8 +244,9 @@ class C: - key8: value8, - key9: value9, - } -- -- def tricky_asserts(self) -> None: ++ NOT_YET_IMPLEMENTED_StmtAssert + + def tricky_asserts(self) -> None: - assert { - key1: value1, - key2: value2, @@ -306,7 +260,8 @@ class C: - } == expected( - value, is_going_to_be="too long to fit in a single line", srsly=True - ), "Not what we expected" -- ++ NOT_YET_IMPLEMENTED_StmtAssert + - assert { - key1: value1, - key2: value2, @@ -320,7 +275,8 @@ class C: - } == expected, ( - "Not what we expected and the message is too long to fit in one line" - ) -- ++ NOT_YET_IMPLEMENTED_StmtAssert + - assert expected( - value, is_going_to_be="too long to fit in a single line", srsly=True - ) == { @@ -334,7 +290,8 @@ class C: - key8: value8, - key9: value9, - }, "Not what we expected" -- ++ NOT_YET_IMPLEMENTED_StmtAssert + - assert expected == { - key1: value1, - key2: value2, @@ -349,19 +306,24 @@ class C: - "Not what we expected and the message is too long to fit in one line" - " because it's too long" - ) -- ++ NOT_YET_IMPLEMENTED_StmtAssert + - dis_c_instance_method = """\ -- %3d 0 LOAD_FAST 1 (x) -- 2 LOAD_CONST 1 (1) -- 4 COMPARE_OP 2 (==) -- 6 LOAD_FAST 0 (self) -- 8 STORE_ATTR 0 (x) -- 10 LOAD_CONST 0 (None) -- 12 RETURN_VALUE ++ dis_c_instance_method = ( ++ """\ + %3d 0 LOAD_FAST 1 (x) + 2 LOAD_CONST 1 (1) + 4 COMPARE_OP 2 (==) +@@ -161,21 +83,8 @@ + 8 STORE_ATTR 0 (x) + 10 LOAD_CONST 0 (None) + 12 RETURN_VALUE - """ % ( - _C.__init__.__code__.co_firstlineno + 1, -- ) -- ++ """ ++ % (_C.__init__.__code__.co_firstlineno + 1,) + ) + - assert ( - expectedexpectedexpectedexpectedexpectedexpectedexpectedexpectedexpect - == { @@ -376,13 +338,102 @@ class C: - key9: value9, - } - ) -+NOT_YET_IMPLEMENTED_StmtClassDef ++ NOT_YET_IMPLEMENTED_StmtAssert ``` ## Ruff Output ```py -NOT_YET_IMPLEMENTED_StmtClassDef +class C: + def test(self) -> None: + with patch("black.out", print): + self.assertEqual( + unstyle(str(report)), "1 file reformatted, 1 file failed to reformat." + ) + self.assertEqual( + unstyle(str(report)), + "1 file reformatted, 1 file left unchanged, 1 file failed to reformat.", + ) + self.assertEqual( + unstyle(str(report)), + "2 files reformatted, 1 file left unchanged, 1 file failed to" + " reformat.", + ) + self.assertEqual( + unstyle(str(report)), + "2 files reformatted, 2 files left unchanged, 2 files failed to" + " reformat.", + ) + for i in (a,): + if ( + # Rule 1 + i % 2 == 0 + # Rule 2 + and i % 3 == 0 + ): + while ( + # Just a comment + call() + ): + # Another + print(i) + xxxxxxxxxxxxxxxx = Yyyy2YyyyyYyyyyy( + push_manager=context.request.resource_manager, + max_items_to_push=num_items, + batch_size=Yyyy2YyyyYyyyyYyyy.FULL_SIZE, + ).push( + # Only send the first n items. + items=items[:num_items] + ) + return ( + 'Utterly failed doctest test for %s\n File "%s", line %s, in %s\n\n%s' + % (test.name, test.filename, lineno, lname, err) + ) + + def omitting_trailers(self) -> None: + get_collection( + hey_this_is_a_very_long_call, it_has_funny_attributes, really=True + )[OneLevelIndex] + get_collection( + hey_this_is_a_very_long_call, it_has_funny_attributes, really=True + )[OneLevelIndex][TwoLevelIndex][ThreeLevelIndex][FourLevelIndex] + d[0][1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20][21][ + 22 + ] + assignment = ( + some.rather.elaborate.rule() and another.rule.ending_with.index[123] + ) + + def easy_asserts(self) -> None: + NOT_YET_IMPLEMENTED_StmtAssert + + NOT_YET_IMPLEMENTED_StmtAssert + + NOT_YET_IMPLEMENTED_StmtAssert + + def tricky_asserts(self) -> None: + NOT_YET_IMPLEMENTED_StmtAssert + + NOT_YET_IMPLEMENTED_StmtAssert + + NOT_YET_IMPLEMENTED_StmtAssert + + NOT_YET_IMPLEMENTED_StmtAssert + + dis_c_instance_method = ( + """\ + %3d 0 LOAD_FAST 1 (x) + 2 LOAD_CONST 1 (1) + 4 COMPARE_OP 2 (==) + 6 LOAD_FAST 0 (self) + 8 STORE_ATTR 0 (x) + 10 LOAD_CONST 0 (None) + 12 RETURN_VALUE + """ + % (_C.__init__.__code__.co_firstlineno + 1,) + ) + + NOT_YET_IMPLEMENTED_StmtAssert ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__composition_no_trailing_comma_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__composition_no_trailing_comma.py.snap similarity index 76% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__composition_no_trailing_comma_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__composition_no_trailing_comma.py.snap index b45f26eec3..975059e7a0 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__composition_no_trailing_comma_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__composition_no_trailing_comma.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/composition_no_trailing_comma.py --- ## Input @@ -194,68 +193,20 @@ class C: ```diff --- Black +++ Ruff -@@ -1,181 +1 @@ --class C: -- def test(self) -> None: -- with patch("black.out", print): -- self.assertEqual( -- unstyle(str(report)), "1 file reformatted, 1 file failed to reformat." -- ) -- self.assertEqual( -- unstyle(str(report)), -- "1 file reformatted, 1 file left unchanged, 1 file failed to reformat.", -- ) -- self.assertEqual( -- unstyle(str(report)), -- "2 files reformatted, 1 file left unchanged, 1 file failed to" -- " reformat.", -- ) -- self.assertEqual( -- unstyle(str(report)), -- "2 files reformatted, 2 files left unchanged, 2 files failed to" -- " reformat.", -- ) -- for i in (a,): -- if ( -- # Rule 1 -- i % 2 == 0 -- # Rule 2 -- and i % 3 == 0 -- ): -- while ( -- # Just a comment -- call() -- # Another +@@ -28,8 +28,8 @@ + while ( + # Just a comment + call() ++ ): + # Another - ): -- print(i) -- xxxxxxxxxxxxxxxx = Yyyy2YyyyyYyyyyy( -- push_manager=context.request.resource_manager, -- max_items_to_push=num_items, -- batch_size=Yyyy2YyyyYyyyyYyyy.FULL_SIZE, -- ).push( -- # Only send the first n items. -- items=items[:num_items] -- ) -- return ( -- 'Utterly failed doctest test for %s\n File "%s", line %s, in %s\n\n%s' -- % (test.name, test.filename, lineno, lname, err) -- ) -- -- def omitting_trailers(self) -> None: -- get_collection( -- hey_this_is_a_very_long_call, it_has_funny_attributes, really=True -- )[OneLevelIndex] -- get_collection( -- hey_this_is_a_very_long_call, it_has_funny_attributes, really=True -- )[OneLevelIndex][TwoLevelIndex][ThreeLevelIndex][FourLevelIndex] -- d[0][1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20][21][ -- 22 -- ] -- assignment = ( -- some.rather.elaborate.rule() and another.rule.ending_with.index[123] -- ) -- -- def easy_asserts(self) -> None: + print(i) + xxxxxxxxxxxxxxxx = Yyyy2YyyyyYyyyyy( + push_manager=context.request.resource_manager, +@@ -59,101 +59,23 @@ + ) + + def easy_asserts(self) -> None: - assert { - key1: value1, - key2: value2, @@ -267,7 +218,8 @@ class C: - key8: value8, - key9: value9, - } == expected, "Not what we expected" -- ++ NOT_YET_IMPLEMENTED_StmtAssert + - assert expected == { - key1: value1, - key2: value2, @@ -279,7 +231,8 @@ class C: - key8: value8, - key9: value9, - }, "Not what we expected" -- ++ NOT_YET_IMPLEMENTED_StmtAssert + - assert expected == { - key1: value1, - key2: value2, @@ -291,8 +244,9 @@ class C: - key8: value8, - key9: value9, - } -- -- def tricky_asserts(self) -> None: ++ NOT_YET_IMPLEMENTED_StmtAssert + + def tricky_asserts(self) -> None: - assert { - key1: value1, - key2: value2, @@ -306,7 +260,8 @@ class C: - } == expected( - value, is_going_to_be="too long to fit in a single line", srsly=True - ), "Not what we expected" -- ++ NOT_YET_IMPLEMENTED_StmtAssert + - assert { - key1: value1, - key2: value2, @@ -320,7 +275,8 @@ class C: - } == expected, ( - "Not what we expected and the message is too long to fit in one line" - ) -- ++ NOT_YET_IMPLEMENTED_StmtAssert + - assert expected( - value, is_going_to_be="too long to fit in a single line", srsly=True - ) == { @@ -334,7 +290,8 @@ class C: - key8: value8, - key9: value9, - }, "Not what we expected" -- ++ NOT_YET_IMPLEMENTED_StmtAssert + - assert expected == { - key1: value1, - key2: value2, @@ -349,19 +306,24 @@ class C: - "Not what we expected and the message is too long to fit in one line" - " because it's too long" - ) -- ++ NOT_YET_IMPLEMENTED_StmtAssert + - dis_c_instance_method = """\ -- %3d 0 LOAD_FAST 1 (x) -- 2 LOAD_CONST 1 (1) -- 4 COMPARE_OP 2 (==) -- 6 LOAD_FAST 0 (self) -- 8 STORE_ATTR 0 (x) -- 10 LOAD_CONST 0 (None) -- 12 RETURN_VALUE ++ dis_c_instance_method = ( ++ """\ + %3d 0 LOAD_FAST 1 (x) + 2 LOAD_CONST 1 (1) + 4 COMPARE_OP 2 (==) +@@ -161,21 +83,8 @@ + 8 STORE_ATTR 0 (x) + 10 LOAD_CONST 0 (None) + 12 RETURN_VALUE - """ % ( - _C.__init__.__code__.co_firstlineno + 1, -- ) -- ++ """ ++ % (_C.__init__.__code__.co_firstlineno + 1,) + ) + - assert ( - expectedexpectedexpectedexpectedexpectedexpectedexpectedexpectedexpect - == { @@ -376,13 +338,102 @@ class C: - key9: value9, - } - ) -+NOT_YET_IMPLEMENTED_StmtClassDef ++ NOT_YET_IMPLEMENTED_StmtAssert ``` ## Ruff Output ```py -NOT_YET_IMPLEMENTED_StmtClassDef +class C: + def test(self) -> None: + with patch("black.out", print): + self.assertEqual( + unstyle(str(report)), "1 file reformatted, 1 file failed to reformat." + ) + self.assertEqual( + unstyle(str(report)), + "1 file reformatted, 1 file left unchanged, 1 file failed to reformat.", + ) + self.assertEqual( + unstyle(str(report)), + "2 files reformatted, 1 file left unchanged, 1 file failed to" + " reformat.", + ) + self.assertEqual( + unstyle(str(report)), + "2 files reformatted, 2 files left unchanged, 2 files failed to" + " reformat.", + ) + for i in (a,): + if ( + # Rule 1 + i % 2 == 0 + # Rule 2 + and i % 3 == 0 + ): + while ( + # Just a comment + call() + ): + # Another + print(i) + xxxxxxxxxxxxxxxx = Yyyy2YyyyyYyyyyy( + push_manager=context.request.resource_manager, + max_items_to_push=num_items, + batch_size=Yyyy2YyyyYyyyyYyyy.FULL_SIZE, + ).push( + # Only send the first n items. + items=items[:num_items] + ) + return ( + 'Utterly failed doctest test for %s\n File "%s", line %s, in %s\n\n%s' + % (test.name, test.filename, lineno, lname, err) + ) + + def omitting_trailers(self) -> None: + get_collection( + hey_this_is_a_very_long_call, it_has_funny_attributes, really=True + )[OneLevelIndex] + get_collection( + hey_this_is_a_very_long_call, it_has_funny_attributes, really=True + )[OneLevelIndex][TwoLevelIndex][ThreeLevelIndex][FourLevelIndex] + d[0][1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20][21][ + 22 + ] + assignment = ( + some.rather.elaborate.rule() and another.rule.ending_with.index[123] + ) + + def easy_asserts(self) -> None: + NOT_YET_IMPLEMENTED_StmtAssert + + NOT_YET_IMPLEMENTED_StmtAssert + + NOT_YET_IMPLEMENTED_StmtAssert + + def tricky_asserts(self) -> None: + NOT_YET_IMPLEMENTED_StmtAssert + + NOT_YET_IMPLEMENTED_StmtAssert + + NOT_YET_IMPLEMENTED_StmtAssert + + NOT_YET_IMPLEMENTED_StmtAssert + + dis_c_instance_method = ( + """\ + %3d 0 LOAD_FAST 1 (x) + 2 LOAD_CONST 1 (1) + 4 COMPARE_OP 2 (==) + 6 LOAD_FAST 0 (self) + 8 STORE_ATTR 0 (x) + 10 LOAD_CONST 0 (None) + 12 RETURN_VALUE + """ + % (_C.__init__.__code__.co_firstlineno + 1,) + ) + + NOT_YET_IMPLEMENTED_StmtAssert ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__docstring.py.snap similarity index 71% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__docstring.py.snap index ca085ed088..60a50df3a5 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__docstring.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/docstring.py --- ## Input @@ -234,64 +233,75 @@ def stable_quote_normalization_with_immediate_inner_single_quote(self): ```diff --- Black +++ Ruff -@@ -1,219 +1,149 @@ --class MyClass: +@@ -1,83 +1,85 @@ + class MyClass: - """Multiline - class docstring - """ -- -- def method(self): -- """Multiline ++ """ Multiline ++ class docstring ++ """ + + def method(self): + """Multiline - method docstring - """ -- pass -+NOT_YET_IMPLEMENTED_StmtClassDef ++ method docstring ++ """ + pass def foo(): - """This is a docstring with - some lines of text here - """ -+ "NOT_YET_IMPLEMENTED_STRING" ++ """This is a docstring with ++ some lines of text here ++ """ return def bar(): -- """This is another docstring + """This is another docstring - with more lines of text - """ -+ "NOT_YET_IMPLEMENTED_STRING" ++ with more lines of text ++ """ return def baz(): -- '''"This" is a string with some + '''"This" is a string with some - embedded "quotes"''' -+ "NOT_YET_IMPLEMENTED_STRING" ++ embedded "quotes"''' return def troz(): -- """Indentation with tabs + """Indentation with tabs - is just as OK - """ -+ "NOT_YET_IMPLEMENTED_STRING" ++ is just as OK ++ """ return def zort(): -- """Another + """Another - multiline - docstring - """ -+ "NOT_YET_IMPLEMENTED_STRING" ++ multiline ++ docstring ++ """ pass def poit(): -- """ + """ - Lorem ipsum dolor sit amet. -- ++ Lorem ipsum dolor sit amet. + - Consectetur adipiscing elit: - - sed do eiusmod tempor incididunt ut labore - - dolore magna aliqua @@ -299,112 +309,117 @@ def stable_quote_normalization_with_immediate_inner_single_quote(self): - - quis nostrud exercitation ullamco laboris nisi - - aliquip ex ea commodo consequat - """ -+ "NOT_YET_IMPLEMENTED_STRING" ++ Consectetur adipiscing elit: ++ - sed do eiusmod tempor incididunt ut labore ++ - dolore magna aliqua ++ - enim ad minim veniam ++ - quis nostrud exercitation ullamco laboris nisi ++ - aliquip ex ea commodo consequat ++ """ pass def under_indent(): -- """ + """ - These lines are indented in a way that does not - make sense. - """ -+ "NOT_YET_IMPLEMENTED_STRING" ++ These lines are indented in a way that does not ++make sense. ++ """ pass def over_indent(): -- """ + """ - This has a shallow indent - - But some lines are deeper - - And the closing quote is too deep -- """ -+ "NOT_YET_IMPLEMENTED_STRING" ++ This has a shallow indent ++ - But some lines are deeper ++ - And the closing quote is too deep + """ pass def single_line(): - """But with a newline after it!""" -+ "NOT_YET_IMPLEMENTED_STRING" ++ """But with a newline after it! ++ ++ """ pass - def this(): -- r""" -- 'hey ho' -- """ -+ "NOT_YET_IMPLEMENTED_STRING" - - - def that(): -- """ "hey yah" """ -+ "NOT_YET_IMPLEMENTED_STRING" - +@@ -93,20 +95,25 @@ def and_that(): -- """ + """ - "hey yah" """ -+ "NOT_YET_IMPLEMENTED_STRING" ++ "hey yah" """ def and_this(): - ''' - "hey yah"''' -+ "NOT_YET_IMPLEMENTED_STRING" ++ ''' ++ "hey yah"''' def multiline_whitespace(): - """ """ -+ "NOT_YET_IMPLEMENTED_STRING" ++ """ ++ ++ ++ ++ ++ """ def oneline_whitespace(): - """ """ -+ "NOT_YET_IMPLEMENTED_STRING" ++ """ """ def empty(): -- """""" -+ "NOT_YET_IMPLEMENTED_STRING" - - - def single_quotes(): -- "testing" -+ "NOT_YET_IMPLEMENTED_STRING" +@@ -118,8 +125,8 @@ def believe_it_or_not_this_is_in_the_py_stdlib(): - ''' - "hey yah"''' -+ "NOT_YET_IMPLEMENTED_STRING" ++ ''' ++"hey yah"''' def ignored_docstring(): -- """a => \ --b""" -+ "NOT_YET_IMPLEMENTED_STRING" +@@ -128,31 +135,31 @@ def single_line_docstring_with_whitespace(): - """This should be stripped""" -+ "NOT_YET_IMPLEMENTED_STRING" ++ """ This should be stripped """ def docstring_with_inline_tabs_and_space_indentation(): -- """hey -- -- tab separated value + """hey + + tab separated value - tab at start of line and then a tab separated value - multiple tabs at the beginning and inline - mixed tabs and spaces at beginning. next line has mixed tabs and spaces only. - - line ends with some tabs -- """ -+ "NOT_YET_IMPLEMENTED_STRING" ++ tab at start of line and then a tab separated value ++ multiple tabs at the beginning and inline ++ mixed tabs and spaces at beginning. next line has mixed tabs and spaces only. ++ ++ line ends with some tabs + """ def docstring_with_inline_tabs_and_tab_indentation(): -- """hey -- + """hey + - tab separated value - tab at start of line and then a tab separated value - multiple tabs at the beginning and inline @@ -412,237 +427,274 @@ def stable_quote_normalization_with_immediate_inner_single_quote(self): - - line ends with some tabs - """ -+ "NOT_YET_IMPLEMENTED_STRING" ++ tab separated value ++ tab at start of line and then a tab separated value ++ multiple tabs at the beginning and inline ++ mixed tabs and spaces at beginning. next line has mixed tabs and spaces only. ++ ++ line ends with some tabs ++ """ pass - def backslash_space(): -- """\ """ -+ "NOT_YET_IMPLEMENTED_STRING" - - - def multiline_backslash_1(): -- """ -- hey\there\ -- \ """ -+ "NOT_YET_IMPLEMENTED_STRING" - +@@ -168,7 +175,7 @@ def multiline_backslash_2(): -- """ + """ - hey there \ """ -+ "NOT_YET_IMPLEMENTED_STRING" ++ hey there \ """ # Regression test for #3425 - def multiline_backslash_really_long_dont_crash(): -- """ -- hey there hello guten tag hi hoow are you ola zdravstvuyte ciao como estas ca va \ """ -+ "NOT_YET_IMPLEMENTED_STRING" - +@@ -179,7 +186,7 @@ def multiline_backslash_3(): -- """ + """ - already escaped \\""" -+ "NOT_YET_IMPLEMENTED_STRING" ++ already escaped \\ """ def my_god_its_full_of_stars_1(): -- "I'm sorry Dave\u2001" -+ "NOT_YET_IMPLEMENTED_STRING" - +@@ -188,7 +195,7 @@ # the space below is actually a \u2001, removed in output def my_god_its_full_of_stars_2(): - "I'm sorry Dave" -+ "NOT_YET_IMPLEMENTED_STRING" ++ "I'm sorry Dave " def docstring_almost_at_line_limit(): -- """long docstring.................................................................""" -+ "NOT_YET_IMPLEMENTED_STRING" - - - def docstring_almost_at_line_limit2(): -- """long docstring................................................................. -- -- .................................................................................. -- """ -+ "NOT_YET_IMPLEMENTED_STRING" - - - def docstring_at_line_limit(): -- """long docstring................................................................""" -+ "NOT_YET_IMPLEMENTED_STRING" - - - def multiline_docstring_at_line_limit(): -- """first line----------------------------------------------------------------------- -- -- second line----------------------------------------------------------------------""" -+ "NOT_YET_IMPLEMENTED_STRING" - - - def stable_quote_normalization_with_immediate_inner_single_quote(self): -- """' -- -- -- """ -+ "NOT_YET_IMPLEMENTED_STRING" ``` ## Ruff Output ```py -NOT_YET_IMPLEMENTED_StmtClassDef +class MyClass: + """ Multiline + class docstring + """ + + def method(self): + """Multiline + method docstring + """ + pass def foo(): - "NOT_YET_IMPLEMENTED_STRING" + """This is a docstring with + some lines of text here + """ return def bar(): - "NOT_YET_IMPLEMENTED_STRING" + """This is another docstring + with more lines of text + """ return def baz(): - "NOT_YET_IMPLEMENTED_STRING" + '''"This" is a string with some + embedded "quotes"''' return def troz(): - "NOT_YET_IMPLEMENTED_STRING" + """Indentation with tabs + is just as OK + """ return def zort(): - "NOT_YET_IMPLEMENTED_STRING" + """Another + multiline + docstring + """ pass def poit(): - "NOT_YET_IMPLEMENTED_STRING" + """ + Lorem ipsum dolor sit amet. + + Consectetur adipiscing elit: + - sed do eiusmod tempor incididunt ut labore + - dolore magna aliqua + - enim ad minim veniam + - quis nostrud exercitation ullamco laboris nisi + - aliquip ex ea commodo consequat + """ pass def under_indent(): - "NOT_YET_IMPLEMENTED_STRING" + """ + These lines are indented in a way that does not +make sense. + """ pass def over_indent(): - "NOT_YET_IMPLEMENTED_STRING" + """ + This has a shallow indent + - But some lines are deeper + - And the closing quote is too deep + """ pass def single_line(): - "NOT_YET_IMPLEMENTED_STRING" + """But with a newline after it! + + """ pass def this(): - "NOT_YET_IMPLEMENTED_STRING" + r""" + 'hey ho' + """ def that(): - "NOT_YET_IMPLEMENTED_STRING" + """ "hey yah" """ def and_that(): - "NOT_YET_IMPLEMENTED_STRING" + """ + "hey yah" """ def and_this(): - "NOT_YET_IMPLEMENTED_STRING" + ''' + "hey yah"''' def multiline_whitespace(): - "NOT_YET_IMPLEMENTED_STRING" + """ + + + + + """ def oneline_whitespace(): - "NOT_YET_IMPLEMENTED_STRING" + """ """ def empty(): - "NOT_YET_IMPLEMENTED_STRING" + """""" def single_quotes(): - "NOT_YET_IMPLEMENTED_STRING" + "testing" def believe_it_or_not_this_is_in_the_py_stdlib(): - "NOT_YET_IMPLEMENTED_STRING" + ''' +"hey yah"''' def ignored_docstring(): - "NOT_YET_IMPLEMENTED_STRING" + """a => \ +b""" def single_line_docstring_with_whitespace(): - "NOT_YET_IMPLEMENTED_STRING" + """ This should be stripped """ def docstring_with_inline_tabs_and_space_indentation(): - "NOT_YET_IMPLEMENTED_STRING" + """hey + + tab separated value + tab at start of line and then a tab separated value + multiple tabs at the beginning and inline + mixed tabs and spaces at beginning. next line has mixed tabs and spaces only. + + line ends with some tabs + """ def docstring_with_inline_tabs_and_tab_indentation(): - "NOT_YET_IMPLEMENTED_STRING" + """hey + + tab separated value + tab at start of line and then a tab separated value + multiple tabs at the beginning and inline + mixed tabs and spaces at beginning. next line has mixed tabs and spaces only. + + line ends with some tabs + """ pass def backslash_space(): - "NOT_YET_IMPLEMENTED_STRING" + """\ """ def multiline_backslash_1(): - "NOT_YET_IMPLEMENTED_STRING" + """ + hey\there\ + \ """ def multiline_backslash_2(): - "NOT_YET_IMPLEMENTED_STRING" + """ + hey there \ """ # Regression test for #3425 def multiline_backslash_really_long_dont_crash(): - "NOT_YET_IMPLEMENTED_STRING" + """ + hey there hello guten tag hi hoow are you ola zdravstvuyte ciao como estas ca va \ """ def multiline_backslash_3(): - "NOT_YET_IMPLEMENTED_STRING" + """ + already escaped \\ """ def my_god_its_full_of_stars_1(): - "NOT_YET_IMPLEMENTED_STRING" + "I'm sorry Dave\u2001" # the space below is actually a \u2001, removed in output def my_god_its_full_of_stars_2(): - "NOT_YET_IMPLEMENTED_STRING" + "I'm sorry Dave " def docstring_almost_at_line_limit(): - "NOT_YET_IMPLEMENTED_STRING" + """long docstring.................................................................""" def docstring_almost_at_line_limit2(): - "NOT_YET_IMPLEMENTED_STRING" + """long docstring................................................................. + + .................................................................................. + """ def docstring_at_line_limit(): - "NOT_YET_IMPLEMENTED_STRING" + """long docstring................................................................""" def multiline_docstring_at_line_limit(): - "NOT_YET_IMPLEMENTED_STRING" + """first line----------------------------------------------------------------------- + + second line----------------------------------------------------------------------""" def stable_quote_normalization_with_immediate_inner_single_quote(self): - "NOT_YET_IMPLEMENTED_STRING" + """' + + + """ ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_preview_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__docstring_preview.py.snap similarity index 78% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_preview_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__docstring_preview.py.snap index 08eea3e67f..d297799051 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_preview_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__docstring_preview.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/docstring_preview.py --- ## Input @@ -63,23 +62,20 @@ def single_quote_docstring_over_line_limit2(): ```diff --- Black +++ Ruff -@@ -1,48 +1,38 @@ +@@ -1,9 +1,10 @@ def docstring_almost_at_line_limit(): - """long docstring.................................................................""" -+ "NOT_YET_IMPLEMENTED_STRING" ++ """long docstring................................................................. ++ """ def docstring_almost_at_line_limit_with_prefix(): - f"""long docstring................................................................""" -+ NOT_YET_IMPLEMENTED_ExprJoinedStr ++ f"NOT_YET_IMPLEMENTED_ExprJoinedStr" def mulitline_docstring_almost_at_line_limit(): -- """long docstring................................................................. -- -- .................................................................................. -- """ -+ "NOT_YET_IMPLEMENTED_STRING" +@@ -14,10 +15,7 @@ def mulitline_docstring_almost_at_line_limit_with_prefix(): @@ -87,84 +83,79 @@ def single_quote_docstring_over_line_limit2(): - - .................................................................................. - """ -+ NOT_YET_IMPLEMENTED_ExprJoinedStr ++ f"NOT_YET_IMPLEMENTED_ExprJoinedStr" def docstring_at_line_limit(): -- """long docstring................................................................""" -+ "NOT_YET_IMPLEMENTED_STRING" +@@ -25,7 +23,7 @@ def docstring_at_line_limit_with_prefix(): - f"""long docstring...............................................................""" -+ NOT_YET_IMPLEMENTED_ExprJoinedStr ++ f"NOT_YET_IMPLEMENTED_ExprJoinedStr" def multiline_docstring_at_line_limit(): -- """first line----------------------------------------------------------------------- -- -- second line----------------------------------------------------------------------""" -+ "NOT_YET_IMPLEMENTED_STRING" +@@ -35,9 +33,7 @@ def multiline_docstring_at_line_limit_with_prefix(): - f"""first line---------------------------------------------------------------------- - - second line----------------------------------------------------------------------""" -+ NOT_YET_IMPLEMENTED_ExprJoinedStr ++ f"NOT_YET_IMPLEMENTED_ExprJoinedStr" def single_quote_docstring_over_line_limit(): -- "We do not want to put the closing quote on a new line as that is invalid (see GH-3141)." -+ "NOT_YET_IMPLEMENTED_STRING" - - - def single_quote_docstring_over_line_limit2(): -- "We do not want to put the closing quote on a new line as that is invalid (see GH-3141)." -+ "NOT_YET_IMPLEMENTED_STRING" ``` ## Ruff Output ```py def docstring_almost_at_line_limit(): - "NOT_YET_IMPLEMENTED_STRING" + """long docstring................................................................. + """ def docstring_almost_at_line_limit_with_prefix(): - NOT_YET_IMPLEMENTED_ExprJoinedStr + f"NOT_YET_IMPLEMENTED_ExprJoinedStr" def mulitline_docstring_almost_at_line_limit(): - "NOT_YET_IMPLEMENTED_STRING" + """long docstring................................................................. + + .................................................................................. + """ def mulitline_docstring_almost_at_line_limit_with_prefix(): - NOT_YET_IMPLEMENTED_ExprJoinedStr + f"NOT_YET_IMPLEMENTED_ExprJoinedStr" def docstring_at_line_limit(): - "NOT_YET_IMPLEMENTED_STRING" + """long docstring................................................................""" def docstring_at_line_limit_with_prefix(): - NOT_YET_IMPLEMENTED_ExprJoinedStr + f"NOT_YET_IMPLEMENTED_ExprJoinedStr" def multiline_docstring_at_line_limit(): - "NOT_YET_IMPLEMENTED_STRING" + """first line----------------------------------------------------------------------- + + second line----------------------------------------------------------------------""" def multiline_docstring_at_line_limit_with_prefix(): - NOT_YET_IMPLEMENTED_ExprJoinedStr + f"NOT_YET_IMPLEMENTED_ExprJoinedStr" def single_quote_docstring_over_line_limit(): - "NOT_YET_IMPLEMENTED_STRING" + "We do not want to put the closing quote on a new line as that is invalid (see GH-3141)." def single_quote_docstring_over_line_limit2(): - "NOT_YET_IMPLEMENTED_STRING" + "We do not want to put the closing quote on a new line as that is invalid (see GH-3141)." ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__empty_lines_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__empty_lines.py.snap similarity index 54% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__empty_lines_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__empty_lines.py.snap index 5446adf8cc..4cfde0d1a6 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__empty_lines_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__empty_lines.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/empty_lines.py --- ## Input @@ -105,166 +104,81 @@ def g(): ```diff --- Black +++ Ruff -@@ -1,89 +1,70 @@ --"""Docstring.""" -+"NOT_YET_IMPLEMENTED_STRING" - - - # leading comment - def f(): -- NO = "" -- SPACE = " " -- DOUBLESPACE = " " -+ NO = "NOT_YET_IMPLEMENTED_STRING" -+ SPACE = "NOT_YET_IMPLEMENTED_STRING" -+ DOUBLESPACE = "NOT_YET_IMPLEMENTED_STRING" - -- t = leaf.type -- p = leaf.parent # trailing comment -- v = leaf.value -+ t = leaf.NOT_IMPLEMENTED_attr -+ p = leaf.NOT_IMPLEMENTED_attr # trailing comment -+ v = leaf.NOT_IMPLEMENTED_attr - -- if t in ALWAYS_NO_SPACE: -+ if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: - pass -- if t == token.COMMENT: # another trailing comment -+ if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: # another trailing comment +@@ -16,7 +16,7 @@ + if t == token.COMMENT: # another trailing comment return DOUBLESPACE - assert p is not None, f"INTERNAL ERROR: hand-made leaf without parent: {leaf!r}" + NOT_YET_IMPLEMENTED_StmtAssert -- prev = leaf.prev_sibling -- if not prev: -- prevp = preceding_leaf(p) -- if not prevp or prevp.type in OPENING_BRACKETS: -+ prev = leaf.NOT_IMPLEMENTED_attr -+ if NOT_YET_IMPLEMENTED_ExprUnaryOp: -+ prevp = NOT_IMPLEMENTED_call() -+ if NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: - return NO - -- if prevp.type == token.EQUAL: -- if prevp.parent and prevp.parent.type in { -- syms.typedargslist, -- syms.varargslist, -- syms.parameters, -- syms.arglist, -- syms.argument, -- }: -+ if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: -+ if NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: - return NO - -- elif prevp.type == token.DOUBLESTAR: -- if prevp.parent and prevp.parent.type in { -- syms.typedargslist, -- syms.varargslist, -- syms.parameters, -- syms.arglist, -- syms.dictsetmaker, -- }: -+ elif NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: -+ if NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: - return NO - - + prev = leaf.prev_sibling + if not prev: +@@ -48,7 +48,6 @@ ############################################################################### # SECTION BECAUSE SECTIONS ############################################################################### - def g(): -- NO = "" -- SPACE = " " -- DOUBLESPACE = " " -+ NO = "NOT_YET_IMPLEMENTED_STRING" -+ SPACE = "NOT_YET_IMPLEMENTED_STRING" -+ DOUBLESPACE = "NOT_YET_IMPLEMENTED_STRING" - -- t = leaf.type -- p = leaf.parent -- v = leaf.value -+ t = leaf.NOT_IMPLEMENTED_attr -+ p = leaf.NOT_IMPLEMENTED_attr -+ v = leaf.NOT_IMPLEMENTED_attr - - # Comment because comments - -- if t in ALWAYS_NO_SPACE: -+ if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: - pass -- if t == token.COMMENT: -+ if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: + NO = "" +@@ -67,7 +66,7 @@ return DOUBLESPACE # Another comment because more comments - assert p is not None, f"INTERNAL ERROR: hand-made leaf without parent: {leaf!r}" + NOT_YET_IMPLEMENTED_StmtAssert -- prev = leaf.prev_sibling -- if not prev: -- prevp = preceding_leaf(p) -+ prev = leaf.NOT_IMPLEMENTED_attr -+ if NOT_YET_IMPLEMENTED_ExprUnaryOp: -+ prevp = NOT_IMPLEMENTED_call() - -- if not prevp or prevp.type in OPENING_BRACKETS: -+ if NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: - # Start of the line or a bracketed expression. - # More than one line for the comment. - return NO - -- if prevp.type == token.EQUAL: -- if prevp.parent and prevp.parent.type in { -- syms.typedargslist, -- syms.varargslist, -- syms.parameters, -- syms.arglist, -- syms.argument, -- }: -+ if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: -+ if NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: - return NO + prev = leaf.prev_sibling + if not prev: ``` ## Ruff Output ```py -"NOT_YET_IMPLEMENTED_STRING" +"""Docstring.""" # leading comment def f(): - NO = "NOT_YET_IMPLEMENTED_STRING" - SPACE = "NOT_YET_IMPLEMENTED_STRING" - DOUBLESPACE = "NOT_YET_IMPLEMENTED_STRING" + NO = "" + SPACE = " " + DOUBLESPACE = " " - t = leaf.NOT_IMPLEMENTED_attr - p = leaf.NOT_IMPLEMENTED_attr # trailing comment - v = leaf.NOT_IMPLEMENTED_attr + t = leaf.type + p = leaf.parent # trailing comment + v = leaf.value - if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: + if t in ALWAYS_NO_SPACE: pass - if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: # another trailing comment + if t == token.COMMENT: # another trailing comment return DOUBLESPACE NOT_YET_IMPLEMENTED_StmtAssert - prev = leaf.NOT_IMPLEMENTED_attr - if NOT_YET_IMPLEMENTED_ExprUnaryOp: - prevp = NOT_IMPLEMENTED_call() - if NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: + prev = leaf.prev_sibling + if not prev: + prevp = preceding_leaf(p) + if not prevp or prevp.type in OPENING_BRACKETS: return NO - if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: - if NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: + if prevp.type == token.EQUAL: + if prevp.parent and prevp.parent.type in { + syms.typedargslist, + syms.varargslist, + syms.parameters, + syms.arglist, + syms.argument, + }: return NO - elif NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: - if NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: + elif prevp.type == token.DOUBLESTAR: + if prevp.parent and prevp.parent.type in { + syms.typedargslist, + syms.varargslist, + syms.parameters, + syms.arglist, + syms.dictsetmaker, + }: return NO @@ -273,35 +187,41 @@ def f(): ############################################################################### def g(): - NO = "NOT_YET_IMPLEMENTED_STRING" - SPACE = "NOT_YET_IMPLEMENTED_STRING" - DOUBLESPACE = "NOT_YET_IMPLEMENTED_STRING" + NO = "" + SPACE = " " + DOUBLESPACE = " " - t = leaf.NOT_IMPLEMENTED_attr - p = leaf.NOT_IMPLEMENTED_attr - v = leaf.NOT_IMPLEMENTED_attr + t = leaf.type + p = leaf.parent + v = leaf.value # Comment because comments - if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: + if t in ALWAYS_NO_SPACE: pass - if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: + if t == token.COMMENT: return DOUBLESPACE # Another comment because more comments NOT_YET_IMPLEMENTED_StmtAssert - prev = leaf.NOT_IMPLEMENTED_attr - if NOT_YET_IMPLEMENTED_ExprUnaryOp: - prevp = NOT_IMPLEMENTED_call() + prev = leaf.prev_sibling + if not prev: + prevp = preceding_leaf(p) - if NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: + if not prevp or prevp.type in OPENING_BRACKETS: # Start of the line or a bracketed expression. # More than one line for the comment. return NO - if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: - if NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: + if prevp.type == token.EQUAL: + if prevp.parent and prevp.parent.type in { + syms.typedargslist, + syms.varargslist, + syms.parameters, + syms.arglist, + syms.argument, + }: return NO ``` diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__expression.py.snap similarity index 52% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__expression.py.snap index 5733bbfdd7..ad164bd6c6 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__expression.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/expression.py --- ## Input @@ -267,56 +266,21 @@ last_call() ```diff --- Black +++ Ruff -@@ -1,5 +1,6 @@ --"some_string" +@@ -1,6 +1,6 @@ + ... + "some_string" -b"\\xa3" -+... -+"NOT_YET_IMPLEMENTED_STRING" +b"NOT_YET_IMPLEMENTED_BYTE_STRING" Name None True -@@ -7,294 +8,236 @@ - 1 - 1.0 - 1j --True or False --True or False or None --True and False --True and False and None --(Name1 and Name2) or Name3 --Name1 and Name2 or Name3 --Name1 or (Name2 and Name3) --Name1 or Name2 and Name3 --(Name1 and Name2) or (Name3 and Name4) --Name1 and Name2 or Name3 and Name4 --Name1 or (Name2 and Name3) or Name4 --Name1 or Name2 and Name3 or Name4 -+NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -+NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -+NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -+NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -+NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -+NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -+NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -+NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -+NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -+NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -+NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -+NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 - v1 << 2 - 1 >> v2 - 1 % finished - 1 + v2 - v3 * 4 ^ 5**v6 / 7 // 8 - ((1 + v2) - (v3 * 4)) ^ (((5**v6) / 7) // 8) --not great --~great --+value ---1 --~int and not v1 ^ 123 + v2 | True --(~int) and (not ((v1 ^ (123 + v2)) | True)) +@@ -31,18 +31,15 @@ + -1 + ~int and not v1 ^ 123 + v2 | True + (~int) and (not ((v1 ^ (123 + v2)) | True)) -+(really ** -(confusing ** ~(operator**-precedence))) --flags & ~select.EPOLLIN and waiters.write_task is not None +++really ** -confusing ** ~operator**-precedence + flags & ~select.EPOLLIN and waiters.write_task is not None -lambda arg: None -lambda a=True: a -lambda a, b, c=True: a @@ -327,325 +291,118 @@ last_call() - "port1": port1_resource, - "port2": port2_resource, -}[port_id] --1 if True else 2 --str or None if True else str or bytes or None --(str or None) if True else (str or bytes or None) --str or None if (1 if True else 2) else str or bytes or None --(str or None) if (1 if True else 2) else (str or bytes or None) --( -- (super_long_variable_name or None) -- if (1 if super_long_test_name else 2) -- else (str or bytes or None) --) --{"2.7": dead, "3.7": (long_live or die_hard)} --{"2.7": dead, "3.7": (long_live or die_hard), **{"3.6": verygood}} --{**a, **b, **c} --{"2.7", "3.6", "3.7", "3.8", "3.9", ("4.0" if gilectomy else "3.10")} ++lambda NOT_YET_IMPLEMENTED_lambda: True ++lambda NOT_YET_IMPLEMENTED_lambda: True ++lambda NOT_YET_IMPLEMENTED_lambda: True ++lambda NOT_YET_IMPLEMENTED_lambda: True ++lambda NOT_YET_IMPLEMENTED_lambda: True ++manylambdas = lambda NOT_YET_IMPLEMENTED_lambda: True ++foo = lambda NOT_YET_IMPLEMENTED_lambda: True + 1 if True else 2 + str or None if True else str or bytes or None + (str or None) if True else (str or bytes or None) +@@ -57,7 +54,13 @@ + {"2.7": dead, "3.7": (long_live or die_hard), **{"3.6": verygood}} + {**a, **b, **c} + {"2.7", "3.6", "3.7", "3.8", "3.9", ("4.0" if gilectomy else "3.10")} -({"a": "b"}, (True or False), (+value), "string", b"bytes") or None --() --(1,) -+NOT_YET_IMPLEMENTED_ExprUnaryOp -+NOT_YET_IMPLEMENTED_ExprUnaryOp -+NOT_YET_IMPLEMENTED_ExprUnaryOp -+NOT_YET_IMPLEMENTED_ExprUnaryOp -+NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -+NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -+NOT_YET_IMPLEMENTED_ExprUnaryOp -+NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -+lambda x: True -+lambda x: True -+lambda x: True -+lambda x: True -+lambda x: True -+manylambdas = lambda x: True -+foo = lambda x: True -+NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false -+NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false -+NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false -+NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false -+NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false -+(NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false) -+{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} -+{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} -+{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} -+{ -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ (NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false), -+} -+NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -+(1, 2) -+(1, 2) -+(1, 2) ++( ++ {"a": "b"}, ++ (True or False), ++ (+value), ++ "string", ++ b"NOT_YET_IMPLEMENTED_BYTE_STRING", ++) or None + () + (1,) (1, 2) --(1, 2, 3) - [] --[1, 2, 3, 4, 5, 6, 7, 8, 9, (10 or A), (11 or B), (12 or C)] - [ - 1, - 2, - 3, --] --[*a] --[*range(10)] --[ -- *a, - 4, - 5, --] --[ -- 4, -- *a, -- 5, -+ 6, -+ 7, -+ 8, -+ 9, -+ (NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2), -+ (NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2), -+ (NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2), - ] -+[1, 2, 3] -+[NOT_YET_IMPLEMENTED_ExprStarred] -+[NOT_YET_IMPLEMENTED_ExprStarred] -+[NOT_YET_IMPLEMENTED_ExprStarred, 4, 5] -+[4, NOT_YET_IMPLEMENTED_ExprStarred, 5] - [ - this_is_a_very_long_variable_which_will_force_a_delimiter_split, - element, +@@ -87,22 +90,19 @@ another, -- *more, -+ NOT_YET_IMPLEMENTED_ExprStarred, + *more, ] -{i for i in (1, 2, 3)} -{(i**2) for i in (1, 2, 3)} -{(i**2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))} -{((i**2) + j) for i in (1, 2, 3) for j in (1, 2, 3)} --[i for i in (1, 2, 3)] --[(i**2) for i in (1, 2, 3)] --[(i**2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))] --[((i**2) + j) for i in (1, 2, 3) for j in (1, 2, 3)] ++{NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set} ++{NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set} ++{NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set} ++{NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set} + [i for i in (1, 2, 3)] + [(i**2) for i in (1, 2, 3)] + [(i**2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))] + [((i**2) + j) for i in (1, 2, 3) for j in (1, 2, 3)] -{i: 0 for i in (1, 2, 3)} -{i: j for i, j in ((1, "a"), (2, "b"), (3, "c"))} -{a: b * 2 for a, b in dictionary.items()} -{a: b * -2 for a, b in dictionary.items()} -+NOT_YET_IMPLEMENTED_ExprSetComp -+NOT_YET_IMPLEMENTED_ExprSetComp -+NOT_YET_IMPLEMENTED_ExprSetComp -+NOT_YET_IMPLEMENTED_ExprSetComp -+[i for i in []] -+[i for i in []] -+[i for i in []] -+[i for i in []] -+{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} -+{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} -+{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} -+{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} -+{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} -+NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right -+NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right -+NOT_IMPLEMENTED_call() -+NOT_IMPLEMENTED_call() -+NOT_IMPLEMENTED_call() -+NOT_IMPLEMENTED_call() -+NOT_IMPLEMENTED_call() -+NOT_IMPLEMENTED_call() # note: no trailing comma pre-3.6 -+NOT_IMPLEMENTED_call() -+NOT_IMPLEMENTED_call() -+NOT_IMPLEMENTED_call() -+NOT_IMPLEMENTED_call() -+lukasz.NOT_IMPLEMENTED_attr.NOT_IMPLEMENTED_attr -+NOT_IMPLEMENTED_call() -+1 .NOT_IMPLEMENTED_attr -+1.0 .NOT_IMPLEMENTED_attr -+....NOT_IMPLEMENTED_attr -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_YET_IMPLEMENTED_StmtAnnAssign -+NOT_YET_IMPLEMENTED_StmtAnnAssign -+NOT_YET_IMPLEMENTED_StmtAnnAssign -+NOT_YET_IMPLEMENTED_StmtAnnAssign # type: ignore -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false -+{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} - { +-{ - k: v - for k, v in this_is_a_very_long_variable_which_will_cause_a_trailing_comma_which_breaks_the_comprehension -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, - } --Python3 > Python2 > COBOL --Life is Life --call() --call(arg) --call(kwarg="hey") --call(arg, kwarg="hey") --call(arg, another, kwarg="hey", **kwargs) --call( -- this_is_a_very_long_variable_which_will_force_a_delimiter_split, -- arg, -- another, -- kwarg="hey", -- **kwargs, --) # note: no trailing comma pre-3.6 --call(*gidgets[:2]) --call(a, *gidgets[:2]) --call(**self.screen_kwargs) --call(b, **self.screen_kwargs) --lukasz.langa.pl --call.me(maybe) --(1).real --(1.0).real --....__class__ --list[str] --dict[str, int] --tuple[str, ...] --tuple[str, int, float, dict[str, int]] --tuple[ -- str, -- int, -- float, -- dict[str, int], -+[ -+ 1, -+ 2, -+ 3, -+ 4, -+ 5, -+ 6, -+ 7, -+ 8, -+ 9, -+ NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2, -+ NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2, -+ NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2, - ] --very_long_variable_name_filters: t.List[ -- t.Tuple[str, t.Union[str, t.List[t.Optional[str]]]], --] --xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod( # type: ignore -- sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__) --) --xxxx_xxx_xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod( # type: ignore -- sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__) --) --xxxx_xxx_xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod( -- sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__) --) # type: ignore --slice[0] --slice[0:1] --slice[0:1:2] --slice[:] +-} ++{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} ++{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} ++{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} ++{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} ++{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} + Python3 > Python2 > COBOL + Life is Life + call() +@@ -115,7 +115,7 @@ + arg, + another, + kwarg="hey", +- **kwargs ++ **kwargs, + ) # note: no trailing comma pre-3.6 + call(*gidgets[:2]) + call(a, *gidgets[:2]) +@@ -152,13 +152,13 @@ + slice[0:1] + slice[0:1:2] + slice[:] -slice[:-1] --slice[1:] ++slice[ : -1] + slice[1:] -slice[::-1] --slice[d :: d + 1] --slice[:c, c - 1] --numpy[:, 0:1] ++slice[ :: -1] + slice[d :: d + 1] + slice[:c, c - 1] + numpy[:, 0:1] -numpy[:, :-1] --numpy[0, :] --numpy[:, i] --numpy[0, :2] --numpy[:N, 0] --numpy[:2, :4] --numpy[2:4, 1:5] --numpy[4:, 2:] --numpy[:, (0, 1, 2, 5)] --numpy[0, [0]] --numpy[:, [i]] --numpy[1 : c + 1, c] --numpy[-(c + 1) :, d] --numpy[:, l[-2]] ++numpy[:, : -1] + numpy[0, :] + numpy[:, i] + numpy[0, :2] +@@ -172,7 +172,7 @@ + numpy[1 : c + 1, c] + numpy[-(c + 1) :, d] + numpy[:, l[-2]] -numpy[:, ::-1] --numpy[np.newaxis, :] --(str or None) if (sys.version_info[0] > (3,)) else (str or bytes or None) --{"2.7": dead, "3.7": long_live or die_hard} --{"2.7", "3.6", "3.7", "3.8", "3.9", "4.0" if gilectomy else "3.10"} --[1, 2, 3, 4, 5, 6, 7, 8, 9, 10 or A, 11 or B, 12 or C] ++numpy[:, :: -1] + numpy[np.newaxis, :] + (str or None) if (sys.version_info[0] > (3,)) else (str or bytes or None) + {"2.7": dead, "3.7": long_live or die_hard} +@@ -181,10 +181,10 @@ (SomeName) SomeName --(Good, Bad, Ugly) + (Good, Bad, Ugly) -(i for i in (1, 2, 3)) -((i**2) for i in (1, 2, 3)) -((i**2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))) -(((i**2) + j) for i in (1, 2, 3) for j in (1, 2, 3)) --(*starred,) --{ -- "id": "1", -- "type": "type", -- "started_at": now(), -- "ended_at": now() + timedelta(days=10), -- "priority": 1, -- "import_session_id": 1, -- **kwargs, --} --a = (1,) --b = (1,) -+(1, 2) -+(i for i in []) -+(i for i in []) -+(i for i in []) -+(i for i in []) -+(1, 2) -+{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} -+a = (1, 2) -+b = (1, 2) - c = 1 --d = (1,) + a + (2,) --e = (1,).count(1) --f = 1, *range(10) --g = 1, *"ten" --what_is_up_with_those_new_coord_names = (coord_names + set(vars_to_create)) + set( -- vars_to_remove -+d = (1, 2) + a + (1, 2) -+e = NOT_IMPLEMENTED_call() -+f = (1, 2) -+g = (1, 2) -+what_is_up_with_those_new_coord_names = ( -+ (coord_names + NOT_IMPLEMENTED_call()) -+ + NOT_IMPLEMENTED_call() - ) --what_is_up_with_those_new_coord_names = (coord_names | set(vars_to_create)) - set( -- vars_to_remove -+what_is_up_with_those_new_coord_names = ( -+ (coord_names | NOT_IMPLEMENTED_call()) -+ - NOT_IMPLEMENTED_call() ++(NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) ++(NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) ++(NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) ++(NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) + (*starred,) + { + "id": "1", +@@ -207,25 +207,15 @@ ) + what_is_up_with_those_new_coord_names = (coord_names | set(vars_to_create)) - set( + vars_to_remove +-) -result = ( - session.query(models.Customer.id) - .filter( @@ -653,7 +410,7 @@ last_call() - ) - .order_by(models.Customer.id.asc()) - .all() --) + ) -result = ( - session.query(models.Customer.id) - .filter( @@ -664,19 +421,18 @@ last_call() - ) - .all() -) --Ø = set() --authors.łukasz.say_thanks() --mapping = { -- A: 0.25 * (10.0 / 12), -- B: 0.1 * (10.0 / 12), -- C: 0.1 * (10.0 / 12), -- D: 0.1 * (10.0 / 12), --} -+result = NOT_IMPLEMENTED_call() -+result = NOT_IMPLEMENTED_call() -+Ø = NOT_IMPLEMENTED_call() -+NOT_IMPLEMENTED_call() -+mapping = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} ++result = session.query(models.Customer.id).filter( ++ models.Customer.account_id == account_id, models.Customer.email == email_address ++).order_by(models.Customer.id.asc()).all() ++result = session.query(models.Customer.id).filter( ++ models.Customer.account_id == account_id, models.Customer.email == email_address ++).order_by( ++ models.Customer.id.asc(), ++).all() + Ø = set() + authors.łukasz.say_thanks() + mapping = { +@@ -237,10 +227,10 @@ def gen(): @@ -691,11 +447,10 @@ last_call() async def f(): -- await some.complicated[0].call(with_args=(True or (1 is not 1))) -+ await NOT_IMPLEMENTED_call() +@@ -248,18 +238,20 @@ --print(*[] or [1]) + print(*[] or [1]) -print(**{1: 3} if False else {x: x for x in range(3)}) -print(*lambda x: x) -assert not Test, "Short message" @@ -703,144 +458,63 @@ last_call() - force=False -), "Short message" -assert parens is TooMany --for (x,) in (1,), (2,), (3,): -- ... --for y in (): -- ... --for z in (i for i in (1, 2, 3)): -- ... --for i in call(): -- ... --for j in 1 + (2 + 3): -- ... --while this and that: -+NOT_IMPLEMENTED_call() -+NOT_IMPLEMENTED_call() -+NOT_IMPLEMENTED_call() ++print( ++ **{1: 3} ++ if False ++ else {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} ++) ++print(*lambda NOT_YET_IMPLEMENTED_lambda: True) +NOT_YET_IMPLEMENTED_StmtAssert +NOT_YET_IMPLEMENTED_StmtAssert +NOT_YET_IMPLEMENTED_StmtAssert -+NOT_YET_IMPLEMENTED_StmtFor -+NOT_YET_IMPLEMENTED_StmtFor -+NOT_YET_IMPLEMENTED_StmtFor -+NOT_YET_IMPLEMENTED_StmtFor -+NOT_YET_IMPLEMENTED_StmtFor -+while NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: + for (x,) in (1,), (2,), (3,): ... --for ( -- addr_family, -- addr_type, -- addr_proto, -- addr_canonname, -- addr_sockaddr, --) in socket.getaddrinfo("google.com", "http"): -- pass --a = ( -- aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp -- in qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz --) --a = ( -- aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp -- not in qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz --) --a = ( -- aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp -- is qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz --) --a = ( -- aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp -- is not qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz --) --if ( -- threading.current_thread() != threading.main_thread() -- and threading.current_thread() != threading.main_thread() -- or signal.getsignal(signal.SIGINT) != signal.default_int_handler --): -+NOT_YET_IMPLEMENTED_StmtFor -+a = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right -+a = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right -+a = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right -+a = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right -+if NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: - return True - if ( - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -@@ -327,24 +270,44 @@ + for y in (): + ... +-for z in (i for i in (1, 2, 3)): ++for z in (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []): + ... + for i in call(): + ... +@@ -328,13 +320,18 @@ ): return True if ( - ~aaaa.a + aaaa.b - aaaa.c * aaaa.d / aaaa.e -- | aaaa.f & aaaa.g % aaaa.h ^ aaaa.i << aaaa.k >> aaaa.l**aaaa.m // aaaa.n -+ NOT_YET_IMPLEMENTED_ExprUnaryOp -+ + aaaa.NOT_IMPLEMENTED_attr -+ - aaaa.NOT_IMPLEMENTED_attr * aaaa.NOT_IMPLEMENTED_attr / aaaa.NOT_IMPLEMENTED_attr -+ | aaaa.NOT_IMPLEMENTED_attr -+ & aaaa.NOT_IMPLEMENTED_attr % aaaa.NOT_IMPLEMENTED_attr -+ ^ aaaa.NOT_IMPLEMENTED_attr -+ << aaaa.NOT_IMPLEMENTED_attr -+ >> aaaa.NOT_IMPLEMENTED_attr**aaaa.NOT_IMPLEMENTED_attr // aaaa.NOT_IMPLEMENTED_attr ++ ~aaaa.a ++ + aaaa.b ++ - aaaa.c * aaaa.d / aaaa.e + | aaaa.f & aaaa.g % aaaa.h ^ aaaa.i << aaaa.k >> aaaa.l**aaaa.m // aaaa.n ): return True if ( - ~aaaaaaaa.a + aaaaaaaa.b - aaaaaaaa.c @ aaaaaaaa.d / aaaaaaaa.e - | aaaaaaaa.f & aaaaaaaa.g % aaaaaaaa.h -- ^ aaaaaaaa.i << aaaaaaaa.k >> aaaaaaaa.l**aaaaaaaa.m // aaaaaaaa.n -+ NOT_YET_IMPLEMENTED_ExprUnaryOp -+ + aaaaaaaa.NOT_IMPLEMENTED_attr -+ - aaaaaaaa.NOT_IMPLEMENTED_attr -+ @ aaaaaaaa.NOT_IMPLEMENTED_attr -+ / aaaaaaaa.NOT_IMPLEMENTED_attr -+ | aaaaaaaa.NOT_IMPLEMENTED_attr -+ & aaaaaaaa.NOT_IMPLEMENTED_attr % aaaaaaaa.NOT_IMPLEMENTED_attr -+ ^ aaaaaaaa.NOT_IMPLEMENTED_attr -+ << aaaaaaaa.NOT_IMPLEMENTED_attr -+ >> aaaaaaaa.NOT_IMPLEMENTED_attr -+ **aaaaaaaa.NOT_IMPLEMENTED_attr -+ // aaaaaaaa.NOT_IMPLEMENTED_attr ++ ~aaaaaaaa.a ++ + aaaaaaaa.b ++ - aaaaaaaa.c @ aaaaaaaa.d / aaaaaaaa.e ++ | aaaaaaaa.f ++ & aaaaaaaa.g % aaaaaaaa.h + ^ aaaaaaaa.i << aaaaaaaa.k >> aaaaaaaa.l**aaaaaaaa.m // aaaaaaaa.n ): return True - if ( -- ~aaaaaaaaaaaaaaaa.a -- + aaaaaaaaaaaaaaaa.b -- - aaaaaaaaaaaaaaaa.c * aaaaaaaaaaaaaaaa.d @ aaaaaaaaaaaaaaaa.e +@@ -342,7 +339,8 @@ + ~aaaaaaaaaaaaaaaa.a + + aaaaaaaaaaaaaaaa.b + - aaaaaaaaaaaaaaaa.c * aaaaaaaaaaaaaaaa.d @ aaaaaaaaaaaaaaaa.e - | aaaaaaaaaaaaaaaa.f & aaaaaaaaaaaaaaaa.g % aaaaaaaaaaaaaaaa.h -- ^ aaaaaaaaaaaaaaaa.i -- << aaaaaaaaaaaaaaaa.k -- >> aaaaaaaaaaaaaaaa.l**aaaaaaaaaaaaaaaa.m // aaaaaaaaaaaaaaaa.n -+ NOT_YET_IMPLEMENTED_ExprUnaryOp -+ + aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr -+ - aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr -+ * aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr -+ @ aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr -+ | aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr -+ & aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr % aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr -+ ^ aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr -+ << aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr -+ >> aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr -+ **aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr -+ // aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr - ): - return True - ( -@@ -363,8 +326,9 @@ - bbbb >> bbbb * bbbb - ( - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -- ^ bbbb.a & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -+ ^ bbbb.NOT_IMPLEMENTED_attr -+ & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - ^ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - ) --last_call() -+NOT_IMPLEMENTED_call() - # standalone comment at ENDMARKER ++ | aaaaaaaaaaaaaaaa.f ++ & aaaaaaaaaaaaaaaa.g % aaaaaaaaaaaaaaaa.h + ^ aaaaaaaaaaaaaaaa.i + << aaaaaaaaaaaaaaaa.k + >> aaaaaaaaaaaaaaaa.l**aaaaaaaaaaaaaaaa.m // aaaaaaaaaaaaaaaa.n ``` ## Ruff Output ```py ... -"NOT_YET_IMPLEMENTED_STRING" +"some_string" b"NOT_YET_IMPLEMENTED_BYTE_STRING" Name None @@ -849,204 +523,222 @@ False 1 1.0 1j -NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 +True or False +True or False or None +True and False +True and False and None +(Name1 and Name2) or Name3 +Name1 and Name2 or Name3 +Name1 or (Name2 and Name3) +Name1 or Name2 and Name3 +(Name1 and Name2) or (Name3 and Name4) +Name1 and Name2 or Name3 and Name4 +Name1 or (Name2 and Name3) or Name4 +Name1 or Name2 and Name3 or Name4 v1 << 2 1 >> v2 1 % finished 1 + v2 - v3 * 4 ^ 5**v6 / 7 // 8 ((1 + v2) - (v3 * 4)) ^ (((5**v6) / 7) // 8) -NOT_YET_IMPLEMENTED_ExprUnaryOp -NOT_YET_IMPLEMENTED_ExprUnaryOp -NOT_YET_IMPLEMENTED_ExprUnaryOp -NOT_YET_IMPLEMENTED_ExprUnaryOp -NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -NOT_YET_IMPLEMENTED_ExprUnaryOp -NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -lambda x: True -lambda x: True -lambda x: True -lambda x: True -lambda x: True -manylambdas = lambda x: True -foo = lambda x: True -NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false -NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false -NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false -NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false -NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false -(NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false) -{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} -{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} -{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} -{ - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - (NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false), -} -NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 -(1, 2) -(1, 2) -(1, 2) +not great +~great ++value +-1 +~int and not v1 ^ 123 + v2 | True +(~int) and (not ((v1 ^ (123 + v2)) | True)) ++really ** -confusing ** ~operator**-precedence +flags & ~select.EPOLLIN and waiters.write_task is not None +lambda NOT_YET_IMPLEMENTED_lambda: True +lambda NOT_YET_IMPLEMENTED_lambda: True +lambda NOT_YET_IMPLEMENTED_lambda: True +lambda NOT_YET_IMPLEMENTED_lambda: True +lambda NOT_YET_IMPLEMENTED_lambda: True +manylambdas = lambda NOT_YET_IMPLEMENTED_lambda: True +foo = lambda NOT_YET_IMPLEMENTED_lambda: True +1 if True else 2 +str or None if True else str or bytes or None +(str or None) if True else (str or bytes or None) +str or None if (1 if True else 2) else str or bytes or None +(str or None) if (1 if True else 2) else (str or bytes or None) +( + (super_long_variable_name or None) + if (1 if super_long_test_name else 2) + else (str or bytes or None) +) +{"2.7": dead, "3.7": (long_live or die_hard)} +{"2.7": dead, "3.7": (long_live or die_hard), **{"3.6": verygood}} +{**a, **b, **c} +{"2.7", "3.6", "3.7", "3.8", "3.9", ("4.0" if gilectomy else "3.10")} +( + {"a": "b"}, + (True or False), + (+value), + "string", + b"NOT_YET_IMPLEMENTED_BYTE_STRING", +) or None +() +(1,) (1, 2) +(1, 2, 3) [] +[1, 2, 3, 4, 5, 6, 7, 8, 9, (10 or A), (11 or B), (12 or C)] [ 1, 2, 3, +] +[*a] +[*range(10)] +[ + *a, 4, 5, - 6, - 7, - 8, - 9, - (NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2), - (NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2), - (NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2), ] -[1, 2, 3] -[NOT_YET_IMPLEMENTED_ExprStarred] -[NOT_YET_IMPLEMENTED_ExprStarred] -[NOT_YET_IMPLEMENTED_ExprStarred, 4, 5] -[4, NOT_YET_IMPLEMENTED_ExprStarred, 5] +[ + 4, + *a, + 5, +] [ this_is_a_very_long_variable_which_will_force_a_delimiter_split, element, another, - NOT_YET_IMPLEMENTED_ExprStarred, + *more, ] -NOT_YET_IMPLEMENTED_ExprSetComp -NOT_YET_IMPLEMENTED_ExprSetComp -NOT_YET_IMPLEMENTED_ExprSetComp -NOT_YET_IMPLEMENTED_ExprSetComp -[i for i in []] -[i for i in []] -[i for i in []] -[i for i in []] +{NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set} +{NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set} +{NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set} +{NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set} +[i for i in (1, 2, 3)] +[(i**2) for i in (1, 2, 3)] +[(i**2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))] +[((i**2) + j) for i in (1, 2, 3) for j in (1, 2, 3)] {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} -NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right -NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right -NOT_IMPLEMENTED_call() -NOT_IMPLEMENTED_call() -NOT_IMPLEMENTED_call() -NOT_IMPLEMENTED_call() -NOT_IMPLEMENTED_call() -NOT_IMPLEMENTED_call() # note: no trailing comma pre-3.6 -NOT_IMPLEMENTED_call() -NOT_IMPLEMENTED_call() -NOT_IMPLEMENTED_call() -NOT_IMPLEMENTED_call() -lukasz.NOT_IMPLEMENTED_attr.NOT_IMPLEMENTED_attr -NOT_IMPLEMENTED_call() -1 .NOT_IMPLEMENTED_attr -1.0 .NOT_IMPLEMENTED_attr -....NOT_IMPLEMENTED_attr -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_YET_IMPLEMENTED_StmtAnnAssign -NOT_YET_IMPLEMENTED_StmtAnnAssign -NOT_YET_IMPLEMENTED_StmtAnnAssign -NOT_YET_IMPLEMENTED_StmtAnnAssign # type: ignore -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false -{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} -{ - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, -} -[ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2, - NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2, - NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2, +Python3 > Python2 > COBOL +Life is Life +call() +call(arg) +call(kwarg="hey") +call(arg, kwarg="hey") +call(arg, another, kwarg="hey", **kwargs) +call( + this_is_a_very_long_variable_which_will_force_a_delimiter_split, + arg, + another, + kwarg="hey", + **kwargs, +) # note: no trailing comma pre-3.6 +call(*gidgets[:2]) +call(a, *gidgets[:2]) +call(**self.screen_kwargs) +call(b, **self.screen_kwargs) +lukasz.langa.pl +call.me(maybe) +(1).real +(1.0).real +....__class__ +list[str] +dict[str, int] +tuple[str, ...] +tuple[str, int, float, dict[str, int]] +tuple[ + str, + int, + float, + dict[str, int], ] +very_long_variable_name_filters: t.List[ + t.Tuple[str, t.Union[str, t.List[t.Optional[str]]]], +] +xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod( # type: ignore + sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__) +) +xxxx_xxx_xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod( # type: ignore + sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__) +) +xxxx_xxx_xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod( + sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__) +) # type: ignore +slice[0] +slice[0:1] +slice[0:1:2] +slice[:] +slice[ : -1] +slice[1:] +slice[ :: -1] +slice[d :: d + 1] +slice[:c, c - 1] +numpy[:, 0:1] +numpy[:, : -1] +numpy[0, :] +numpy[:, i] +numpy[0, :2] +numpy[:N, 0] +numpy[:2, :4] +numpy[2:4, 1:5] +numpy[4:, 2:] +numpy[:, (0, 1, 2, 5)] +numpy[0, [0]] +numpy[:, [i]] +numpy[1 : c + 1, c] +numpy[-(c + 1) :, d] +numpy[:, l[-2]] +numpy[:, :: -1] +numpy[np.newaxis, :] +(str or None) if (sys.version_info[0] > (3,)) else (str or bytes or None) +{"2.7": dead, "3.7": long_live or die_hard} +{"2.7", "3.6", "3.7", "3.8", "3.9", "4.0" if gilectomy else "3.10"} +[1, 2, 3, 4, 5, 6, 7, 8, 9, 10 or A, 11 or B, 12 or C] (SomeName) SomeName -(1, 2) -(i for i in []) -(i for i in []) -(i for i in []) -(i for i in []) -(1, 2) -{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} -a = (1, 2) -b = (1, 2) +(Good, Bad, Ugly) +(NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) +(NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) +(NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) +(NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) +(*starred,) +{ + "id": "1", + "type": "type", + "started_at": now(), + "ended_at": now() + timedelta(days=10), + "priority": 1, + "import_session_id": 1, + **kwargs, +} +a = (1,) +b = (1,) c = 1 -d = (1, 2) + a + (1, 2) -e = NOT_IMPLEMENTED_call() -f = (1, 2) -g = (1, 2) -what_is_up_with_those_new_coord_names = ( - (coord_names + NOT_IMPLEMENTED_call()) - + NOT_IMPLEMENTED_call() +d = (1,) + a + (2,) +e = (1,).count(1) +f = 1, *range(10) +g = 1, *"ten" +what_is_up_with_those_new_coord_names = (coord_names + set(vars_to_create)) + set( + vars_to_remove ) -what_is_up_with_those_new_coord_names = ( - (coord_names | NOT_IMPLEMENTED_call()) - - NOT_IMPLEMENTED_call() +what_is_up_with_those_new_coord_names = (coord_names | set(vars_to_create)) - set( + vars_to_remove ) -result = NOT_IMPLEMENTED_call() -result = NOT_IMPLEMENTED_call() -Ø = NOT_IMPLEMENTED_call() -NOT_IMPLEMENTED_call() -mapping = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} +result = session.query(models.Customer.id).filter( + models.Customer.account_id == account_id, models.Customer.email == email_address +).order_by(models.Customer.id.asc()).all() +result = session.query(models.Customer.id).filter( + models.Customer.account_id == account_id, models.Customer.email == email_address +).order_by( + models.Customer.id.asc(), +).all() +Ø = set() +authors.łukasz.say_thanks() +mapping = { + A: 0.25 * (10.0 / 12), + B: 0.1 * (10.0 / 12), + C: 0.1 * (10.0 / 12), + D: 0.1 * (10.0 / 12), +} def gen(): @@ -1057,28 +749,60 @@ def gen(): async def f(): - await NOT_IMPLEMENTED_call() + await some.complicated[0].call(with_args=(True or (1 is not 1))) -NOT_IMPLEMENTED_call() -NOT_IMPLEMENTED_call() -NOT_IMPLEMENTED_call() +print(*[] or [1]) +print( + **{1: 3} + if False + else {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} +) +print(*lambda NOT_YET_IMPLEMENTED_lambda: True) NOT_YET_IMPLEMENTED_StmtAssert NOT_YET_IMPLEMENTED_StmtAssert NOT_YET_IMPLEMENTED_StmtAssert -NOT_YET_IMPLEMENTED_StmtFor -NOT_YET_IMPLEMENTED_StmtFor -NOT_YET_IMPLEMENTED_StmtFor -NOT_YET_IMPLEMENTED_StmtFor -NOT_YET_IMPLEMENTED_StmtFor -while NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: +for (x,) in (1,), (2,), (3,): ... -NOT_YET_IMPLEMENTED_StmtFor -a = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right -a = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right -a = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right -a = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right -if NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: +for y in (): + ... +for z in (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []): + ... +for i in call(): + ... +for j in 1 + (2 + 3): + ... +while this and that: + ... +for ( + addr_family, + addr_type, + addr_proto, + addr_canonname, + addr_sockaddr, +) in socket.getaddrinfo("google.com", "http"): + pass +a = ( + aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp + in qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz +) +a = ( + aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp + not in qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz +) +a = ( + aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp + is qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz +) +a = ( + aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp + is not qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz +) +if ( + threading.current_thread() != threading.main_thread() + and threading.current_thread() != threading.main_thread() + or signal.getsignal(signal.SIGINT) != signal.default_int_handler +): return True if ( aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa @@ -1111,44 +835,30 @@ if ( ): return True if ( - NOT_YET_IMPLEMENTED_ExprUnaryOp - + aaaa.NOT_IMPLEMENTED_attr - - aaaa.NOT_IMPLEMENTED_attr * aaaa.NOT_IMPLEMENTED_attr / aaaa.NOT_IMPLEMENTED_attr - | aaaa.NOT_IMPLEMENTED_attr - & aaaa.NOT_IMPLEMENTED_attr % aaaa.NOT_IMPLEMENTED_attr - ^ aaaa.NOT_IMPLEMENTED_attr - << aaaa.NOT_IMPLEMENTED_attr - >> aaaa.NOT_IMPLEMENTED_attr**aaaa.NOT_IMPLEMENTED_attr // aaaa.NOT_IMPLEMENTED_attr + ~aaaa.a + + aaaa.b + - aaaa.c * aaaa.d / aaaa.e + | aaaa.f & aaaa.g % aaaa.h ^ aaaa.i << aaaa.k >> aaaa.l**aaaa.m // aaaa.n ): return True if ( - NOT_YET_IMPLEMENTED_ExprUnaryOp - + aaaaaaaa.NOT_IMPLEMENTED_attr - - aaaaaaaa.NOT_IMPLEMENTED_attr - @ aaaaaaaa.NOT_IMPLEMENTED_attr - / aaaaaaaa.NOT_IMPLEMENTED_attr - | aaaaaaaa.NOT_IMPLEMENTED_attr - & aaaaaaaa.NOT_IMPLEMENTED_attr % aaaaaaaa.NOT_IMPLEMENTED_attr - ^ aaaaaaaa.NOT_IMPLEMENTED_attr - << aaaaaaaa.NOT_IMPLEMENTED_attr - >> aaaaaaaa.NOT_IMPLEMENTED_attr - **aaaaaaaa.NOT_IMPLEMENTED_attr - // aaaaaaaa.NOT_IMPLEMENTED_attr + ~aaaaaaaa.a + + aaaaaaaa.b + - aaaaaaaa.c @ aaaaaaaa.d / aaaaaaaa.e + | aaaaaaaa.f + & aaaaaaaa.g % aaaaaaaa.h + ^ aaaaaaaa.i << aaaaaaaa.k >> aaaaaaaa.l**aaaaaaaa.m // aaaaaaaa.n ): return True if ( - NOT_YET_IMPLEMENTED_ExprUnaryOp - + aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr - - aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr - * aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr - @ aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr - | aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr - & aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr % aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr - ^ aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr - << aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr - >> aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr - **aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr - // aaaaaaaaaaaaaaaa.NOT_IMPLEMENTED_attr + ~aaaaaaaaaaaaaaaa.a + + aaaaaaaaaaaaaaaa.b + - aaaaaaaaaaaaaaaa.c * aaaaaaaaaaaaaaaa.d @ aaaaaaaaaaaaaaaa.e + | aaaaaaaaaaaaaaaa.f + & aaaaaaaaaaaaaaaa.g % aaaaaaaaaaaaaaaa.h + ^ aaaaaaaaaaaaaaaa.i + << aaaaaaaaaaaaaaaa.k + >> aaaaaaaaaaaaaaaa.l**aaaaaaaaaaaaaaaa.m // aaaaaaaaaaaaaaaa.n ): return True ( @@ -1167,17 +877,17 @@ aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa bbbb >> bbbb * bbbb ( aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - ^ bbbb.NOT_IMPLEMENTED_attr - & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + ^ bbbb.a & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ^ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ) -NOT_IMPLEMENTED_call() +last_call() # standalone comment at ENDMARKER ``` ## Black Output ```py +... "some_string" b"\\xa3" Name @@ -1294,7 +1004,7 @@ call( arg, another, kwarg="hey", - **kwargs, + **kwargs ) # note: no trailing comma pre-3.6 call(*gidgets[:2]) call(a, *gidgets[:2]) diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff.py.snap similarity index 64% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff.py.snap index febe1c8ba1..2aa1ef102d 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtonoff.py --- ## Input @@ -199,30 +198,20 @@ d={'a':1, ```diff --- Black +++ Ruff -@@ -1,16 +1,14 @@ - #!/usr/bin/env python3 --import asyncio --import sys -+NOT_YET_IMPLEMENTED_StmtImport -+NOT_YET_IMPLEMENTED_StmtImport +@@ -6,10 +6,9 @@ --from third_party import X, Y, Z -- --from library import some_connection, some_decorator -+NOT_YET_IMPLEMENTED_StmtImportFrom - -+NOT_YET_IMPLEMENTED_StmtImportFrom + from library import some_connection, some_decorator # fmt: off -from third_party import (X, - Y, Z) -+NOT_YET_IMPLEMENTED_StmtImportFrom ++from third_party import X, Y, Z # fmt: on -f"trigger 3.6 mode" -+NOT_YET_IMPLEMENTED_ExprJoinedStr ++f"NOT_YET_IMPLEMENTED_ExprJoinedStr" # Comment 1 # Comment 2 -@@ -18,109 +16,111 @@ +@@ -17,30 +16,44 @@ # fmt: off def func_no_args(): @@ -238,11 +227,13 @@ d={'a':1, + b + c + if True: -+ NOT_YET_IMPLEMENTED_StmtRaise ++ raise RuntimeError + if False: + ... -+ NOT_YET_IMPLEMENTED_StmtFor -+ NOT_IMPLEMENTED_call() ++ for i in range(10): ++ print(i) ++ continue ++ exec("new-style exec", {}, {}) + return None + + @@ -251,99 +242,67 @@ d={'a':1, - async with some_connection() as conn: - await conn.do_what_i_mean('SELECT bobby, tables FROM xkcd', timeout=2) - await asyncio.sleep(1) --@asyncio.coroutine ++ "Single-line docstring. Multiline is harder to reformat." ++ async with some_connection() as conn: ++ await conn.do_what_i_mean("SELECT bobby, tables FROM xkcd", timeout=2) ++ await asyncio.sleep(1) ++ ++ + @asyncio.coroutine -@some_decorator( -with_args=True, -many_args=[1,2,3] -) -def function_signature_stress_test(number:int,no_annotation=None,text:str='default',* ,debug:bool=False,**kwargs) -> str: - return text[number:-1] -+ "NOT_YET_IMPLEMENTED_STRING" -+ NOT_YET_IMPLEMENTED_StmtAsyncWith -+ await NOT_IMPLEMENTED_call() -+ -+ -+@asyncio.NOT_IMPLEMENTED_attr -+@NOT_IMPLEMENTED_call() ++@some_decorator(with_args=True, many_args=[1, 2, 3]) +def function_signature_stress_test( + number: int, + no_annotation=None, -+ text: str = "NOT_YET_IMPLEMENTED_STRING", ++ text: str = "default", ++ *, + debug: bool = False, + **kwargs, +) -> str: -+ return NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] ++ return text[number : -1] + + # fmt: on --def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r""): + def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r""): - offset = attr.ib(default=attr.Factory(lambda: _r.uniform(1, 2))) - assert task._cancel_stack[: len(old_stack)] == old_stack -+def spaces( -+ a=1, -+ b=(1, 2), -+ c=[], -+ d={NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value}, -+ e=True, -+ f=NOT_YET_IMPLEMENTED_ExprUnaryOp, -+ g=NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, -+ h="NOT_YET_IMPLEMENTED_STRING", -+ i="NOT_YET_IMPLEMENTED_STRING", -+): -+ offset = NOT_IMPLEMENTED_call() ++ offset = attr.ib(default=attr.Factory(lambda NOT_YET_IMPLEMENTED_lambda: True)) + NOT_YET_IMPLEMENTED_StmtAssert def spaces_types( - a: int = 1, -- b: tuple = (), -+ b: tuple = (1, 2), - c: list = [], -- d: dict = {}, -+ d: dict = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value}, - e: bool = True, -- f: int = -1, -- g: int = 1 if False else 2, -- h: str = "", -- i: str = r"", -+ f: int = NOT_YET_IMPLEMENTED_ExprUnaryOp, -+ g: int = NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, -+ h: str = "NOT_YET_IMPLEMENTED_STRING", -+ i: str = "NOT_YET_IMPLEMENTED_STRING", - ): - ... +@@ -63,15 +76,15 @@ - --def spaces2(result=_core.Value(None)): -+def spaces2(result=NOT_IMPLEMENTED_call()): - ... - - --something = { -- # fmt: off + something = { + # fmt: off - key: 'value', --} -+something = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} ++ key: "value", + } def subscriptlist(): -- atom[ -- # fmt: off + atom[ + # fmt: off - 'some big and', - 'complex subscript', -- # fmt: on -- goes + here, -- andhere, -- ] -+ NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] - ++ "some big and", ++ "complex subscript", + # fmt: on + goes + here, + andhere, +@@ -80,38 +93,35 @@ def import_as_names(): # fmt: off - from hello import a, b - 'unformatted' -+ NOT_YET_IMPLEMENTED_StmtImportFrom -+ "NOT_YET_IMPLEMENTED_STRING" ++ from hello import a, b ++ "unformatted" # fmt: on @@ -351,8 +310,8 @@ d={'a':1, # fmt: off - a , b = *hello - 'unformatted' -+ (1, 2) = NOT_YET_IMPLEMENTED_ExprStarred -+ "NOT_YET_IMPLEMENTED_STRING" ++ a, b = *hello ++ "unformatted" # fmt: on @@ -361,15 +320,14 @@ d={'a':1, - yield hello - 'unformatted' + NOT_YET_IMPLEMENTED_ExprYield -+ "NOT_YET_IMPLEMENTED_STRING" ++ "unformatted" # fmt: on -- "formatted" -+ "NOT_YET_IMPLEMENTED_STRING" + "formatted" # fmt: off - ( yield hello ) - 'unformatted' + (NOT_YET_IMPLEMENTED_ExprYield) -+ "NOT_YET_IMPLEMENTED_STRING" ++ "unformatted" # fmt: on @@ -381,121 +339,50 @@ d={'a':1, - models.Customer.email == email_address)\ - .order_by(models.Customer.id.asc())\ - .all() -+ result = NOT_IMPLEMENTED_call() ++ result = session.query(models.Customer.id).filter( ++ models.Customer.account_id == account_id, models.Customer.email == email_address ++ ).order_by(models.Customer.id.asc()).all() # fmt: on - def off_and_on_without_data(): -- """All comments here are technically on the same prefix. -- -- The comments between will be formatted. This is a known limitation. -- """ -+ "NOT_YET_IMPLEMENTED_STRING" - # fmt: off - - # hey, that won't work -@@ -130,13 +130,15 @@ - - - def on_and_off_broken(): -- """Another known limitation.""" -+ "NOT_YET_IMPLEMENTED_STRING" +@@ -132,10 +142,10 @@ + """Another known limitation.""" # fmt: on # fmt: off - this=should.not_be.formatted() - and_=indeed . it is not formatted - because . the . handling . inside . generate_ignored_nodes() - now . considers . multiple . fmt . directives . within . one . prefix -+ this = NOT_IMPLEMENTED_call() -+ and_ = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right -+ NOT_IMPLEMENTED_call() -+ ( -+ now.NOT_IMPLEMENTED_attr.NOT_IMPLEMENTED_attr.NOT_IMPLEMENTED_attr.NOT_IMPLEMENTED_attr.NOT_IMPLEMENTED_attr.NOT_IMPLEMENTED_attr.NOT_IMPLEMENTED_attr -+ ) ++ this = should.not_be.formatted() ++ and_ = indeed.it is not formatted ++ because.the.handling.inside.generate_ignored_nodes() ++ now.considers.multiple.fmt.directives.within.one.prefix # fmt: on # fmt: off # ...but comments still get reformatted even though they should not be -@@ -145,80 +147,21 @@ - - def long_lines(): - if True: -- typedargslist.extend( -- gen_annotated_params( -- ast_args.kwonlyargs, -- ast_args.kw_defaults, -- parameters, -- implicit_default=True, -- ) -- ) -+ NOT_IMPLEMENTED_call() +@@ -153,9 +163,7 @@ + ) + ) # fmt: off - a = ( - unnecessary_bracket() - ) -+ a = NOT_IMPLEMENTED_call() ++ a = unnecessary_bracket() # fmt: on -- _type_comment_re = re.compile( -- r""" -- ^ -- [\t ]* -- \#[ ]type:[ ]* -- (?P -- [^#\t\n]+? -- ) -- (? to match -- # a trailing space which is why we need the silliness below -- (? -- (?:\#[^\n]*)? -- \n? -- ) -- $ -- """, -- # fmt: off + _type_comment_re = re.compile( + r""" +@@ -178,7 +186,7 @@ + $ + """, + # fmt: off - re.MULTILINE|re.VERBOSE -- # fmt: on -- ) -+ _type_comment_re = NOT_IMPLEMENTED_call() ++ re.MULTILINE | re.VERBOSE, + # fmt: on + ) - - def single_literal_yapf_disable(): -- """Black does not support this.""" -- BAZ = {(1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)} # yapf: disable -+ "NOT_YET_IMPLEMENTED_STRING" -+ BAZ = {(1, 2), (1, 2), (1, 2)} # yapf: disable - - --cfg.rule( -- "Default", -- "address", -- xxxx_xxxx=["xxx-xxxxxx-xxxxxxxxxx"], -- xxxxxx="xx_xxxxx", -- xxxxxxx="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", -- xxxxxxxxx_xxxx=True, -- xxxxxxxx_xxxxxxxxxx=False, -- xxxxxx_xxxxxx=2, -- xxxxxx_xxxxx_xxxxxxxx=70, -- xxxxxx_xxxxxx_xxxxx=True, -- # fmt: off -- xxxxxxx_xxxxxxxxxxxx={ -- "xxxxxxxx": { -- "xxxxxx": False, -- "xxxxxxx": False, -- "xxxx_xxxxxx": "xxxxx", -- }, -- "xxxxxxxx-xxxxx": { -- "xxxxxx": False, -- "xxxxxxx": True, -- "xxxx_xxxxxx": "xxxxxx", -- }, -- }, -- # fmt: on -- xxxxxxxxxx_xxxxxxxxxxx_xxxxxxx_xxxxxxxxx=5, --) -+NOT_IMPLEMENTED_call() +@@ -216,8 +224,7 @@ + xxxxxxxxxx_xxxxxxxxxxx_xxxxxxx_xxxxxxxxx=5, + ) # fmt: off -yield 'hello' +NOT_YET_IMPLEMENTED_ExprYield @@ -504,23 +391,23 @@ d={'a':1, -d={'a':1, - 'b':2} +l = [1, 2, 3] -+d = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} ++d = {"a": 1, "b": 2} ``` ## Ruff Output ```py #!/usr/bin/env python3 -NOT_YET_IMPLEMENTED_StmtImport -NOT_YET_IMPLEMENTED_StmtImport +import asyncio +import sys -NOT_YET_IMPLEMENTED_StmtImportFrom +from third_party import X, Y, Z -NOT_YET_IMPLEMENTED_StmtImportFrom +from library import some_connection, some_decorator # fmt: off -NOT_YET_IMPLEMENTED_StmtImportFrom +from third_party import X, Y, Z # fmt: on -NOT_YET_IMPLEMENTED_ExprJoinedStr +f"NOT_YET_IMPLEMENTED_ExprJoinedStr" # Comment 1 # Comment 2 @@ -532,107 +419,116 @@ def func_no_args(): b c if True: - NOT_YET_IMPLEMENTED_StmtRaise + raise RuntimeError if False: ... - NOT_YET_IMPLEMENTED_StmtFor - NOT_IMPLEMENTED_call() + for i in range(10): + print(i) + continue + exec("new-style exec", {}, {}) return None async def coroutine(arg, exec=False): - "NOT_YET_IMPLEMENTED_STRING" - NOT_YET_IMPLEMENTED_StmtAsyncWith - await NOT_IMPLEMENTED_call() + "Single-line docstring. Multiline is harder to reformat." + async with some_connection() as conn: + await conn.do_what_i_mean("SELECT bobby, tables FROM xkcd", timeout=2) + await asyncio.sleep(1) -@asyncio.NOT_IMPLEMENTED_attr -@NOT_IMPLEMENTED_call() +@asyncio.coroutine +@some_decorator(with_args=True, many_args=[1, 2, 3]) def function_signature_stress_test( number: int, no_annotation=None, - text: str = "NOT_YET_IMPLEMENTED_STRING", + text: str = "default", + *, debug: bool = False, **kwargs, ) -> str: - return NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] + return text[number : -1] # fmt: on -def spaces( - a=1, - b=(1, 2), - c=[], - d={NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value}, - e=True, - f=NOT_YET_IMPLEMENTED_ExprUnaryOp, - g=NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, - h="NOT_YET_IMPLEMENTED_STRING", - i="NOT_YET_IMPLEMENTED_STRING", -): - offset = NOT_IMPLEMENTED_call() +def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r""): + offset = attr.ib(default=attr.Factory(lambda NOT_YET_IMPLEMENTED_lambda: True)) NOT_YET_IMPLEMENTED_StmtAssert def spaces_types( a: int = 1, - b: tuple = (1, 2), + b: tuple = (), c: list = [], - d: dict = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value}, + d: dict = {}, e: bool = True, - f: int = NOT_YET_IMPLEMENTED_ExprUnaryOp, - g: int = NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, - h: str = "NOT_YET_IMPLEMENTED_STRING", - i: str = "NOT_YET_IMPLEMENTED_STRING", + f: int = -1, + g: int = 1 if False else 2, + h: str = "", + i: str = r"", ): ... -def spaces2(result=NOT_IMPLEMENTED_call()): +def spaces2(result=_core.Value(None)): ... -something = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} +something = { + # fmt: off + key: "value", +} def subscriptlist(): - NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] + atom[ + # fmt: off + "some big and", + "complex subscript", + # fmt: on + goes + here, + andhere, + ] def import_as_names(): # fmt: off - NOT_YET_IMPLEMENTED_StmtImportFrom - "NOT_YET_IMPLEMENTED_STRING" + from hello import a, b + "unformatted" # fmt: on def testlist_star_expr(): # fmt: off - (1, 2) = NOT_YET_IMPLEMENTED_ExprStarred - "NOT_YET_IMPLEMENTED_STRING" + a, b = *hello + "unformatted" # fmt: on def yield_expr(): # fmt: off NOT_YET_IMPLEMENTED_ExprYield - "NOT_YET_IMPLEMENTED_STRING" + "unformatted" # fmt: on - "NOT_YET_IMPLEMENTED_STRING" + "formatted" # fmt: off (NOT_YET_IMPLEMENTED_ExprYield) - "NOT_YET_IMPLEMENTED_STRING" + "unformatted" # fmt: on def example(session): # fmt: off - result = NOT_IMPLEMENTED_call() + result = session.query(models.Customer.id).filter( + models.Customer.account_id == account_id, models.Customer.email == email_address + ).order_by(models.Customer.id.asc()).all() # fmt: on def off_and_on_without_data(): - "NOT_YET_IMPLEMENTED_STRING" + """All comments here are technically on the same prefix. + + The comments between will be formatted. This is a known limitation. + """ # fmt: off # hey, that won't work @@ -642,15 +538,13 @@ def off_and_on_without_data(): def on_and_off_broken(): - "NOT_YET_IMPLEMENTED_STRING" + """Another known limitation.""" # fmt: on # fmt: off - this = NOT_IMPLEMENTED_call() - and_ = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right - NOT_IMPLEMENTED_call() - ( - now.NOT_IMPLEMENTED_attr.NOT_IMPLEMENTED_attr.NOT_IMPLEMENTED_attr.NOT_IMPLEMENTED_attr.NOT_IMPLEMENTED_attr.NOT_IMPLEMENTED_attr.NOT_IMPLEMENTED_attr - ) + this = should.not_be.formatted() + and_ = indeed.it is not formatted + because.the.handling.inside.generate_ignored_nodes() + now.considers.multiple.fmt.directives.within.one.prefix # fmt: on # fmt: off # ...but comments still get reformatted even though they should not be @@ -659,24 +553,80 @@ def on_and_off_broken(): def long_lines(): if True: - NOT_IMPLEMENTED_call() + typedargslist.extend( + gen_annotated_params( + ast_args.kwonlyargs, + ast_args.kw_defaults, + parameters, + implicit_default=True, + ) + ) # fmt: off - a = NOT_IMPLEMENTED_call() + a = unnecessary_bracket() # fmt: on - _type_comment_re = NOT_IMPLEMENTED_call() + _type_comment_re = re.compile( + r""" + ^ + [\t ]* + \#[ ]type:[ ]* + (?P + [^#\t\n]+? + ) + (? to match + # a trailing space which is why we need the silliness below + (? + (?:\#[^\n]*)? + \n? + ) + $ + """, + # fmt: off + re.MULTILINE | re.VERBOSE, + # fmt: on + ) def single_literal_yapf_disable(): - "NOT_YET_IMPLEMENTED_STRING" - BAZ = {(1, 2), (1, 2), (1, 2)} # yapf: disable + """Black does not support this.""" + BAZ = {(1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)} # yapf: disable -NOT_IMPLEMENTED_call() +cfg.rule( + "Default", + "address", + xxxx_xxxx=["xxx-xxxxxx-xxxxxxxxxx"], + xxxxxx="xx_xxxxx", + xxxxxxx="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + xxxxxxxxx_xxxx=True, + xxxxxxxx_xxxxxxxxxx=False, + xxxxxx_xxxxxx=2, + xxxxxx_xxxxx_xxxxxxxx=70, + xxxxxx_xxxxxx_xxxxx=True, + # fmt: off + xxxxxxx_xxxxxxxxxxxx={ + "xxxxxxxx": { + "xxxxxx": False, + "xxxxxxx": False, + "xxxx_xxxxxx": "xxxxx", + }, + "xxxxxxxx-xxxxx": { + "xxxxxx": False, + "xxxxxxx": True, + "xxxx_xxxxxx": "xxxxxx", + }, + }, + # fmt: on + xxxxxxxxxx_xxxxxxxxxxx_xxxxxxx_xxxxxxxxx=5, +) # fmt: off NOT_YET_IMPLEMENTED_ExprYield # No formatting to the end of the file l = [1, 2, 3] -d = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} +d = {"a": 1, "b": 2} ``` ## Black Output @@ -689,7 +639,6 @@ import sys from third_party import X, Y, Z from library import some_connection, some_decorator - # fmt: off from third_party import (X, Y, Z) diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff2_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff2.py.snap similarity index 82% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff2_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff2.py.snap index d4de723ff0..abdfcb6d56 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff2_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff2.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtonoff2.py --- ## Input @@ -53,12 +52,7 @@ def test_calculate_fades(): ```diff --- Black +++ Ruff -@@ -1,40 +1,38 @@ --import pytest -+NOT_YET_IMPLEMENTED_StmtImport - - TmSt = 1 - TmEx = 2 +@@ -5,36 +5,40 @@ # fmt: off @@ -69,11 +63,15 @@ def test_calculate_fades(): -@pytest.mark.parametrize('test', [ - - # Test don't manage the volume -- [ ++@pytest.mark.parametrize( ++ "test", + [ - ('stuff', 'in') -- ], ++ # Test don't manage the volume ++ [("stuff", "in")], + ], -]) -+@NOT_IMPLEMENTED_call() ++) def test_fader(test): pass @@ -90,9 +88,8 @@ def test_calculate_fades(): + def verify_fader(test): -- """Hey, ho.""" + """Hey, ho.""" - assert test.passed() -+ "NOT_YET_IMPLEMENTED_STRING" + NOT_YET_IMPLEMENTED_StmtAssert + @@ -101,8 +98,8 @@ def test_calculate_fades(): # one is zero/none - (0, 4, 0, 0, 10, 0, 0, 6, 10), - (None, 4, 0, 0, 10, 0, 0, 6, 10), -+ (1, 2), -+ (1, 2), ++ (0, 4, 0, 0, 10, 0, 0, 6, 10), ++ (None, 4, 0, 0, 10, 0, 0, 6, 10), ] # fmt: on @@ -111,7 +108,7 @@ def test_calculate_fades(): ## Ruff Output ```py -NOT_YET_IMPLEMENTED_StmtImport +import pytest TmSt = 1 TmEx = 2 @@ -122,7 +119,13 @@ TmEx = 2 # Test data: # Position, Volume, State, TmSt/TmEx/None, [call, [arg1...]] -@NOT_IMPLEMENTED_call() +@pytest.mark.parametrize( + "test", + [ + # Test don't manage the volume + [("stuff", "in")], + ], +) def test_fader(test): pass @@ -137,15 +140,15 @@ def verify_fader(test): def verify_fader(test): - "NOT_YET_IMPLEMENTED_STRING" + """Hey, ho.""" NOT_YET_IMPLEMENTED_StmtAssert def test_calculate_fades(): calcs = [ # one is zero/none - (1, 2), - (1, 2), + (0, 4, 0, 0, 10, 0, 0, 6, 10), + (None, 4, 0, 0, 10, 0, 0, 6, 10), ] # fmt: on diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff3_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff3.py.snap similarity index 73% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff3_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff3.py.snap index 1d24dd47e2..42ebc85486 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff3_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff3.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtonoff3.py --- ## Input @@ -30,35 +29,50 @@ x = [ ```diff --- Black +++ Ruff -@@ -1,15 +1,9 @@ +@@ -1,14 +1,18 @@ # fmt: off --x = [ + x = [ - 1, 2, - 3, 4, --] -+x = [1, 2, 3, 4] ++ 1, ++ 2, ++ 3, ++ 4, + ] # fmt: on # fmt: off --x = [ + x = [ - 1, 2, - 3, 4, --] -+x = [1, 2, 3, 4] ++ 1, ++ 2, ++ 3, ++ 4, + ] # fmt: on - x = [1, 2, 3, 4] ``` ## Ruff Output ```py # fmt: off -x = [1, 2, 3, 4] +x = [ + 1, + 2, + 3, + 4, +] # fmt: on # fmt: off -x = [1, 2, 3, 4] +x = [ + 1, + 2, + 3, + 4, +] # fmt: on x = [1, 2, 3, 4] diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff4_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff4.py.snap similarity index 73% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff4_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff4.py.snap index 9fd0b0b4b4..a8dc2ef620 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff4_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff4.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtonoff4.py --- ## Input @@ -26,42 +25,50 @@ def f(): pass ```diff --- Black +++ Ruff -@@ -1,20 +1,10 @@ +@@ -1,8 +1,12 @@ # fmt: off -@test([ - 1, 2, - 3, 4, -]) -+@NOT_IMPLEMENTED_call() ++@test( ++ [ ++ 1, ++ 2, ++ 3, ++ 4, ++ ] ++) # fmt: on def f(): pass - - --@test( -- [ -- 1, -- 2, -- 3, -- 4, -- ] --) -+@NOT_IMPLEMENTED_call() - def f(): - pass ``` ## Ruff Output ```py # fmt: off -@NOT_IMPLEMENTED_call() +@test( + [ + 1, + 2, + 3, + 4, + ] +) # fmt: on def f(): pass -@NOT_IMPLEMENTED_call() +@test( + [ + 1, + 2, + 3, + 4, + ] +) def f(): pass ``` diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff5_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff5.py.snap similarity index 71% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff5_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff5.py.snap index 7582c7cdee..99e98844ff 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff5_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff5.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtonoff5.py --- ## Input @@ -97,44 +96,30 @@ elif unformatted: ```diff --- Black +++ Ruff -@@ -1,33 +1,15 @@ - # Regression test for https://github.com/psf/black/issues/3129. --setup( -- entry_points={ -- # fmt: off -- "console_scripts": [ +@@ -3,10 +3,9 @@ + entry_points={ + # fmt: off + "console_scripts": [ - "foo-bar" - "=foo.bar.:main", - # fmt: on - ] # Includes an formatted indentation. -- }, --) -+NOT_IMPLEMENTED_call() - - - # Regression test for https://github.com/psf/black/issues/2015. --run( -- # fmt: off -- [ -- "ls", -- "-la", -- ] -- # fmt: on -- + path, -- check=True, --) -+NOT_IMPLEMENTED_call() - ++ "foo-bar" "=foo.bar.:main", ++ # fmt: on ++ ] # Includes an formatted indentation. + }, + ) +@@ -27,7 +26,7 @@ # Regression test for https://github.com/psf/black/issues/3026. def test_func(): # yapf: disable - if unformatted( args ): -+ if NOT_IMPLEMENTED_call(): ++ if unformatted(args): return True # yapf: enable elif b: -@@ -39,49 +21,27 @@ +@@ -39,10 +38,10 @@ # Regression test for https://github.com/psf/black/issues/2567. if True: # fmt: off @@ -142,73 +127,89 @@ elif unformatted: - # fmt: on - print ( "This won't be formatted" ) - print ( "This won't be formatted either" ) -+ NOT_YET_IMPLEMENTED_StmtFor -+ NOT_IMPLEMENTED_call() ++ for _ in range(1): ++ # fmt: on ++ print("This won't be formatted") ++ print("This won't be formatted either") else: -- print("This will be formatted") -+ NOT_IMPLEMENTED_call() + print("This will be formatted") - - # Regression test for https://github.com/psf/black/issues/3184. --class A: -- async def call(param): -- if param: -- # fmt: off +@@ -52,14 +51,12 @@ + async def call(param): + if param: + # fmt: off - if param[0:4] in ( - "ABCD", "EFGH" - ) : -- # fmt: on ++ if param[0:4] in ("ABCD", "EFGH"): + # fmt: on - print ( "This won't be formatted" ) -- -- elif param[0:4] in ("ZZZZ",): ++ print("This won't be formatted") + + elif param[0:4] in ("ZZZZ",): - print ( "This won't be formatted either" ) -- -- print("This will be formatted") -+NOT_YET_IMPLEMENTED_StmtClassDef ++ print("This won't be formatted either") + print("This will be formatted") - # Regression test for https://github.com/psf/black/issues/2985. --class Named(t.Protocol): -- # fmt: off -- @property +@@ -68,13 +65,13 @@ + class Named(t.Protocol): + # fmt: off + @property - def this_wont_be_formatted ( self ) -> str: ... -+NOT_YET_IMPLEMENTED_StmtClassDef ++ def this_wont_be_formatted(self) -> str: ++ ... --class Factory(t.Protocol): -- def this_will_be_formatted(self, **kwargs) -> Named: -- ... + class Factory(t.Protocol): + def this_will_be_formatted(self, **kwargs) -> Named: + ... - -- # fmt: on -+NOT_YET_IMPLEMENTED_StmtClassDef + # fmt: on - # Regression test for https://github.com/psf/black/issues/3436. +@@ -82,6 +79,6 @@ if x: return x # fmt: off -elif unformatted: +elif unformatted: # fmt: on -- will_be_formatted() -+ NOT_IMPLEMENTED_call() + will_be_formatted() ``` ## Ruff Output ```py # Regression test for https://github.com/psf/black/issues/3129. -NOT_IMPLEMENTED_call() +setup( + entry_points={ + # fmt: off + "console_scripts": [ + "foo-bar" "=foo.bar.:main", + # fmt: on + ] # Includes an formatted indentation. + }, +) # Regression test for https://github.com/psf/black/issues/2015. -NOT_IMPLEMENTED_call() +run( + # fmt: off + [ + "ls", + "-la", + ] + # fmt: on + + path, + check=True, +) # Regression test for https://github.com/psf/black/issues/3026. def test_func(): # yapf: disable - if NOT_IMPLEMENTED_call(): + if unformatted(args): return True # yapf: enable elif b: @@ -220,21 +221,41 @@ def test_func(): # Regression test for https://github.com/psf/black/issues/2567. if True: # fmt: off - NOT_YET_IMPLEMENTED_StmtFor - NOT_IMPLEMENTED_call() + for _ in range(1): + # fmt: on + print("This won't be formatted") + print("This won't be formatted either") else: - NOT_IMPLEMENTED_call() + print("This will be formatted") # Regression test for https://github.com/psf/black/issues/3184. -NOT_YET_IMPLEMENTED_StmtClassDef +class A: + async def call(param): + if param: + # fmt: off + if param[0:4] in ("ABCD", "EFGH"): + # fmt: on + print("This won't be formatted") + + elif param[0:4] in ("ZZZZ",): + print("This won't be formatted either") + + print("This will be formatted") # Regression test for https://github.com/psf/black/issues/2985. -NOT_YET_IMPLEMENTED_StmtClassDef +class Named(t.Protocol): + # fmt: off + @property + def this_wont_be_formatted(self) -> str: + ... -NOT_YET_IMPLEMENTED_StmtClassDef +class Factory(t.Protocol): + def this_will_be_formatted(self, **kwargs) -> Named: + ... + # fmt: on # Regression test for https://github.com/psf/black/issues/3436. @@ -243,7 +264,7 @@ if x: # fmt: off elif unformatted: # fmt: on - NOT_IMPLEMENTED_call() + will_be_formatted() ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip.py.snap similarity index 77% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip.py.snap index 47305ef7ac..3c9f58710b 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtskip.py --- ## Input @@ -17,9 +16,8 @@ d = 5 --- Black +++ Ruff @@ -1,3 +1,3 @@ --a, b = 1, 2 + a, b = 1, 2 -c = 6 # fmt: skip -+(1, 2) = (1, 2) +c = 6 # fmt: skip d = 5 ``` @@ -27,7 +25,7 @@ d = 5 ## Ruff Output ```py -(1, 2) = (1, 2) +a, b = 1, 2 c = 6 # fmt: skip d = 5 ``` diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip2_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip2.py.snap similarity index 52% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip2_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip2.py.snap index 79cc4331e4..6b686d5412 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip2_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip2.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtskip2.py --- ## Input @@ -16,48 +15,38 @@ l3 = ["I have", "trailing comma", "so I should be braked",] ```diff --- Black +++ Ruff -@@ -1,11 +1,15 @@ - l1 = [ -- "This list should be broken up", -- "into multiple lines", -- "because it is way too long", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", +@@ -3,7 +3,11 @@ + "into multiple lines", + "because it is way too long", ] -l2 = ["But this list shouldn't", "even though it also has", "way too many characters in it"] # fmt: skip +l2 = [ -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", ++ "But this list shouldn't", ++ "even though it also has", ++ "way too many characters in it", +] # fmt: skip l3 = [ -- "I have", -- "trailing comma", -- "so I should be braked", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", -+ "NOT_YET_IMPLEMENTED_STRING", - ] + "I have", + "trailing comma", ``` ## Ruff Output ```py l1 = [ - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", + "This list should be broken up", + "into multiple lines", + "because it is way too long", ] l2 = [ - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", + "But this list shouldn't", + "even though it also has", + "way too many characters in it", ] # fmt: skip l3 = [ - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", - "NOT_YET_IMPLEMENTED_STRING", + "I have", + "trailing comma", + "so I should be braked", ] ``` diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip3_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip3.py.snap similarity index 67% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip3_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip3.py.snap index 1a16430854..002454e8da 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip3_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip3.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtskip3.py --- ## Input @@ -20,20 +19,16 @@ f = ["This is a very long line that should be formatted into a clearer line ", " ```diff --- Black +++ Ruff -@@ -1,10 +1,7 @@ +@@ -1,7 +1,7 @@ a = 3 # fmt: off -b, c = 1, 2 -d = 6 # fmt: skip -+(1, 2) = (1, 2) ++b, c = 1, 2 +d = 6 # fmt: skip e = 5 # fmt: on --f = [ -- "This is a very long line that should be formatted into a clearer line ", -- "by rearranging.", --] -+f = ["NOT_YET_IMPLEMENTED_STRING", "NOT_YET_IMPLEMENTED_STRING"] + f = [ ``` ## Ruff Output @@ -41,11 +36,14 @@ f = ["This is a very long line that should be formatted into a clearer line ", " ```py a = 3 # fmt: off -(1, 2) = (1, 2) +b, c = 1, 2 d = 6 # fmt: skip e = 5 # fmt: on -f = ["NOT_YET_IMPLEMENTED_STRING", "NOT_YET_IMPLEMENTED_STRING"] +f = [ + "This is a very long line that should be formatted into a clearer line ", + "by rearranging.", +] ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip5_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip5.py.snap similarity index 54% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip5_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip5.py.snap index 9f346191a9..58985692cd 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip5_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip5.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtskip5.py --- ## Input @@ -22,30 +21,29 @@ else: ```diff --- Black +++ Ruff -@@ -1,9 +1,5 @@ --a, b, c = 3, 4, 5 --if ( -- a == 3 +@@ -1,7 +1,7 @@ + a, b, c = 3, 4, 5 + if ( + a == 3 - and b != 9 # fmt: skip -- and c is not None --): -- print("I'm good!") -+(1, 2) = (1, 2) -+if NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: -+ NOT_IMPLEMENTED_call() - else: -- print("I'm bad") -+ NOT_IMPLEMENTED_call() ++ and b != 9 # fmt: skip + and c is not None + ): + print("I'm good!") ``` ## Ruff Output ```py -(1, 2) = (1, 2) -if NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2: - NOT_IMPLEMENTED_call() +a, b, c = 3, 4, 5 +if ( + a == 3 + and b != 9 # fmt: skip + and c is not None +): + print("I'm good!") else: - NOT_IMPLEMENTED_call() + print("I'm bad") ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip7_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip7.py.snap similarity index 70% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip7_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip7.py.snap index f088f1dd20..cd13d57248 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip7_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip7.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtskip7.py --- ## Input @@ -18,22 +17,20 @@ d = "thisisasuperlongstringthisisasuperlongstringthisisasuperlongstringthisisasu --- Black +++ Ruff @@ -1,4 +1,4 @@ --a = "this is some code" + a = "this is some code" -b = 5 # fmt:skip -+a = "NOT_YET_IMPLEMENTED_STRING" +b = 5 # fmt:skip c = 9 # fmt: skip --d = "thisisasuperlongstringthisisasuperlongstringthisisasuperlongstringthisisasuperlongstring" # fmt:skip -+d = "NOT_YET_IMPLEMENTED_STRING" # fmt:skip + d = "thisisasuperlongstringthisisasuperlongstringthisisasuperlongstringthisisasuperlongstring" # fmt:skip ``` ## Ruff Output ```py -a = "NOT_YET_IMPLEMENTED_STRING" +a = "this is some code" b = 5 # fmt:skip c = 9 # fmt: skip -d = "NOT_YET_IMPLEMENTED_STRING" # fmt:skip +d = "thisisasuperlongstringthisisasuperlongstringthisisasuperlongstringthisisasuperlongstring" # fmt:skip ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip8_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip8.py.snap similarity index 72% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip8_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip8.py.snap index f8d1d415c3..4d65173c25 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip8_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtskip8.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtskip8.py --- ## Input @@ -75,62 +74,57 @@ async def test_async_with(): ```diff --- Black +++ Ruff -@@ -1,62 +1,46 @@ +@@ -1,62 +1,62 @@ # Make sure a leading comment is not removed. -def some_func( unformatted, args ): # fmt: skip -- print("I am some_func") +def some_func(unformatted, args): # fmt: skip -+ NOT_IMPLEMENTED_call() + print("I am some_func") return 0 # Make sure this comment is not removed. # Make sure a leading comment is not removed. -async def some_async_func( unformatted, args): # fmt: skip -- print("I am some_async_func") -- await asyncio.sleep(1) +async def some_async_func(unformatted, args): # fmt: skip -+ NOT_IMPLEMENTED_call() -+ await NOT_IMPLEMENTED_call() + print("I am some_async_func") + await asyncio.sleep(1) # Make sure a leading comment is not removed. -class SomeClass( Unformatted, SuperClasses ): # fmt: skip - def some_method( self, unformatted, args ): # fmt: skip -- print("I am some_method") -- return 0 -- ++class SomeClass(Unformatted, SuperClasses): # fmt: skip ++ def some_method(self, unformatted, args): # fmt: skip + print("I am some_method") + return 0 + - async def some_async_method( self, unformatted, args ): # fmt: skip -- print("I am some_async_method") -- await asyncio.sleep(1) -+NOT_YET_IMPLEMENTED_StmtClassDef ++ async def some_async_method(self, unformatted, args): # fmt: skip + print("I am some_async_method") + await asyncio.sleep(1) # Make sure a leading comment is not removed. -if unformatted_call( args ): # fmt: skip -- print("First branch") -+if NOT_IMPLEMENTED_call(): # fmt: skip -+ NOT_IMPLEMENTED_call() ++if unformatted_call(args): # fmt: skip + print("First branch") # Make sure this is not removed. -elif another_unformatted_call( args ): # fmt: skip -- print("Second branch") ++elif another_unformatted_call(args): # fmt: skip + print("Second branch") -else : # fmt: skip -- print("Last branch") -+elif NOT_IMPLEMENTED_call(): # fmt: skip -+ NOT_IMPLEMENTED_call() +else: # fmt: skip -+ NOT_IMPLEMENTED_call() + print("Last branch") -while some_condition( unformatted, args ): # fmt: skip -- print("Do something") -+while NOT_IMPLEMENTED_call(): # fmt: skip -+ NOT_IMPLEMENTED_call() ++while some_condition(unformatted, args): # fmt: skip + print("Do something") -for i in some_iter( unformatted, args ): # fmt: skip -- print("Do something") -+NOT_YET_IMPLEMENTED_StmtFor # fmt: skip ++for i in some_iter(unformatted, args): # fmt: skip + print("Do something") async def test_async_for(): @@ -140,23 +134,27 @@ async def test_async_with(): -try : # fmt: skip -- some_call() ++try: ++ # fmt: skip + some_call() -except UnformattedError as ex: # fmt: skip - handle_exception() -finally : # fmt: skip -- finally_call() -+NOT_YET_IMPLEMENTED_StmtTry ++except UnformattedError as ex: # fmt: skip ++ handle_exception() # fmt: skip ++finally: + finally_call() -with give_me_context( unformatted, args ): # fmt: skip -- print("Do something") -+NOT_YET_IMPLEMENTED_StmtWith # fmt: skip ++with give_me_context(unformatted, args): # fmt: skip + print("Do something") async def test_async_with(): - async with give_me_async_context( unformatted, args ): # fmt: skip -- print("Do something") -+ NOT_YET_IMPLEMENTED_StmtAsyncWith # fmt: skip ++ async with give_me_async_context(unformatted, args): # fmt: skip + print("Do something") ``` ## Ruff Output @@ -164,50 +162,66 @@ async def test_async_with(): ```py # Make sure a leading comment is not removed. def some_func(unformatted, args): # fmt: skip - NOT_IMPLEMENTED_call() + print("I am some_func") return 0 # Make sure this comment is not removed. # Make sure a leading comment is not removed. async def some_async_func(unformatted, args): # fmt: skip - NOT_IMPLEMENTED_call() - await NOT_IMPLEMENTED_call() + print("I am some_async_func") + await asyncio.sleep(1) # Make sure a leading comment is not removed. -NOT_YET_IMPLEMENTED_StmtClassDef +class SomeClass(Unformatted, SuperClasses): # fmt: skip + def some_method(self, unformatted, args): # fmt: skip + print("I am some_method") + return 0 + + async def some_async_method(self, unformatted, args): # fmt: skip + print("I am some_async_method") + await asyncio.sleep(1) # Make sure a leading comment is not removed. -if NOT_IMPLEMENTED_call(): # fmt: skip - NOT_IMPLEMENTED_call() +if unformatted_call(args): # fmt: skip + print("First branch") # Make sure this is not removed. -elif NOT_IMPLEMENTED_call(): # fmt: skip - NOT_IMPLEMENTED_call() +elif another_unformatted_call(args): # fmt: skip + print("Second branch") else: # fmt: skip - NOT_IMPLEMENTED_call() + print("Last branch") -while NOT_IMPLEMENTED_call(): # fmt: skip - NOT_IMPLEMENTED_call() +while some_condition(unformatted, args): # fmt: skip + print("Do something") -NOT_YET_IMPLEMENTED_StmtFor # fmt: skip +for i in some_iter(unformatted, args): # fmt: skip + print("Do something") async def test_async_for(): NOT_YET_IMPLEMENTED_StmtAsyncFor # fmt: skip -NOT_YET_IMPLEMENTED_StmtTry +try: + # fmt: skip + some_call() +except UnformattedError as ex: # fmt: skip + handle_exception() # fmt: skip +finally: + finally_call() -NOT_YET_IMPLEMENTED_StmtWith # fmt: skip +with give_me_context(unformatted, args): # fmt: skip + print("Do something") async def test_async_with(): - NOT_YET_IMPLEMENTED_StmtAsyncWith # fmt: skip + async with give_me_async_context(unformatted, args): # fmt: skip + print("Do something") ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fstring_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fstring.py.snap similarity index 57% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fstring_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fstring.py.snap index d442a2cf0f..7282dc5589 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fstring_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fstring.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fstring.py --- ## Input @@ -15,6 +14,8 @@ f"{f'''{'nested'} inner'''} outer" f"\"{f'{nested} inner'}\" outer" f"space between opening braces: { {a for a in (1, 2, 3)}}" f'Hello \'{tricky + "example"}\'' +f"Tried directories {str(rootdirs)} \ +but none started with prefix {parentdir_prefix}" ``` ## Black Differences @@ -22,7 +23,7 @@ f'Hello \'{tricky + "example"}\'' ```diff --- Black +++ Ruff -@@ -1,9 +1,9 @@ +@@ -1,11 +1,10 @@ -f"f-string without formatted values is just a string" -f"{{NOT a formatted value}}" -f'{{NOT \'a\' "formatted" "value"}}' @@ -32,29 +33,33 @@ f'Hello \'{tricky + "example"}\'' -f"\"{f'{nested} inner'}\" outer" -f"space between opening braces: { {a for a in (1, 2, 3)}}" -f'Hello \'{tricky + "example"}\'' -+NOT_YET_IMPLEMENTED_ExprJoinedStr -+NOT_YET_IMPLEMENTED_ExprJoinedStr -+NOT_YET_IMPLEMENTED_ExprJoinedStr -+NOT_YET_IMPLEMENTED_ExprJoinedStr -+NOT_YET_IMPLEMENTED_ExprJoinedStr -+NOT_YET_IMPLEMENTED_ExprJoinedStr -+NOT_YET_IMPLEMENTED_ExprJoinedStr -+NOT_YET_IMPLEMENTED_ExprJoinedStr -+NOT_YET_IMPLEMENTED_ExprJoinedStr +-f"Tried directories {str(rootdirs)} \ +-but none started with prefix {parentdir_prefix}" ++f"NOT_YET_IMPLEMENTED_ExprJoinedStr" ++f"NOT_YET_IMPLEMENTED_ExprJoinedStr" ++f"NOT_YET_IMPLEMENTED_ExprJoinedStr" ++f"NOT_YET_IMPLEMENTED_ExprJoinedStr" ++f"NOT_YET_IMPLEMENTED_ExprJoinedStr" ++f"NOT_YET_IMPLEMENTED_ExprJoinedStr" ++f"NOT_YET_IMPLEMENTED_ExprJoinedStr" ++f"NOT_YET_IMPLEMENTED_ExprJoinedStr" ++f"NOT_YET_IMPLEMENTED_ExprJoinedStr" ++f"NOT_YET_IMPLEMENTED_ExprJoinedStr" ``` ## Ruff Output ```py -NOT_YET_IMPLEMENTED_ExprJoinedStr -NOT_YET_IMPLEMENTED_ExprJoinedStr -NOT_YET_IMPLEMENTED_ExprJoinedStr -NOT_YET_IMPLEMENTED_ExprJoinedStr -NOT_YET_IMPLEMENTED_ExprJoinedStr -NOT_YET_IMPLEMENTED_ExprJoinedStr -NOT_YET_IMPLEMENTED_ExprJoinedStr -NOT_YET_IMPLEMENTED_ExprJoinedStr -NOT_YET_IMPLEMENTED_ExprJoinedStr +f"NOT_YET_IMPLEMENTED_ExprJoinedStr" +f"NOT_YET_IMPLEMENTED_ExprJoinedStr" +f"NOT_YET_IMPLEMENTED_ExprJoinedStr" +f"NOT_YET_IMPLEMENTED_ExprJoinedStr" +f"NOT_YET_IMPLEMENTED_ExprJoinedStr" +f"NOT_YET_IMPLEMENTED_ExprJoinedStr" +f"NOT_YET_IMPLEMENTED_ExprJoinedStr" +f"NOT_YET_IMPLEMENTED_ExprJoinedStr" +f"NOT_YET_IMPLEMENTED_ExprJoinedStr" +f"NOT_YET_IMPLEMENTED_ExprJoinedStr" ``` ## Black Output @@ -69,6 +74,8 @@ f"{f'''{'nested'} inner'''} outer" f"\"{f'{nested} inner'}\" outer" f"space between opening braces: { {a for a in (1, 2, 3)}}" f'Hello \'{tricky + "example"}\'' +f"Tried directories {str(rootdirs)} \ +but none started with prefix {parentdir_prefix}" ``` diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function.py.snap similarity index 60% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function.py.snap index 3e8b74e6cf..219714a500 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/function.py --- ## Input @@ -42,7 +41,7 @@ def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r'' def spaces_types(a: int = 1, b: tuple = (), c: list = [], d: dict = {}, e: bool = True, f: int = -1, g: int = 1 if False else 2, h: str = "", i: str = r''): ... def spaces2(result= _core.Value(None)): assert fut is self._read_fut, (fut, self._read_fut) - + # EMPTY LINE WITH WHITESPACE (this comment will be removed) def example(session): result = session.query(models.Customer.id).filter( models.Customer.account_id == account_id, @@ -108,110 +107,39 @@ def __await__(): return (yield) ```diff --- Black +++ Ruff -@@ -1,12 +1,11 @@ - #!/usr/bin/env python3 --import asyncio --import sys +@@ -5,8 +5,7 @@ + from third_party import X, Y, Z + + from library import some_connection, some_decorator - --from third_party import X, Y, Z -+NOT_YET_IMPLEMENTED_StmtImport -+NOT_YET_IMPLEMENTED_StmtImport - --from library import some_connection, some_decorator -+NOT_YET_IMPLEMENTED_StmtImportFrom - -f"trigger 3.6 mode" -+NOT_YET_IMPLEMENTED_StmtImportFrom -+NOT_YET_IMPLEMENTED_ExprJoinedStr ++f"NOT_YET_IMPLEMENTED_ExprJoinedStr" def func_no_args(): -@@ -14,135 +13,86 @@ - b - c - if True: -- raise RuntimeError -+ NOT_YET_IMPLEMENTED_StmtRaise - if False: - ... -- for i in range(10): -- print(i) -- continue -- exec("new-style exec", {}, {}) -+ NOT_YET_IMPLEMENTED_StmtFor -+ NOT_IMPLEMENTED_call() - return None - - - async def coroutine(arg, exec=False): -- "Single-line docstring. Multiline is harder to reformat." -- async with some_connection() as conn: -- await conn.do_what_i_mean("SELECT bobby, tables FROM xkcd", timeout=2) -- await asyncio.sleep(1) -+ "NOT_YET_IMPLEMENTED_STRING" -+ NOT_YET_IMPLEMENTED_StmtAsyncWith -+ await NOT_IMPLEMENTED_call() - - --@asyncio.coroutine --@some_decorator(with_args=True, many_args=[1, 2, 3]) -+@asyncio.NOT_IMPLEMENTED_attr -+@NOT_IMPLEMENTED_call() - def function_signature_stress_test( - number: int, - no_annotation=None, -- text: str = "default", -- *, -+ text: str = "NOT_YET_IMPLEMENTED_STRING", +@@ -41,12 +40,12 @@ debug: bool = False, **kwargs, ) -> str: - return text[number:-1] -+ return NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] ++ return text[number : -1] --def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r""): + def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r""): - offset = attr.ib(default=attr.Factory(lambda: _r.uniform(10000, 200000))) - assert task._cancel_stack[: len(old_stack)] == old_stack -+def spaces( -+ a=1, -+ b=(1, 2), -+ c=[], -+ d={NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value}, -+ e=True, -+ f=NOT_YET_IMPLEMENTED_ExprUnaryOp, -+ g=NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, -+ h="NOT_YET_IMPLEMENTED_STRING", -+ i="NOT_YET_IMPLEMENTED_STRING", -+): -+ offset = NOT_IMPLEMENTED_call() ++ offset = attr.ib(default=attr.Factory(lambda NOT_YET_IMPLEMENTED_lambda: True)) + NOT_YET_IMPLEMENTED_StmtAssert def spaces_types( - a: int = 1, -- b: tuple = (), -+ b: tuple = (1, 2), - c: list = [], -- d: dict = {}, -+ d: dict = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value}, - e: bool = True, -- f: int = -1, -- g: int = 1 if False else 2, -- h: str = "", -- i: str = r"", -+ f: int = NOT_YET_IMPLEMENTED_ExprUnaryOp, -+ g: int = NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, -+ h: str = "NOT_YET_IMPLEMENTED_STRING", -+ i: str = "NOT_YET_IMPLEMENTED_STRING", - ): - ... +@@ -64,19 +63,15 @@ --def spaces2(result=_core.Value(None)): + def spaces2(result=_core.Value(None)): - assert fut is self._read_fut, (fut, self._read_fut) -+def spaces2(result=NOT_IMPLEMENTED_call()): + NOT_YET_IMPLEMENTED_StmtAssert ++ # EMPTY LINE WITH WHITESPACE (this comment will be removed) def example(session): @@ -224,66 +152,14 @@ def __await__(): return (yield) - .order_by(models.Customer.id.asc()) - .all() - ) -+ result = NOT_IMPLEMENTED_call() ++ result = session.query(models.Customer.id).filter( ++ models.Customer.account_id == account_id, ++ models.Customer.email == email_address, ++ ).order_by(models.Customer.id.asc()).all() def long_lines(): - if True: -- typedargslist.extend( -- gen_annotated_params( -- ast_args.kwonlyargs, -- ast_args.kw_defaults, -- parameters, -- implicit_default=True, -- ) -- ) -- typedargslist.extend( -- gen_annotated_params( -- ast_args.kwonlyargs, -- ast_args.kw_defaults, -- parameters, -- implicit_default=True, -- # trailing standalone comment -- ) -- ) -- _type_comment_re = re.compile( -- r""" -- ^ -- [\t ]* -- \#[ ]type:[ ]* -- (?P -- [^#\t\n]+? -- ) -- (? to match -- # a trailing space which is why we need the silliness below -- (? -- (?:\#[^\n]*)? -- \n? -- ) -- $ -- """, -- re.MULTILINE | re.VERBOSE, -- ) -+ NOT_IMPLEMENTED_call() -+ NOT_IMPLEMENTED_call() -+ _type_comment_re = NOT_IMPLEMENTED_call() - - - def trailing_comma(): -- mapping = { -- A: 0.25 * (10.0 / 12), -- B: 0.1 * (10.0 / 12), -- C: 0.1 * (10.0 / 12), -- D: 0.1 * (10.0 / 12), -- } -+ mapping = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} - - - def f( +@@ -135,14 +130,8 @@ a, **kwargs, ) -> A: @@ -306,13 +182,13 @@ def __await__(): return (yield) ```py #!/usr/bin/env python3 -NOT_YET_IMPLEMENTED_StmtImport -NOT_YET_IMPLEMENTED_StmtImport +import asyncio +import sys -NOT_YET_IMPLEMENTED_StmtImportFrom +from third_party import X, Y, Z -NOT_YET_IMPLEMENTED_StmtImportFrom -NOT_YET_IMPLEMENTED_ExprJoinedStr +from library import some_connection, some_decorator +f"NOT_YET_IMPLEMENTED_ExprJoinedStr" def func_no_args(): @@ -320,78 +196,117 @@ def func_no_args(): b c if True: - NOT_YET_IMPLEMENTED_StmtRaise + raise RuntimeError if False: ... - NOT_YET_IMPLEMENTED_StmtFor - NOT_IMPLEMENTED_call() + for i in range(10): + print(i) + continue + exec("new-style exec", {}, {}) return None async def coroutine(arg, exec=False): - "NOT_YET_IMPLEMENTED_STRING" - NOT_YET_IMPLEMENTED_StmtAsyncWith - await NOT_IMPLEMENTED_call() + "Single-line docstring. Multiline is harder to reformat." + async with some_connection() as conn: + await conn.do_what_i_mean("SELECT bobby, tables FROM xkcd", timeout=2) + await asyncio.sleep(1) -@asyncio.NOT_IMPLEMENTED_attr -@NOT_IMPLEMENTED_call() +@asyncio.coroutine +@some_decorator(with_args=True, many_args=[1, 2, 3]) def function_signature_stress_test( number: int, no_annotation=None, - text: str = "NOT_YET_IMPLEMENTED_STRING", + text: str = "default", + *, debug: bool = False, **kwargs, ) -> str: - return NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] + return text[number : -1] -def spaces( - a=1, - b=(1, 2), - c=[], - d={NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value}, - e=True, - f=NOT_YET_IMPLEMENTED_ExprUnaryOp, - g=NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, - h="NOT_YET_IMPLEMENTED_STRING", - i="NOT_YET_IMPLEMENTED_STRING", -): - offset = NOT_IMPLEMENTED_call() +def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r""): + offset = attr.ib(default=attr.Factory(lambda NOT_YET_IMPLEMENTED_lambda: True)) NOT_YET_IMPLEMENTED_StmtAssert def spaces_types( a: int = 1, - b: tuple = (1, 2), + b: tuple = (), c: list = [], - d: dict = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value}, + d: dict = {}, e: bool = True, - f: int = NOT_YET_IMPLEMENTED_ExprUnaryOp, - g: int = NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false, - h: str = "NOT_YET_IMPLEMENTED_STRING", - i: str = "NOT_YET_IMPLEMENTED_STRING", + f: int = -1, + g: int = 1 if False else 2, + h: str = "", + i: str = r"", ): ... -def spaces2(result=NOT_IMPLEMENTED_call()): +def spaces2(result=_core.Value(None)): NOT_YET_IMPLEMENTED_StmtAssert + # EMPTY LINE WITH WHITESPACE (this comment will be removed) def example(session): - result = NOT_IMPLEMENTED_call() + result = session.query(models.Customer.id).filter( + models.Customer.account_id == account_id, + models.Customer.email == email_address, + ).order_by(models.Customer.id.asc()).all() def long_lines(): if True: - NOT_IMPLEMENTED_call() - NOT_IMPLEMENTED_call() - _type_comment_re = NOT_IMPLEMENTED_call() + typedargslist.extend( + gen_annotated_params( + ast_args.kwonlyargs, + ast_args.kw_defaults, + parameters, + implicit_default=True, + ) + ) + typedargslist.extend( + gen_annotated_params( + ast_args.kwonlyargs, + ast_args.kw_defaults, + parameters, + implicit_default=True, + # trailing standalone comment + ) + ) + _type_comment_re = re.compile( + r""" + ^ + [\t ]* + \#[ ]type:[ ]* + (?P + [^#\t\n]+? + ) + (? to match + # a trailing space which is why we need the silliness below + (? + (?:\#[^\n]*)? + \n? + ) + $ + """, + re.MULTILINE | re.VERBOSE, + ) def trailing_comma(): - mapping = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} + mapping = { + A: 0.25 * (10.0 / 12), + B: 0.1 * (10.0 / 12), + C: 0.1 * (10.0 / 12), + D: 0.1 * (10.0 / 12), + } def f( diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function2_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function2.py.snap similarity index 59% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function2_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function2.py.snap index 44b9556da9..23da8b6351 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function2_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function2.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/function2.py --- ## Input @@ -66,83 +65,36 @@ with hmm_but_this_should_get_two_preceding_newlines(): ```diff --- Black +++ Ruff -@@ -2,64 +2,39 @@ - a, - **kwargs, - ) -> A: -- with cache_dir(): -- if something: -- result = CliRunner().invoke( -- black.main, [str(src1), str(src2), "--diff", "--check"] -- ) -- limited.append(-limited.pop()) # negate top -- return A( -- very_long_argument_name1=very_long_value_for_the_argument, -- very_long_argument_name2=-very.long.value.for_the_argument, -- **kwargs, -- ) -+ NOT_YET_IMPLEMENTED_StmtWith -+ NOT_IMPLEMENTED_call() # negate top -+ return NOT_IMPLEMENTED_call() - - - def g(): -- "Docstring." -+ "NOT_YET_IMPLEMENTED_STRING" - - def inner(): - pass - -- print("Inner defs should breathe a little.") -+ NOT_IMPLEMENTED_call() - - - def h(): - def inner(): - pass - -- print("Inner defs should breathe a little.") -+ NOT_IMPLEMENTED_call() - - --if os.name == "posix": -- import termios -+if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: -+ NOT_YET_IMPLEMENTED_StmtImport +@@ -36,7 +36,6 @@ def i_should_be_followed_by_only_one_newline(): pass - --elif os.name == "nt": -- try: -- import msvcrt + elif os.name == "nt": + try: + import msvcrt +@@ -45,21 +44,16 @@ + pass + + except ImportError: - -- def i_should_be_followed_by_only_one_newline(): -- pass -- -- except ImportError: -- -- def i_should_be_followed_by_only_one_newline(): -- pass -+elif NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: -+ NOT_YET_IMPLEMENTED_StmtTry + def i_should_be_followed_by_only_one_newline(): + pass elif False: - -- class IHopeYouAreHavingALovelyDay: -- def __call__(self): -- print("i_should_be_followed_by_only_one_newline") + class IHopeYouAreHavingALovelyDay: + def __call__(self): + print("i_should_be_followed_by_only_one_newline") - -+ NOT_YET_IMPLEMENTED_StmtClassDef else: - def foo(): pass - - --with hmm_but_this_should_get_two_preceding_newlines(): -- pass -+NOT_YET_IMPLEMENTED_StmtWith + + with hmm_but_this_should_get_two_preceding_newlines(): + pass ``` ## Ruff Output @@ -152,42 +104,61 @@ def f( a, **kwargs, ) -> A: - NOT_YET_IMPLEMENTED_StmtWith - NOT_IMPLEMENTED_call() # negate top - return NOT_IMPLEMENTED_call() + with cache_dir(): + if something: + result = CliRunner().invoke( + black.main, [str(src1), str(src2), "--diff", "--check"] + ) + limited.append(-limited.pop()) # negate top + return A( + very_long_argument_name1=very_long_value_for_the_argument, + very_long_argument_name2=-very.long.value.for_the_argument, + **kwargs, + ) def g(): - "NOT_YET_IMPLEMENTED_STRING" + "Docstring." def inner(): pass - NOT_IMPLEMENTED_call() + print("Inner defs should breathe a little.") def h(): def inner(): pass - NOT_IMPLEMENTED_call() + print("Inner defs should breathe a little.") -if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: - NOT_YET_IMPLEMENTED_StmtImport +if os.name == "posix": + import termios def i_should_be_followed_by_only_one_newline(): pass -elif NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: - NOT_YET_IMPLEMENTED_StmtTry +elif os.name == "nt": + try: + import msvcrt + + def i_should_be_followed_by_only_one_newline(): + pass + + except ImportError: + def i_should_be_followed_by_only_one_newline(): + pass elif False: - NOT_YET_IMPLEMENTED_StmtClassDef + class IHopeYouAreHavingALovelyDay: + def __call__(self): + print("i_should_be_followed_by_only_one_newline") else: def foo(): pass -NOT_YET_IMPLEMENTED_StmtWith +with hmm_but_this_should_get_two_preceding_newlines(): + pass ``` ## Black Output diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__import_spacing.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__import_spacing.py.snap new file mode 100644 index 0000000000..f08a097d01 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__import_spacing.py.snap @@ -0,0 +1,213 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/import_spacing.py +--- +## Input + +```py +"""The asyncio package, tracking PEP 3156.""" + +# flake8: noqa + +from logging import ( + WARNING +) +from logging import ( + ERROR, +) +import sys + +# This relies on each of the submodules having an __all__ variable. +from .base_events import * +from .coroutines import * +from .events import * # comment here + +from .futures import * +from .locks import * # comment here +from .protocols import * + +from ..runners import * # comment here +from ..queues import * +from ..streams import * + +from some_library import ( + Just, Enough, Libraries, To, Fit, In, This, Nice, Split, Which, We, No, Longer, Use +) +from name_of_a_company.extremely_long_project_name.component.ttypes import CuteLittleServiceHandlerFactoryyy +from name_of_a_company.extremely_long_project_name.extremely_long_component_name.ttypes import * + +from .a.b.c.subprocess import * +from . import (tasks) +from . import (A, B, C) +from . import SomeVeryLongNameAndAllOfItsAdditionalLetters1, \ + SomeVeryLongNameAndAllOfItsAdditionalLetters2 + +__all__ = ( + base_events.__all__ + + coroutines.__all__ + + events.__all__ + + futures.__all__ + + locks.__all__ + + protocols.__all__ + + runners.__all__ + + queues.__all__ + + streams.__all__ + + tasks.__all__ +) +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -38,7 +38,7 @@ + Use, + ) + from name_of_a_company.extremely_long_project_name.component.ttypes import ( +- CuteLittleServiceHandlerFactoryyy, ++ CuteLittleServiceHandlerFactoryyy + ) + from name_of_a_company.extremely_long_project_name.extremely_long_component_name.ttypes import * + +``` + +## Ruff Output + +```py +"""The asyncio package, tracking PEP 3156.""" + +# flake8: noqa + +from logging import WARNING +from logging import ( + ERROR, +) +import sys + +# This relies on each of the submodules having an __all__ variable. +from .base_events import * +from .coroutines import * +from .events import * # comment here + +from .futures import * +from .locks import * # comment here +from .protocols import * + +from ..runners import * # comment here +from ..queues import * +from ..streams import * + +from some_library import ( + Just, + Enough, + Libraries, + To, + Fit, + In, + This, + Nice, + Split, + Which, + We, + No, + Longer, + Use, +) +from name_of_a_company.extremely_long_project_name.component.ttypes import ( + CuteLittleServiceHandlerFactoryyy +) +from name_of_a_company.extremely_long_project_name.extremely_long_component_name.ttypes import * + +from .a.b.c.subprocess import * +from . import tasks +from . import A, B, C +from . import ( + SomeVeryLongNameAndAllOfItsAdditionalLetters1, + SomeVeryLongNameAndAllOfItsAdditionalLetters2, +) + +__all__ = ( + base_events.__all__ + + coroutines.__all__ + + events.__all__ + + futures.__all__ + + locks.__all__ + + protocols.__all__ + + runners.__all__ + + queues.__all__ + + streams.__all__ + + tasks.__all__ +) +``` + +## Black Output + +```py +"""The asyncio package, tracking PEP 3156.""" + +# flake8: noqa + +from logging import WARNING +from logging import ( + ERROR, +) +import sys + +# This relies on each of the submodules having an __all__ variable. +from .base_events import * +from .coroutines import * +from .events import * # comment here + +from .futures import * +from .locks import * # comment here +from .protocols import * + +from ..runners import * # comment here +from ..queues import * +from ..streams import * + +from some_library import ( + Just, + Enough, + Libraries, + To, + Fit, + In, + This, + Nice, + Split, + Which, + We, + No, + Longer, + Use, +) +from name_of_a_company.extremely_long_project_name.component.ttypes import ( + CuteLittleServiceHandlerFactoryyy, +) +from name_of_a_company.extremely_long_project_name.extremely_long_component_name.ttypes import * + +from .a.b.c.subprocess import * +from . import tasks +from . import A, B, C +from . import ( + SomeVeryLongNameAndAllOfItsAdditionalLetters1, + SomeVeryLongNameAndAllOfItsAdditionalLetters2, +) + +__all__ = ( + base_events.__all__ + + coroutines.__all__ + + events.__all__ + + futures.__all__ + + locks.__all__ + + protocols.__all__ + + runners.__all__ + + queues.__all__ + + streams.__all__ + + tasks.__all__ +) +``` + + diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__power_op_spacing_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__power_op_spacing.py.snap similarity index 51% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__power_op_spacing_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__power_op_spacing.py.snap index 7c3bdb253a..846734242d 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__power_op_spacing_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__power_op_spacing.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/power_op_spacing.py --- ## Input @@ -76,101 +75,42 @@ return np.divide( ```diff --- Black +++ Ruff -@@ -8,56 +8,49 @@ - - - def function_dont_replace_spaces(): -- {**a, **b, **c} -+ {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} - - --a = 5**~4 --b = 5 ** f() --c = -(5**2) --d = 5 ** f["hi"] +@@ -15,7 +15,7 @@ + b = 5 ** f() + c = -(5**2) + d = 5 ** f["hi"] -e = lazy(lambda **kwargs: 5) --f = f() ** 5 --g = a.b**c.d --h = 5 ** funcs.f() --i = funcs.f() ** 5 --j = super().name ** 5 --k = [(2**idx, value) for idx, value in pairs] --l = mod.weights_[0] == pytest.approx(0.95**100, abs=0.001) --m = [([2**63], [1, 2**63])] --n = count <= 10**5 --o = settings(max_examples=10**6) ++e = lazy(lambda NOT_YET_IMPLEMENTED_lambda: True) + f = f() ** 5 + g = a.b**c.d + h = 5 ** funcs.f() +@@ -26,7 +26,7 @@ + m = [([2**63], [1, 2**63])] + n = count <= 10**5 + o = settings(max_examples=10**6) -p = {(k, k**2): v**2 for k, v in pairs} --q = [10**i for i in range(6)] -+a = 5**NOT_YET_IMPLEMENTED_ExprUnaryOp -+b = 5 ** NOT_IMPLEMENTED_call() -+c = NOT_YET_IMPLEMENTED_ExprUnaryOp -+d = 5 ** NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+e = NOT_IMPLEMENTED_call() -+f = NOT_IMPLEMENTED_call() ** 5 -+g = a.NOT_IMPLEMENTED_attr**c.NOT_IMPLEMENTED_attr -+h = 5 ** NOT_IMPLEMENTED_call() -+i = NOT_IMPLEMENTED_call() ** 5 -+j = NOT_IMPLEMENTED_call().NOT_IMPLEMENTED_attr ** 5 -+k = [i for i in []] -+l = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right -+m = [(1, 2)] -+n = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right -+o = NOT_IMPLEMENTED_call() +p = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} -+q = [i for i in []] + q = [10**i for i in range(6)] r = x**y --a = 5.0**~4.0 --b = 5.0 ** f() --c = -(5.0**2.0) --d = 5.0 ** f["hi"] +@@ -34,7 +34,7 @@ + b = 5.0 ** f() + c = -(5.0**2.0) + d = 5.0 ** f["hi"] -e = lazy(lambda **kwargs: 5) --f = f() ** 5.0 --g = a.b**c.d --h = 5.0 ** funcs.f() --i = funcs.f() ** 5.0 --j = super().name ** 5.0 --k = [(2.0**idx, value) for idx, value in pairs] --l = mod.weights_[0] == pytest.approx(0.95**100, abs=0.001) --m = [([2.0**63.0], [1.0, 2**63.0])] --n = count <= 10**5.0 --o = settings(max_examples=10**6.0) ++e = lazy(lambda NOT_YET_IMPLEMENTED_lambda: True) + f = f() ** 5.0 + g = a.b**c.d + h = 5.0 ** funcs.f() +@@ -45,7 +45,7 @@ + m = [([2.0**63.0], [1.0, 2**63.0])] + n = count <= 10**5.0 + o = settings(max_examples=10**6.0) -p = {(k, k**2): v**2.0 for k, v in pairs} --q = [10.5**i for i in range(6)] -+a = 5.0**NOT_YET_IMPLEMENTED_ExprUnaryOp -+b = 5.0 ** NOT_IMPLEMENTED_call() -+c = NOT_YET_IMPLEMENTED_ExprUnaryOp -+d = 5.0 ** NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -+e = NOT_IMPLEMENTED_call() -+f = NOT_IMPLEMENTED_call() ** 5.0 -+g = a.NOT_IMPLEMENTED_attr**c.NOT_IMPLEMENTED_attr -+h = 5.0 ** NOT_IMPLEMENTED_call() -+i = NOT_IMPLEMENTED_call() ** 5.0 -+j = NOT_IMPLEMENTED_call().NOT_IMPLEMENTED_attr ** 5.0 -+k = [i for i in []] -+l = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right -+m = [(1, 2)] -+n = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right -+o = NOT_IMPLEMENTED_call() +p = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} -+q = [i for i in []] + q = [10.5**i for i in range(6)] - # WE SHOULD DEFINITELY NOT EAT THESE COMMENTS (https://github.com/psf/black/issues/2873) --if hasattr(view, "sum_of_weights"): -- return np.divide( # type: ignore[no-any-return] -- view.variance, # type: ignore[union-attr] -- view.sum_of_weights, # type: ignore[union-attr] -- out=np.full(view.sum_of_weights.shape, np.nan), # type: ignore[union-attr] -- where=view.sum_of_weights**2 > view.sum_of_weights_squared, # type: ignore[union-attr] -- ) -+if NOT_IMPLEMENTED_call(): -+ return NOT_IMPLEMENTED_call() - --return np.divide( -- where=view.sum_of_weights_of_weight_long**2 > view.sum_of_weights_squared, # type: ignore --) -+return NOT_IMPLEMENTED_call() ``` ## Ruff Output @@ -186,52 +126,59 @@ def function_replace_spaces(**kwargs): def function_dont_replace_spaces(): - {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} + {**a, **b, **c} -a = 5**NOT_YET_IMPLEMENTED_ExprUnaryOp -b = 5 ** NOT_IMPLEMENTED_call() -c = NOT_YET_IMPLEMENTED_ExprUnaryOp -d = 5 ** NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -e = NOT_IMPLEMENTED_call() -f = NOT_IMPLEMENTED_call() ** 5 -g = a.NOT_IMPLEMENTED_attr**c.NOT_IMPLEMENTED_attr -h = 5 ** NOT_IMPLEMENTED_call() -i = NOT_IMPLEMENTED_call() ** 5 -j = NOT_IMPLEMENTED_call().NOT_IMPLEMENTED_attr ** 5 -k = [i for i in []] -l = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right -m = [(1, 2)] -n = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right -o = NOT_IMPLEMENTED_call() +a = 5**~4 +b = 5 ** f() +c = -(5**2) +d = 5 ** f["hi"] +e = lazy(lambda NOT_YET_IMPLEMENTED_lambda: True) +f = f() ** 5 +g = a.b**c.d +h = 5 ** funcs.f() +i = funcs.f() ** 5 +j = super().name ** 5 +k = [(2**idx, value) for idx, value in pairs] +l = mod.weights_[0] == pytest.approx(0.95**100, abs=0.001) +m = [([2**63], [1, 2**63])] +n = count <= 10**5 +o = settings(max_examples=10**6) p = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} -q = [i for i in []] +q = [10**i for i in range(6)] r = x**y -a = 5.0**NOT_YET_IMPLEMENTED_ExprUnaryOp -b = 5.0 ** NOT_IMPLEMENTED_call() -c = NOT_YET_IMPLEMENTED_ExprUnaryOp -d = 5.0 ** NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] -e = NOT_IMPLEMENTED_call() -f = NOT_IMPLEMENTED_call() ** 5.0 -g = a.NOT_IMPLEMENTED_attr**c.NOT_IMPLEMENTED_attr -h = 5.0 ** NOT_IMPLEMENTED_call() -i = NOT_IMPLEMENTED_call() ** 5.0 -j = NOT_IMPLEMENTED_call().NOT_IMPLEMENTED_attr ** 5.0 -k = [i for i in []] -l = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right -m = [(1, 2)] -n = NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right -o = NOT_IMPLEMENTED_call() +a = 5.0**~4.0 +b = 5.0 ** f() +c = -(5.0**2.0) +d = 5.0 ** f["hi"] +e = lazy(lambda NOT_YET_IMPLEMENTED_lambda: True) +f = f() ** 5.0 +g = a.b**c.d +h = 5.0 ** funcs.f() +i = funcs.f() ** 5.0 +j = super().name ** 5.0 +k = [(2.0**idx, value) for idx, value in pairs] +l = mod.weights_[0] == pytest.approx(0.95**100, abs=0.001) +m = [([2.0**63.0], [1.0, 2**63.0])] +n = count <= 10**5.0 +o = settings(max_examples=10**6.0) p = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} -q = [i for i in []] +q = [10.5**i for i in range(6)] # WE SHOULD DEFINITELY NOT EAT THESE COMMENTS (https://github.com/psf/black/issues/2873) -if NOT_IMPLEMENTED_call(): - return NOT_IMPLEMENTED_call() +if hasattr(view, "sum_of_weights"): + return np.divide( # type: ignore[no-any-return] + view.variance, # type: ignore[union-attr] + view.sum_of_weights, # type: ignore[union-attr] + out=np.full(view.sum_of_weights.shape, np.nan), # type: ignore[union-attr] + where=view.sum_of_weights**2 > view.sum_of_weights_squared, # type: ignore[union-attr] + ) -return NOT_IMPLEMENTED_call() +return np.divide( + where=view.sum_of_weights_of_weight_long**2 > view.sum_of_weights_squared, # type: ignore +) ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_await_parens_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_await_parens.py.snap similarity index 71% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_await_parens_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_await_parens.py.snap index 7923c03bb5..2468093c5f 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_await_parens_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_await_parens.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/remove_await_parens.py --- ## Input @@ -94,31 +93,22 @@ async def main(): ```diff --- Black +++ Ruff -@@ -1,66 +1,57 @@ --import asyncio -+NOT_YET_IMPLEMENTED_StmtImport - - - # Control example - async def main(): -- await asyncio.sleep(1) -+ await NOT_IMPLEMENTED_call() - +@@ -8,28 +8,33 @@ # Remove brackets for short coroutine/task async def main(): - await asyncio.sleep(1) -+ await (NOT_IMPLEMENTED_call()) ++ await (asyncio.sleep(1)) async def main(): - await asyncio.sleep(1) -+ await (NOT_IMPLEMENTED_call()) ++ await (asyncio.sleep(1)) async def main(): - await asyncio.sleep(1) -+ await (NOT_IMPLEMENTED_call()) ++ await (asyncio.sleep(1)) # Check comments @@ -126,69 +116,33 @@ async def main(): - await asyncio.sleep(1) # Hello + ( + await # Hello -+ NOT_IMPLEMENTED_call() ++ asyncio.sleep(1) + ) async def main(): - await asyncio.sleep(1) # Hello -+ ( -+ await ( -+ NOT_IMPLEMENTED_call() # Hello -+ ) ++ await ( ++ asyncio.sleep(1) # Hello + ) async def main(): - await asyncio.sleep(1) # Hello -+ await (NOT_IMPLEMENTED_call()) # Hello ++ await (asyncio.sleep(1)) # Hello # Long lines - async def main(): -- await asyncio.gather( -- asyncio.sleep(1), -- asyncio.sleep(1), -- asyncio.sleep(1), -- asyncio.sleep(1), -- asyncio.sleep(1), -- asyncio.sleep(1), -- asyncio.sleep(1), -- ) -+ await NOT_IMPLEMENTED_call() - - - # Same as above but with magic trailing comma in function - async def main(): -- await asyncio.gather( -- asyncio.sleep(1), -- asyncio.sleep(1), -- asyncio.sleep(1), -- asyncio.sleep(1), -- asyncio.sleep(1), -- asyncio.sleep(1), -- asyncio.sleep(1), -- ) -+ await NOT_IMPLEMENTED_call() - +@@ -60,7 +65,7 @@ # Cr@zY Br@ck3Tz async def main(): - await black(1) -+ await (NOT_IMPLEMENTED_call()) ++ await (black(1)) # Keep brackets around non power operations and nested awaits -@@ -69,7 +60,7 @@ - - - async def main(): -- await (await asyncio.sleep(1)) -+ await (await NOT_IMPLEMENTED_call()) - - - # It's awaits all the way down... -@@ -78,16 +69,16 @@ +@@ -78,16 +83,16 @@ async def main(): @@ -198,12 +152,12 @@ async def main(): async def main(): - await (await asyncio.sleep(1)) -+ await (await (NOT_IMPLEMENTED_call())) ++ await (await (asyncio.sleep(1))) async def main(): - await (await (await (await (await asyncio.sleep(1))))) -+ await (await (await (await (await (NOT_IMPLEMENTED_call()))))) ++ await (await (await (await (await (asyncio.sleep(1)))))) async def main(): @@ -214,60 +168,74 @@ async def main(): ## Ruff Output ```py -NOT_YET_IMPLEMENTED_StmtImport +import asyncio # Control example async def main(): - await NOT_IMPLEMENTED_call() + await asyncio.sleep(1) # Remove brackets for short coroutine/task async def main(): - await (NOT_IMPLEMENTED_call()) + await (asyncio.sleep(1)) async def main(): - await (NOT_IMPLEMENTED_call()) + await (asyncio.sleep(1)) async def main(): - await (NOT_IMPLEMENTED_call()) + await (asyncio.sleep(1)) # Check comments async def main(): ( await # Hello - NOT_IMPLEMENTED_call() + asyncio.sleep(1) ) async def main(): - ( - await ( - NOT_IMPLEMENTED_call() # Hello - ) + await ( + asyncio.sleep(1) # Hello ) async def main(): - await (NOT_IMPLEMENTED_call()) # Hello + await (asyncio.sleep(1)) # Hello # Long lines async def main(): - await NOT_IMPLEMENTED_call() + await asyncio.gather( + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + ) # Same as above but with magic trailing comma in function async def main(): - await NOT_IMPLEMENTED_call() + await asyncio.gather( + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + asyncio.sleep(1), + ) # Cr@zY Br@ck3Tz async def main(): - await (NOT_IMPLEMENTED_call()) + await (black(1)) # Keep brackets around non power operations and nested awaits @@ -276,7 +244,7 @@ async def main(): async def main(): - await (await NOT_IMPLEMENTED_call()) + await (await asyncio.sleep(1)) # It's awaits all the way down... @@ -289,11 +257,11 @@ async def main(): async def main(): - await (await (NOT_IMPLEMENTED_call())) + await (await (asyncio.sleep(1))) async def main(): - await (await (await (await (await (NOT_IMPLEMENTED_call()))))) + await (await (await (await (await (asyncio.sleep(1)))))) async def main(): diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_except_parens_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_except_parens.py.snap similarity index 63% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_except_parens_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_except_parens.py.snap index 21fdd1942d..1b9f9e5d08 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_except_parens_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_except_parens.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/remove_except_parens.py --- ## Input @@ -48,69 +47,62 @@ except (some.really.really.really.looooooooooooooooooooooooooooooooong.module.ov ```diff --- Black +++ Ruff -@@ -1,42 +1,9 @@ - # These brackets are redundant, therefore remove. --try: -- a.something --except AttributeError as err: -- raise err -- --# This is tuple of exceptions. --# Although this could be replaced with just the exception, --# we do not remove brackets to preserve AST. --try: -- a.something --except (AttributeError,) as err: -- raise err -- --# This is a tuple of exceptions. Do not remove brackets. --try: -- a.something --except (AttributeError, ValueError) as err: -- raise err -- --# Test long variants. --try: -- a.something +@@ -21,9 +21,7 @@ + # Test long variants. + try: + a.something -except ( - some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error -) as err: -- raise err -+NOT_YET_IMPLEMENTED_StmtTry -+NOT_YET_IMPLEMENTED_StmtTry -+NOT_YET_IMPLEMENTED_StmtTry -+NOT_YET_IMPLEMENTED_StmtTry ++except some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error as err: + raise err --try: -- a.something --except ( -- some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error, --) as err: -- raise err -+NOT_YET_IMPLEMENTED_StmtTry - --try: -- a.something --except ( -- some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error, -- some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error, --) as err: -- raise err -+NOT_YET_IMPLEMENTED_StmtTry + try: ``` ## Ruff Output ```py # These brackets are redundant, therefore remove. -NOT_YET_IMPLEMENTED_StmtTry -NOT_YET_IMPLEMENTED_StmtTry -NOT_YET_IMPLEMENTED_StmtTry -NOT_YET_IMPLEMENTED_StmtTry +try: + a.something +except AttributeError as err: + raise err -NOT_YET_IMPLEMENTED_StmtTry +# This is tuple of exceptions. +# Although this could be replaced with just the exception, +# we do not remove brackets to preserve AST. +try: + a.something +except (AttributeError,) as err: + raise err -NOT_YET_IMPLEMENTED_StmtTry +# This is a tuple of exceptions. Do not remove brackets. +try: + a.something +except (AttributeError, ValueError) as err: + raise err + +# Test long variants. +try: + a.something +except some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error as err: + raise err + +try: + a.something +except ( + some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error, +) as err: + raise err + +try: + a.something +except ( + some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error, + some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error, +) as err: + raise err ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_for_brackets_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_for_brackets.py.snap similarity index 67% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_for_brackets_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_for_brackets.py.snap index dbdfdc6508..7b10496c12 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_for_brackets_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_for_brackets.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/remove_for_brackets.py --- ## Input @@ -32,57 +31,56 @@ for (((((k, v))))) in d.items(): ```diff --- Black +++ Ruff -@@ -1,27 +1,13 @@ - # Only remove tuple brackets after `for` --for k, v in d.items(): -- print(k, v) -+NOT_YET_IMPLEMENTED_StmtFor - +@@ -5,7 +5,7 @@ # Don't touch tuple brackets after `in` --for module in (core, _unicodefun): -- if hasattr(module, "_verify_python3_env"): + for module in (core, _unicodefun): + if hasattr(module, "_verify_python3_env"): - module._verify_python3_env = lambda: None -+NOT_YET_IMPLEMENTED_StmtFor ++ module._verify_python3_env = lambda NOT_YET_IMPLEMENTED_lambda: True # Brackets remain for long for loop lines --for ( -- why_would_anyone_choose_to_name_a_loop_variable_with_a_name_this_long, -- i_dont_know_but_we_should_still_check_the_behaviour_if_they_do, --) in d.items(): -- print(k, v) -+NOT_YET_IMPLEMENTED_StmtFor - --for ( -- k, -- v, + for ( +@@ -17,9 +17,7 @@ + for ( + k, + v, -) in ( - dfkasdjfldsjflkdsjflkdsjfdslkfjldsjfgkjdshgkljjdsfldgkhsdofudsfudsofajdslkfjdslkfjldisfjdffjsdlkfjdlkjjkdflskadjldkfjsalkfjdasj.items() -): -- print(k, v) -+NOT_YET_IMPLEMENTED_StmtFor ++) in dfkasdjfldsjflkdsjflkdsjfdslkfjldsjfgkjdshgkljjdsfldgkhsdofudsfudsofajdslkfjdslkfjldisfjdffjsdlkfjdlkjjkdflskadjldkfjsalkfjdasj.items(): + print(k, v) # Test deeply nested brackets --for k, v in d.items(): -- print(k, v) -+NOT_YET_IMPLEMENTED_StmtFor ``` ## Ruff Output ```py # Only remove tuple brackets after `for` -NOT_YET_IMPLEMENTED_StmtFor +for k, v in d.items(): + print(k, v) # Don't touch tuple brackets after `in` -NOT_YET_IMPLEMENTED_StmtFor +for module in (core, _unicodefun): + if hasattr(module, "_verify_python3_env"): + module._verify_python3_env = lambda NOT_YET_IMPLEMENTED_lambda: True # Brackets remain for long for loop lines -NOT_YET_IMPLEMENTED_StmtFor +for ( + why_would_anyone_choose_to_name_a_loop_variable_with_a_name_this_long, + i_dont_know_but_we_should_still_check_the_behaviour_if_they_do, +) in d.items(): + print(k, v) -NOT_YET_IMPLEMENTED_StmtFor +for ( + k, + v, +) in dfkasdjfldsjflkdsjflkdsjfdslkfjldsjfgkjdshgkljjdsfldgkhsdofudsfudsofajdslkfjdslkfjldisfjdffjsdlkfjdlkjjkdflskadjldkfjsalkfjdasj.items(): + print(k, v) # Test deeply nested brackets -NOT_YET_IMPLEMENTED_StmtFor +for k, v in d.items(): + print(k, v) ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_newline_after_code_block_open_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_newline_after_code_block_open.py.snap similarity index 55% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_newline_after_code_block_open_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_newline_after_code_block_open.py.snap index 79f86d3478..a47f6ddb56 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_newline_after_code_block_open_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_newline_after_code_block_open.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/remove_newline_after_code_block_open.py --- ## Input @@ -121,181 +120,109 @@ with open("/path/to/file.txt", mode="r") as read_file: ```diff --- Black +++ Ruff -@@ -1,78 +1,68 @@ --import random -+NOT_YET_IMPLEMENTED_StmtImport +@@ -27,16 +27,16 @@ - def foo1(): -- print("The newline above me should be deleted!") -+ NOT_IMPLEMENTED_call() - - - def foo2(): -- print("All the newlines above me should be deleted!") -+ NOT_IMPLEMENTED_call() - - - def foo3(): -- print("No newline above me!") -+ NOT_IMPLEMENTED_call() - -- print("There is a newline above me, and that's OK!") -+ NOT_IMPLEMENTED_call() - - - def foo4(): - # There is a comment here - -- print("The newline above me should not be deleted!") -+ NOT_IMPLEMENTED_call() - - --class Foo: -- def bar(self): -- print("The newline above me should be deleted!") -+NOT_YET_IMPLEMENTED_StmtClassDef - - --for i in range(5): + for i in range(5): - print(f"{i}) The line above me should be removed!") -+NOT_YET_IMPLEMENTED_StmtFor ++ print(f"NOT_YET_IMPLEMENTED_ExprJoinedStr") --for i in range(5): + for i in range(5): - print(f"{i}) The lines above me should be removed!") -+NOT_YET_IMPLEMENTED_StmtFor ++ print(f"NOT_YET_IMPLEMENTED_ExprJoinedStr") --for i in range(5): -- for j in range(7): + for i in range(5): + for j in range(7): - print(f"{i}) The lines above me should be removed!") -+NOT_YET_IMPLEMENTED_StmtFor ++ print(f"NOT_YET_IMPLEMENTED_ExprJoinedStr") --if random.randint(0, 3) == 0: -- print("The new line above me is about to be removed!") -+if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: -+ NOT_IMPLEMENTED_call() - - --if random.randint(0, 3) == 0: -- print("The new lines above me is about to be removed!") -+if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: -+ NOT_IMPLEMENTED_call() - - --if random.randint(0, 3) == 0: -- if random.uniform(0, 1) > 0.5: -- print("Two lines above me are about to be removed!") -+if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: -+ if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: -+ NOT_IMPLEMENTED_call() - - - while True: -- print("The newline above me should be deleted!") -+ NOT_IMPLEMENTED_call() - - - while True: -- print("The newlines above me should be deleted!") -+ NOT_IMPLEMENTED_call() - - - while True: - while False: -- print("The newlines above me should be deleted!") -+ NOT_IMPLEMENTED_call() - - --with open("/path/to/file.txt", mode="w") as file: -- file.write("The new line above me is about to be removed!") -+NOT_YET_IMPLEMENTED_StmtWith - - --with open("/path/to/file.txt", mode="w") as file: -- file.write("The new lines above me is about to be removed!") -+NOT_YET_IMPLEMENTED_StmtWith - - --with open("/path/to/file.txt", mode="r") as read_file: -- with open("/path/to/output_file.txt", mode="w") as write_file: -- write_file.writelines(read_file.readlines()) -+NOT_YET_IMPLEMENTED_StmtWith + if random.randint(0, 3) == 0: ``` ## Ruff Output ```py -NOT_YET_IMPLEMENTED_StmtImport +import random def foo1(): - NOT_IMPLEMENTED_call() + print("The newline above me should be deleted!") def foo2(): - NOT_IMPLEMENTED_call() + print("All the newlines above me should be deleted!") def foo3(): - NOT_IMPLEMENTED_call() + print("No newline above me!") - NOT_IMPLEMENTED_call() + print("There is a newline above me, and that's OK!") def foo4(): # There is a comment here - NOT_IMPLEMENTED_call() + print("The newline above me should not be deleted!") -NOT_YET_IMPLEMENTED_StmtClassDef +class Foo: + def bar(self): + print("The newline above me should be deleted!") -NOT_YET_IMPLEMENTED_StmtFor +for i in range(5): + print(f"NOT_YET_IMPLEMENTED_ExprJoinedStr") -NOT_YET_IMPLEMENTED_StmtFor +for i in range(5): + print(f"NOT_YET_IMPLEMENTED_ExprJoinedStr") -NOT_YET_IMPLEMENTED_StmtFor +for i in range(5): + for j in range(7): + print(f"NOT_YET_IMPLEMENTED_ExprJoinedStr") -if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: - NOT_IMPLEMENTED_call() +if random.randint(0, 3) == 0: + print("The new line above me is about to be removed!") -if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: - NOT_IMPLEMENTED_call() +if random.randint(0, 3) == 0: + print("The new lines above me is about to be removed!") -if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: - if NOT_IMPLEMENTED_left < NOT_IMPLEMENTED_right: - NOT_IMPLEMENTED_call() +if random.randint(0, 3) == 0: + if random.uniform(0, 1) > 0.5: + print("Two lines above me are about to be removed!") while True: - NOT_IMPLEMENTED_call() + print("The newline above me should be deleted!") while True: - NOT_IMPLEMENTED_call() + print("The newlines above me should be deleted!") while True: while False: - NOT_IMPLEMENTED_call() + print("The newlines above me should be deleted!") -NOT_YET_IMPLEMENTED_StmtWith +with open("/path/to/file.txt", mode="w") as file: + file.write("The new line above me is about to be removed!") -NOT_YET_IMPLEMENTED_StmtWith +with open("/path/to/file.txt", mode="w") as file: + file.write("The new lines above me is about to be removed!") -NOT_YET_IMPLEMENTED_StmtWith +with open("/path/to/file.txt", mode="r") as read_file: + with open("/path/to/output_file.txt", mode="w") as write_file: + write_file.writelines(read_file.readlines()) ``` ## Black Output diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_parens_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_parens.py.snap similarity index 86% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_parens_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_parens.py.snap index 5e1e16cd2e..b7afd62f41 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_parens_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_parens.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/remove_parens.py --- ## Input @@ -68,42 +67,25 @@ def example8(): ```diff --- Black +++ Ruff -@@ -1,24 +1,16 @@ - x = 1 - x = 1.2 - --data = ( -- "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" --).encode() -+data = NOT_IMPLEMENTED_call() - - - async def show_status(): +@@ -10,9 +10,7 @@ while True: -- try: -- if report_host: + try: + if report_host: - data = ( - f"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - ).encode() -- except Exception as e: -- pass -+ NOT_YET_IMPLEMENTED_StmtTry ++ data = (f"NOT_YET_IMPLEMENTED_ExprJoinedStr").encode() + except Exception as e: + pass - - def example(): -- return "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -+ return "NOT_YET_IMPLEMENTED_STRING" - - - def example1(): -@@ -30,15 +22,11 @@ +@@ -30,15 +28,11 @@ def example2(): - return ( - "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - ) -+ return "NOT_YET_IMPLEMENTED_STRING" ++ return "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" def example3(): @@ -114,12 +96,7 @@ def example8(): def example4(): -@@ -46,39 +34,15 @@ - - - def example5(): -- return () -+ return (1, 2) +@@ -50,35 +44,11 @@ def example6(): @@ -165,16 +142,22 @@ def example8(): x = 1 x = 1.2 -data = NOT_IMPLEMENTED_call() +data = ( + "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" +).encode() async def show_status(): while True: - NOT_YET_IMPLEMENTED_StmtTry + try: + if report_host: + data = (f"NOT_YET_IMPLEMENTED_ExprJoinedStr").encode() + except Exception as e: + pass def example(): - return "NOT_YET_IMPLEMENTED_STRING" + return "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" def example1(): @@ -186,7 +169,7 @@ def example1point5(): def example2(): - return "NOT_YET_IMPLEMENTED_STRING" + return "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" def example3(): @@ -198,7 +181,7 @@ def example4(): def example5(): - return (1, 2) + return () def example6(): diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__return_annotation_brackets_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__return_annotation_brackets.py.snap similarity index 88% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__return_annotation_brackets_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__return_annotation_brackets.py.snap index 404098c431..c35111a8b1 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__return_annotation_brackets_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__return_annotation_brackets.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/return_annotation_brackets.py --- ## Input @@ -123,36 +122,6 @@ def foo() -> tuple[int, int, int,]: return 2 -@@ -95,26 +99,14 @@ - - - # Return type with commas --def foo() -> tuple[int, int, int]: -+def foo() -> NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key]: - return 2 - - --def foo() -> ( -- tuple[ -- loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, -- loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, -- loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, -- ] --): -+def foo() -> NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key]: - return 2 - - - # Magic trailing comma example --def foo() -> ( -- tuple[ -- int, -- int, -- int, -- ] --): -+def foo() -> NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key]: - return 2 ``` ## Ruff Output @@ -259,16 +228,28 @@ def foo() -> ( # Return type with commas -def foo() -> NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key]: +def foo() -> tuple[int, int, int]: return 2 -def foo() -> NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key]: +def foo() -> ( + tuple[ + loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, + loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, + loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, + ] +): return 2 # Magic trailing comma example -def foo() -> NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key]: +def foo() -> ( + tuple[ + int, + int, + int, + ] +): return 2 ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__slices.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__slices.py.snap new file mode 100644 index 0000000000..08f07ad1d3 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__slices.py.snap @@ -0,0 +1,158 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/slices.py +--- +## Input + +```py +slice[a.b : c.d] +slice[d :: d + 1] +slice[d + 1 :: d] +slice[d::d] +slice[0] +slice[-1] +slice[:-1] +slice[::-1] +slice[:c, c - 1] +slice[c, c + 1, d::] +slice[ham[c::d] :: 1] +slice[ham[cheese**2 : -1] : 1 : 1, ham[1:2]] +slice[:-1:] +slice[lambda: None : lambda: None] +slice[lambda x, y, *args, really=2, **kwargs: None :, None::] +slice[1 or 2 : True and False] +slice[not so_simple : 1 < val <= 10] +slice[(1 for i in range(42)) : x] +slice[:: [i for i in range(42)]] + + +async def f(): + slice[await x : [i async for i in arange(42)] : 42] + + +# These are from PEP-8: +ham[1:9], ham[1:9:3], ham[:9:3], ham[1::3], ham[1:9:] +ham[lower:upper], ham[lower:upper:], ham[lower::step] +# ham[lower+offset : upper+offset] +ham[: upper_fn(x) : step_fn(x)], ham[:: step_fn(x)] +ham[lower + offset : upper + offset] +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -4,19 +4,21 @@ + slice[d::d] + slice[0] + slice[-1] +-slice[:-1] +-slice[::-1] ++slice[ : -1] ++slice[ :: -1] + slice[:c, c - 1] + slice[c, c + 1, d::] + slice[ham[c::d] :: 1] + slice[ham[cheese**2 : -1] : 1 : 1, ham[1:2]] +-slice[:-1:] +-slice[lambda: None : lambda: None] +-slice[lambda x, y, *args, really=2, **kwargs: None :, None::] ++slice[ : -1 :] ++slice[lambda NOT_YET_IMPLEMENTED_lambda: True : lambda NOT_YET_IMPLEMENTED_lambda: True] ++slice[lambda NOT_YET_IMPLEMENTED_lambda: True :, None::] + slice[1 or 2 : True and False] + slice[not so_simple : 1 < val <= 10] +-slice[(1 for i in range(42)) : x] +-slice[:: [i for i in range(42)]] ++slice[ ++ (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) : x ++] ++slice[ :: [i for i in range(42)]] + + + async def f(): +@@ -27,5 +29,5 @@ + ham[1:9], ham[1:9:3], ham[:9:3], ham[1::3], ham[1:9:] + ham[lower:upper], ham[lower:upper:], ham[lower::step] + # ham[lower+offset : upper+offset] +-ham[: upper_fn(x) : step_fn(x)], ham[:: step_fn(x)] ++ham[ : upper_fn(x) : step_fn(x)], ham[ :: step_fn(x)] + ham[lower + offset : upper + offset] +``` + +## Ruff Output + +```py +slice[a.b : c.d] +slice[d :: d + 1] +slice[d + 1 :: d] +slice[d::d] +slice[0] +slice[-1] +slice[ : -1] +slice[ :: -1] +slice[:c, c - 1] +slice[c, c + 1, d::] +slice[ham[c::d] :: 1] +slice[ham[cheese**2 : -1] : 1 : 1, ham[1:2]] +slice[ : -1 :] +slice[lambda NOT_YET_IMPLEMENTED_lambda: True : lambda NOT_YET_IMPLEMENTED_lambda: True] +slice[lambda NOT_YET_IMPLEMENTED_lambda: True :, None::] +slice[1 or 2 : True and False] +slice[not so_simple : 1 < val <= 10] +slice[ + (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) : x +] +slice[ :: [i for i in range(42)]] + + +async def f(): + slice[await x : [i async for i in arange(42)] : 42] + + +# These are from PEP-8: +ham[1:9], ham[1:9:3], ham[:9:3], ham[1::3], ham[1:9:] +ham[lower:upper], ham[lower:upper:], ham[lower::step] +# ham[lower+offset : upper+offset] +ham[ : upper_fn(x) : step_fn(x)], ham[ :: step_fn(x)] +ham[lower + offset : upper + offset] +``` + +## Black Output + +```py +slice[a.b : c.d] +slice[d :: d + 1] +slice[d + 1 :: d] +slice[d::d] +slice[0] +slice[-1] +slice[:-1] +slice[::-1] +slice[:c, c - 1] +slice[c, c + 1, d::] +slice[ham[c::d] :: 1] +slice[ham[cheese**2 : -1] : 1 : 1, ham[1:2]] +slice[:-1:] +slice[lambda: None : lambda: None] +slice[lambda x, y, *args, really=2, **kwargs: None :, None::] +slice[1 or 2 : True and False] +slice[not so_simple : 1 < val <= 10] +slice[(1 for i in range(42)) : x] +slice[:: [i for i in range(42)]] + + +async def f(): + slice[await x : [i async for i in arange(42)] : 42] + + +# These are from PEP-8: +ham[1:9], ham[1:9:3], ham[:9:3], ham[1::3], ham[1:9:] +ham[lower:upper], ham[lower:upper:], ham[lower::step] +# ham[lower+offset : upper+offset] +ham[: upper_fn(x) : step_fn(x)], ham[:: step_fn(x)] +ham[lower + offset : upper + offset] +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__string_prefixes.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__string_prefixes.py.snap new file mode 100644 index 0000000000..5d5f5028ad --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__string_prefixes.py.snap @@ -0,0 +1,141 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/string_prefixes.py +--- +## Input + +```py +#!/usr/bin/env python3 + +name = "Łukasz" +(f"hello {name}", F"hello {name}") +(b"", B"") +(u"", U"") +(r"", R"") + +(rf"", fr"", Rf"", fR"", rF"", Fr"", RF"", FR"") +(rb"", br"", Rb"", bR"", rB"", Br"", RB"", BR"") + + +def docstring_singleline(): + R"""2020 was one hell of a year. The good news is that we were able to""" + + +def docstring_multiline(): + R""" + clear out all of the issues opened in that time :p + """ +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1,13 +1,31 @@ + #!/usr/bin/env python3 + + name = "Łukasz" +-(f"hello {name}", f"hello {name}") +-(b"", b"") ++(f"NOT_YET_IMPLEMENTED_ExprJoinedStr", f"NOT_YET_IMPLEMENTED_ExprJoinedStr") ++(b"NOT_YET_IMPLEMENTED_BYTE_STRING", b"NOT_YET_IMPLEMENTED_BYTE_STRING") + ("", "") + (r"", R"") + +-(rf"", rf"", Rf"", Rf"", rf"", rf"", Rf"", Rf"") +-(rb"", rb"", Rb"", Rb"", rb"", rb"", Rb"", Rb"") ++( ++ f"NOT_YET_IMPLEMENTED_ExprJoinedStr", ++ f"NOT_YET_IMPLEMENTED_ExprJoinedStr", ++ f"NOT_YET_IMPLEMENTED_ExprJoinedStr", ++ f"NOT_YET_IMPLEMENTED_ExprJoinedStr", ++ f"NOT_YET_IMPLEMENTED_ExprJoinedStr", ++ f"NOT_YET_IMPLEMENTED_ExprJoinedStr", ++ f"NOT_YET_IMPLEMENTED_ExprJoinedStr", ++ f"NOT_YET_IMPLEMENTED_ExprJoinedStr", ++) ++( ++ b"NOT_YET_IMPLEMENTED_BYTE_STRING", ++ b"NOT_YET_IMPLEMENTED_BYTE_STRING", ++ b"NOT_YET_IMPLEMENTED_BYTE_STRING", ++ b"NOT_YET_IMPLEMENTED_BYTE_STRING", ++ b"NOT_YET_IMPLEMENTED_BYTE_STRING", ++ b"NOT_YET_IMPLEMENTED_BYTE_STRING", ++ b"NOT_YET_IMPLEMENTED_BYTE_STRING", ++ b"NOT_YET_IMPLEMENTED_BYTE_STRING", ++) + + + def docstring_singleline(): +``` + +## Ruff Output + +```py +#!/usr/bin/env python3 + +name = "Łukasz" +(f"NOT_YET_IMPLEMENTED_ExprJoinedStr", f"NOT_YET_IMPLEMENTED_ExprJoinedStr") +(b"NOT_YET_IMPLEMENTED_BYTE_STRING", b"NOT_YET_IMPLEMENTED_BYTE_STRING") +("", "") +(r"", R"") + +( + f"NOT_YET_IMPLEMENTED_ExprJoinedStr", + f"NOT_YET_IMPLEMENTED_ExprJoinedStr", + f"NOT_YET_IMPLEMENTED_ExprJoinedStr", + f"NOT_YET_IMPLEMENTED_ExprJoinedStr", + f"NOT_YET_IMPLEMENTED_ExprJoinedStr", + f"NOT_YET_IMPLEMENTED_ExprJoinedStr", + f"NOT_YET_IMPLEMENTED_ExprJoinedStr", + f"NOT_YET_IMPLEMENTED_ExprJoinedStr", +) +( + b"NOT_YET_IMPLEMENTED_BYTE_STRING", + b"NOT_YET_IMPLEMENTED_BYTE_STRING", + b"NOT_YET_IMPLEMENTED_BYTE_STRING", + b"NOT_YET_IMPLEMENTED_BYTE_STRING", + b"NOT_YET_IMPLEMENTED_BYTE_STRING", + b"NOT_YET_IMPLEMENTED_BYTE_STRING", + b"NOT_YET_IMPLEMENTED_BYTE_STRING", + b"NOT_YET_IMPLEMENTED_BYTE_STRING", +) + + +def docstring_singleline(): + R"""2020 was one hell of a year. The good news is that we were able to""" + + +def docstring_multiline(): + R""" + clear out all of the issues opened in that time :p + """ +``` + +## Black Output + +```py +#!/usr/bin/env python3 + +name = "Łukasz" +(f"hello {name}", f"hello {name}") +(b"", b"") +("", "") +(r"", R"") + +(rf"", rf"", Rf"", Rf"", rf"", rf"", Rf"", Rf"") +(rb"", rb"", Rb"", Rb"", rb"", rb"", Rb"", Rb"") + + +def docstring_singleline(): + R"""2020 was one hell of a year. The good news is that we were able to""" + + +def docstring_multiline(): + R""" + clear out all of the issues opened in that time :p + """ +``` + + diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__torture_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__torture.py.snap similarity index 80% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__torture_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__torture.py.snap index 0aee58f5ca..058819773d 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__torture_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__torture.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/torture.py --- ## Input @@ -42,11 +41,9 @@ assert ( ```diff --- Black +++ Ruff -@@ -1,58 +1,22 @@ - importA +@@ -2,20 +2,10 @@ ( -- () -+ (1, 2) + () << 0 - ** 101234234242352525425252352352525234890264906820496920680926538059059209922523523525 + **101234234242352525425252352352525234890264906820496920680926538059059209922523523525 @@ -67,39 +64,19 @@ assert ( importA 0 - 0 ^ 0 # - - --class A: -- def foo(self): -- for _ in range(10): +@@ -25,9 +15,7 @@ + class A: + def foo(self): + for _ in range(10): - aaaaaaaaaaaaaaaaaaa = bbbbbbbbbbbbbbb.cccccccccc( - xxxxxxxxxxxx - ) # pylint: disable=no-member -+NOT_YET_IMPLEMENTED_StmtClassDef ++ aaaaaaaaaaaaaaaaaaa = bbbbbbbbbbbbbbb.cccccccccc(xxxxxxxxxxxx) # pylint: disable=no-member def test(self, othr): -- return 1 == 2 and ( -- name, -- description, -- self.default, -- self.selected, -- self.auto_generated, -- self.parameters, -- self.meta_data, -- self.schedule, -- ) == ( -- name, -- description, -- othr.default, -- othr.selected, -- othr.auto_generated, -- othr.parameters, -- othr.meta_data, -- othr.schedule, -- ) -+ return NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 +@@ -52,7 +40,4 @@ + ) -assert a_function( @@ -114,7 +91,7 @@ assert ( ```py importA ( - (1, 2) + () << 0 **101234234242352525425252352352525234890264906820496920680926538059059209922523523525 ) # @@ -126,11 +103,32 @@ importA 0 ^ 0 # -NOT_YET_IMPLEMENTED_StmtClassDef +class A: + def foo(self): + for _ in range(10): + aaaaaaaaaaaaaaaaaaa = bbbbbbbbbbbbbbb.cccccccccc(xxxxxxxxxxxx) # pylint: disable=no-member def test(self, othr): - return NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 + return 1 == 2 and ( + name, + description, + self.default, + self.selected, + self.auto_generated, + self.parameters, + self.meta_data, + self.schedule, + ) == ( + name, + description, + othr.default, + othr.selected, + othr.auto_generated, + othr.parameters, + othr.meta_data, + othr.schedule, + ) NOT_YET_IMPLEMENTED_StmtAssert diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens3_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__trailing_comma_optional_parens3.py.snap similarity index 61% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens3_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__trailing_comma_optional_parens3.py.snap index 889aaf9c58..d13dff5ee4 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens3_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__trailing_comma_optional_parens3.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/trailing_comma_optional_parens3.py --- ## Input @@ -21,7 +20,7 @@ if True: ```diff --- Black +++ Ruff -@@ -1,8 +1,7 @@ +@@ -1,8 +1,14 @@ if True: if True: if True: @@ -31,8 +30,15 @@ if True: - "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwe", - ) % {"reported_username": reported_username, "report_reason": report_reason} + return ( -+ NOT_IMPLEMENTED_call() -+ % {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} ++ _( ++ "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweas " ++ + "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwegqweasdzxcqweasdzxc.", ++ "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwe", ++ ) ++ % { ++ "reported_username": reported_username, ++ "report_reason": report_reason, ++ } + ) ``` @@ -43,8 +49,15 @@ if True: if True: if True: return ( - NOT_IMPLEMENTED_call() - % {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value} + _( + "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweas " + + "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwegqweasdzxcqweasdzxc.", + "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwe", + ) + % { + "reported_username": reported_username, + "report_reason": report_reason, + } ) ``` diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_commas_in_leading_parts_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__trailing_commas_in_leading_parts.py.snap similarity index 77% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_commas_in_leading_parts_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__trailing_commas_in_leading_parts.py.snap index d1a1d68ab9..5084e5caa4 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_commas_in_leading_parts_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__trailing_commas_in_leading_parts.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/trailing_commas_in_leading_parts.py --- ## Input @@ -46,48 +45,18 @@ assert xxxxxxxxx.xxxxxxxxx.xxxxxxxxx( ```diff --- Black +++ Ruff -@@ -1,50 +1,21 @@ --zero( -- one, --).two( -- three, --).four( -- five, --) -+NOT_IMPLEMENTED_call() - --func1(arg1).func2( -- arg2, --).func3(arg3).func4( -- arg4, --).func5(arg5) -+NOT_IMPLEMENTED_call() - - # Inner one-element tuple shouldn't explode --func1(arg1).func2(arg1, (one_tuple,)).func3(arg3) -+NOT_IMPLEMENTED_call() - --( -- a, -- b, -- c, -- d, +@@ -20,9 +20,7 @@ + b, + c, + d, -) = func1( - arg1 -) and func2(arg2) -+(1, 2) = NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 ++) = func1(arg1) and func2(arg2) # Example from https://github.com/psf/black/issues/3229 - def refresh_token(self, device_family, refresh_token, api_key): -- return self.orchestration.refresh_token( -- data={ -- "refreshToken": refresh_token, -- }, -- api_key=api_key, -- )["extensions"]["sdk"]["token"] -+ return NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] - +@@ -37,14 +35,7 @@ # Edge case where a bug in a working-in-progress version of # https://github.com/psf/black/pull/3370 causes an infinite recursion. @@ -109,19 +78,39 @@ assert xxxxxxxxx.xxxxxxxxx.xxxxxxxxx( ## Ruff Output ```py -NOT_IMPLEMENTED_call() +zero( + one, +).two( + three, +).four( + five, +) -NOT_IMPLEMENTED_call() +func1(arg1).func2( + arg2, +).func3(arg3).func4( + arg4, +).func5(arg5) # Inner one-element tuple shouldn't explode -NOT_IMPLEMENTED_call() +func1(arg1).func2(arg1, (one_tuple,)).func3(arg3) -(1, 2) = NOT_IMPLEMENTED_bool_op1 and NOT_IMPLEMENTED_bool_op2 +( + a, + b, + c, + d, +) = func1(arg1) and func2(arg2) # Example from https://github.com/psf/black/issues/3229 def refresh_token(self, device_family, refresh_token, api_key): - return NOT_IMPLEMENTED_value[NOT_IMPLEMENTED_key] + return self.orchestration.refresh_token( + data={ + "refreshToken": refresh_token, + }, + api_key=api_key, + )["extensions"]["sdk"]["token"] # Edge case where a bug in a working-in-progress version of diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__tupleassign_py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__tupleassign.py.snap similarity index 65% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__tupleassign_py.snap rename to crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__tupleassign.py.snap index 643ac351f9..6e9dbd02a2 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__tupleassign_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__tupleassign.py.snap @@ -1,6 +1,5 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/tupleassign.py --- ## Input @@ -20,34 +19,35 @@ this_will_be_wrapped_in_parens, = struct.unpack(b"12345678901234567890") ```diff --- Black +++ Ruff -@@ -1,12 +1,7 @@ - # This is a standalone comment. --( -- sdfjklsdfsjldkflkjsf, -- sdfjsdfjlksdljkfsdlkf, -- sdfsdjfklsdfjlksdljkf, -- sdsfsdfjskdflsfsdf, +@@ -4,9 +4,9 @@ + sdfjsdfjlksdljkfsdlkf, + sdfsdjfklsdfjlksdljkf, + sdsfsdfjskdflsfsdf, -) = (1, 2, 3) -+(1, 2) = (1, 2) ++) = 1, 2, 3 # This is as well. -(this_will_be_wrapped_in_parens,) = struct.unpack(b"12345678901234567890") -+(1, 2) = NOT_IMPLEMENTED_call() ++(this_will_be_wrapped_in_parens,) = struct.unpack(b"NOT_YET_IMPLEMENTED_BYTE_STRING") --(a,) = call() -+(1, 2) = NOT_IMPLEMENTED_call() + (a,) = call() ``` ## Ruff Output ```py # This is a standalone comment. -(1, 2) = (1, 2) +( + sdfjklsdfsjldkflkjsf, + sdfjsdfjlksdljkfsdlkf, + sdfsdjfklsdfjlksdljkf, + sdsfsdfjskdflsfsdf, +) = 1, 2, 3 # This is as well. -(1, 2) = NOT_IMPLEMENTED_call() +(this_will_be_wrapped_in_parens,) = struct.unpack(b"NOT_YET_IMPLEMENTED_BYTE_STRING") -(1, 2) = NOT_IMPLEMENTED_call() +(a,) = call() ``` ## Black Output diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__whitespace.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__whitespace.py.snap new file mode 100644 index 0000000000..70636c22b7 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__whitespace.py.snap @@ -0,0 +1,31 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/whitespace.py +--- +## Input + +```py + +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -1 +0,0 @@ +- +``` + +## Ruff Output + +```py +``` + +## Black Output + +```py + +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@carriage_return__string.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@carriage_return__string.py.snap new file mode 100644 index 0000000000..522538378a --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@carriage_return__string.py.snap @@ -0,0 +1,26 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/carriage_return/string.py +--- +## Input +```py +'This string will not include \ +backslashes or newline characters.' + +"""Multiline +String \" +""" +``` + +## Output +```py +"This string will not include \ +backslashes or newline characters." + +"""Multiline +String \" +""" +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__annotated_assign.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__annotated_assign.py.snap new file mode 100644 index 0000000000..55c646d9fd --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__annotated_assign.py.snap @@ -0,0 +1,37 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/annotated_assign.py +--- +## Input +```py +a: string + +b: string = "test" + +b: list[ + string, + int +] = [1, 2] + +b: list[ + string, + int, +] = [1, 2] +``` + +## Output +```py +a: string + +b: string = "test" + +b: list[string, int] = [1, 2] + +b: list[ + string, + int, +] = [1, 2] +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__attribute.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__attribute.py.snap new file mode 100644 index 0000000000..2b48c542e5 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__attribute.py.snap @@ -0,0 +1,195 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/attribute.py +--- +## Input +```py +from argparse import Namespace + +a = Namespace() + +( + a + # comment + .b # trailing comment +) + +( + a + # comment + .b # trailing dot comment # trailing identifier comment +) + +( + a + # comment + .b # trailing identifier comment +) + + +( + a + # comment + . # trailing dot comment + # in between + b # trailing identifier comment +) + + +a.aaaaaaaaaaaaaaaaaaaaa.lllllllllllllllllllllllllllloooooooooong.chaaaaaaaaaaaaaaaaaaaaaaiiiiiiiiiiiiiiiiiiiiiiinnnnnnnn.ooooooooooooooooooooooooofffffffff.aaaaaaaaaattr + + +# Test that we add parentheses around the outermost attribute access in an attribute +# chain if and only if we need them, that is if there are own line comments inside +# the chain. +x1 = ( + a + .b + # comment 1 + . # comment 2 + # comment 3 + c + .d +) + +x20 = ( + a + .b +) +x21 = ( + # leading name own line + a # trailing name end-of-line + .b +) +x22 = ( + a + # outermost leading own line + .b # outermost trailing end-of-line +) + +x31 = ( + a + # own line between nodes 1 + .b +) +x321 = ( + a + . # end-of-line dot comment + b +) +x322 = ( + a + . # end-of-line dot comment 2 + b + .c +) +x331 = ( + a. + # own line between nodes 3 + b +) +x332 = ( + "" + # own line between nodes + .find +) + +x8 = ( + (a + a) + .b +) + +x51 = ( + a.b.c +) +x52 = a.askjdfahdlskjflsajfadhsaf.akjdsf.aksjdlfadhaljsashdfljaf.askjdflhasfdlashdlfaskjfd.asdkjfksahdfkjafs +x53 = ( + a.askjdfahdlskjflsajfadhsaf.akjdsf.aksjdlfadhaljsashdfljaf.askjdflhasfdlashdlfaskjfd.asdkjfksahdfkjafs +) + +``` + +## Output +```py +from argparse import Namespace + +a = Namespace() + +( + a. + # comment + b # trailing comment +) + +( + a. + # comment + b # trailing dot comment # trailing identifier comment +) + +( + a. + # comment + b # trailing identifier comment +) + + +( + a. + # comment + # in between + b # trailing dot comment # trailing identifier comment +) + + +a.aaaaaaaaaaaaaaaaaaaaa.lllllllllllllllllllllllllllloooooooooong.chaaaaaaaaaaaaaaaaaaaaaaiiiiiiiiiiiiiiiiiiiiiiinnnnnnnn.ooooooooooooooooooooooooofffffffff.aaaaaaaaaattr + + +# Test that we add parentheses around the outermost attribute access in an attribute +# chain if and only if we need them, that is if there are own line comments inside +# the chain. +x1 = ( + a.b. + # comment 1 + # comment 3 + c.d # comment 2 +) + +x20 = a.b +x21 = ( + # leading name own line + a.b # trailing name end-of-line +) +x22 = ( + a. + # outermost leading own line + b # outermost trailing end-of-line +) + +x31 = ( + a. + # own line between nodes 1 + b +) +x321 = a.b # end-of-line dot comment +x322 = a.b.c # end-of-line dot comment 2 +x331 = ( + a. + # own line between nodes 3 + b +) +x332 = ( + "". + # own line between nodes + find +) + +x8 = (a + a).b + +x51 = a.b.c +x52 = a.askjdfahdlskjflsajfadhsaf.akjdsf.aksjdlfadhaljsashdfljaf.askjdflhasfdlashdlfaskjfd.asdkjfksahdfkjafs +x53 = a.askjdfahdlskjflsajfadhsaf.akjdsf.aksjdlfadhaljsashdfljaf.askjdflhasfdlashdlfaskjfd.asdkjfksahdfkjafs +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__binary.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__binary.py.snap new file mode 100644 index 0000000000..890bfd41cb --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__binary.py.snap @@ -0,0 +1,492 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary.py +--- +## Input +```py +(aaaaaaaa + + # trailing operator comment + b # trailing right comment +) + + +(aaaaaaaa # trailing left comment + + # trailing operator comment + # leading right comment + b +) + +( + # leading left most comment + aaaaaaaa + + # trailing operator comment + # leading b comment + b # trailing b comment + # trailing b ownline comment + + # trailing second operator comment + # leading c comment + c # trailing c comment + # trailing own line comment + ) + + +# Black breaks the right side first for the following expressions: +aaaaaaaaaaaaaa + caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaal(argument1, argument2, argument3) +aaaaaaaaaaaaaa + [bbbbbbbbbbbbbbbbbbbbbb, ccccccccccccccccccccc, dddddddddddddddd, eeeeeee] +aaaaaaaaaaaaaa + (bbbbbbbbbbbbbbbbbbbbbb, ccccccccccccccccccccc, dddddddddddddddd, eeeeeee) +aaaaaaaaaaaaaa + { key1:bbbbbbbbbbbbbbbbbbbbbb, key2: ccccccccccccccccccccc, key3: dddddddddddddddd, key4: eeeeeee } +aaaaaaaaaaaaaa + { bbbbbbbbbbbbbbbbbbbbbb, ccccccccccccccccccccc, dddddddddddddddd, eeeeeee } +aaaaaaaaaaaaaa + [a for x in bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb ] +aaaaaaaaaaaaaa + (a for x in bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb ) +aaaaaaaaaaaaaa + {a for x in bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb} + +# Wraps it in parentheses if it needs to break both left and right +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + [ + bbbbbbbbbbbbbbbbbbbbbb, + ccccccccccccccccccccc, + dddddddddddddddd, + eee +] # comment + + + +# But only for expressions that have a statement parent. +not (aaaaaaaaaaaaaa + {a for x in bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb}) +[a + [bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb] in c ] + + +# leading comment +( + # comment + content + b +) + + +if ( + aaaaaaaaaaaaaaaaaa + + # has the child process finished? + bbbbbbbbbbbbbbb + + # the child process has finished, but the + # transport hasn't been notified yet? + ccccccccccc +): + pass + + +# Left only breaks +if [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +] & aaaaaaaaaaaaaaaaaaaaaaaaaa: + ... + +if [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +] & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: + ... + +# Right only can break +if aaaaaaaaaaaaaaaaaaaaaaaaaa & [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +]: + ... + +if aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa & [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +]: + ... + + +# Left or right can break +if [2222, 333] & [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +]: + ... + +if [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +] & [2222, 333]: + ... + +if [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +] & [fffffffffffffffff, gggggggggggggggggggg, hhhhhhhhhhhhhhhhhhhhh, iiiiiiiiiiiiiiii, jjjjjjjjjjjjj]: + ... + +if ( + # comment + [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, + ] +) & [ + fffffffffffffffff, + gggggggggggggggggggg, + hhhhhhhhhhhhhhhhhhhhh, + iiiiiiiiiiiiiiii, + jjjjjjjjjjjjj, +]: + pass + + ... + +# Nesting +if (aaaa + b) & [ + fffffffffffffffff, + gggggggggggggggggggg, + hhhhhhhhhhhhhhhhhhhhh, + iiiiiiiiiiiiiiii, + jjjjjjjjjjjjj, +]: + ... + +if [ + fffffffffffffffff, + gggggggggggggggggggg, + hhhhhhhhhhhhhhhhhhhhh, + iiiiiiiiiiiiiiii, + jjjjjjjjjjjjj, +] & (a + b): + ... + + +if [ + fffffffffffffffff, + gggggggggggggggggggg, + hhhhhhhhhhhhhhhhhhhhh, + iiiiiiiiiiiiiiii, + jjjjjjjjjjjjj, +] & ( + # comment + a + + b +): + ... + +if ( + [ + fffffffffffffffff, + gggggggggggggggggggg, + hhhhhhhhhhhhhhhhhhhhh, + iiiiiiiiiiiiiiii, + jjjjjjjjjjjjj, + ] + & + # comment + a + b +): + ... + + +# Unstable formatting in https://github.com/realtyem/synapse-unraid/blob/unraid_develop/synapse/handlers/presence.py +for user_id in set(target_user_ids) - {u.user_id for u in updates}: + updates.append(UserPresenceState.default(user_id)) + +# Keeps parenthesized left hand sides +( + log(self.price / self.strike) + + (self.risk_free - self.div_cont + 0.5 * (self.sigma**2)) * self.exp_time +) / self.sigmaT +``` + +## Output +```py +( + aaaaaaaa + + # trailing operator comment + b # trailing right comment +) + + +( + aaaaaaaa # trailing left comment + + # trailing operator comment + # leading right comment + b +) + +( + # leading left most comment + aaaaaaaa + + # trailing operator comment + # leading b comment + b # trailing b comment + # trailing b ownline comment + + # trailing second operator comment + # leading c comment + c # trailing c comment + # trailing own line comment +) + + +# Black breaks the right side first for the following expressions: +aaaaaaaaaaaaaa + caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaal( + argument1, argument2, argument3 +) +aaaaaaaaaaaaaa + [ + bbbbbbbbbbbbbbbbbbbbbb, + ccccccccccccccccccccc, + dddddddddddddddd, + eeeeeee, +] +aaaaaaaaaaaaaa + ( + bbbbbbbbbbbbbbbbbbbbbb, + ccccccccccccccccccccc, + dddddddddddddddd, + eeeeeee, +) +aaaaaaaaaaaaaa + { + key1: bbbbbbbbbbbbbbbbbbbbbb, + key2: ccccccccccccccccccccc, + key3: dddddddddddddddd, + key4: eeeeeee, +} +aaaaaaaaaaaaaa + { + bbbbbbbbbbbbbbbbbbbbbb, + ccccccccccccccccccccc, + dddddddddddddddd, + eeeeeee, +} +aaaaaaaaaaaaaa + [ + a + for x in bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +] +( + aaaaaaaaaaaaaa + + (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) +) +aaaaaaaaaaaaaa + {NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set} + +# Wraps it in parentheses if it needs to break both left and right +( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + [bbbbbbbbbbbbbbbbbbbbbb, ccccccccccccccccccccc, dddddddddddddddd, eee] +) # comment + + +# But only for expressions that have a statement parent. +not (aaaaaaaaaaaaaa + {NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set}) +[ + a + + [ + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + ] + in c +] + + +# leading comment +( + # comment + content + b +) + + +if ( + aaaaaaaaaaaaaaaaaa + + + # has the child process finished? + bbbbbbbbbbbbbbb + + + # the child process has finished, but the + # transport hasn't been notified yet? + ccccccccccc +): + pass + + +# Left only breaks +if [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +] & aaaaaaaaaaaaaaaaaaaaaaaaaa: + ... + +if ( + [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, + ] + & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +): + ... + +# Right only can break +if aaaaaaaaaaaaaaaaaaaaaaaaaa & [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +]: + ... + +if ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + & [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, + ] +): + ... + + +# Left or right can break +if [2222, 333] & [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +]: + ... + +if [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +] & [2222, 333]: + ... + +if [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +] & [ + fffffffffffffffff, + gggggggggggggggggggg, + hhhhhhhhhhhhhhhhhhhhh, + iiiiiiiiiiiiiiii, + jjjjjjjjjjjjj, +]: + ... + +if ( + # comment + [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, + ] +) & [ + fffffffffffffffff, + gggggggggggggggggggg, + hhhhhhhhhhhhhhhhhhhhh, + iiiiiiiiiiiiiiii, + jjjjjjjjjjjjj, +]: + pass + + ... + +# Nesting +if (aaaa + b) & [ + fffffffffffffffff, + gggggggggggggggggggg, + hhhhhhhhhhhhhhhhhhhhh, + iiiiiiiiiiiiiiii, + jjjjjjjjjjjjj, +]: + ... + +if [ + fffffffffffffffff, + gggggggggggggggggggg, + hhhhhhhhhhhhhhhhhhhhh, + iiiiiiiiiiiiiiii, + jjjjjjjjjjjjj, +] & (a + b): + ... + + +if ( + [ + fffffffffffffffff, + gggggggggggggggggggg, + hhhhhhhhhhhhhhhhhhhhh, + iiiiiiiiiiiiiiii, + jjjjjjjjjjjjj, + ] + & + ( + # comment + a + b + ) +): + ... + +if ( + [ + fffffffffffffffff, + gggggggggggggggggggg, + hhhhhhhhhhhhhhhhhhhhh, + iiiiiiiiiiiiiiii, + jjjjjjjjjjjjj, + ] + & + # comment + a + b +): + ... + + +# Unstable formatting in https://github.com/realtyem/synapse-unraid/blob/unraid_develop/synapse/handlers/presence.py +for user_id in set( + target_user_ids +) - {NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set}: + updates.append(UserPresenceState.default(user_id)) + +# Keeps parenthesized left hand sides +( + log(self.price / self.strike) + + (self.risk_free - self.div_cont + 0.5 * (self.sigma**2)) * self.exp_time +) / self.sigmaT +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__boolean_operation.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__boolean_operation.py.snap new file mode 100644 index 0000000000..979f6fb805 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__boolean_operation.py.snap @@ -0,0 +1,142 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/boolean_operation.py +--- +## Input +```py +if ( + self._proc + # has the child process finished? + and self._returncode + # the child process has finished, but the + # transport hasn't been notified yet? + and self._proc.poll() +): + pass + +if ( + self._proc + and self._returncode + and self._proc.poll() + and self._proc + and self._returncode + and self._proc.poll() +): + ... + +if ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + and aaaaaaaaaaaaaaaaa + and aaaaaaaaaaaaaaaaaaaaaa + and aaaaaaaaaaaaaaaaaaaaaaaa + and aaaaaaaaaaaaaaaaaaaaaaaaaa + and aaaaaaaaaaaaaaaaaaaaaaaaaaaa +): + ... + + +if ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaas + and aaaaaaaaaaaaaaaaa +): + ... + + +if [2222, 333] and [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +]: + ... + +if [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +] and [2222, 333]: + pass + +# Break right only applies for boolean operations with a left and right side +if ( + aaaaaaaaaaaaaaaaaaaaaaaaaa + and bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + and ccccccccccccccccc + and [dddddddddddddd, eeeeeeeeee, fffffffffffffff] +): + pass +``` + +## Output +```py +if ( + self._proc + # has the child process finished? + and self._returncode + # the child process has finished, but the + # transport hasn't been notified yet? + and self._proc.poll() +): + pass + +if ( + self._proc + and self._returncode + and self._proc.poll() + and self._proc + and self._returncode + and self._proc.poll() +): + ... + +if ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + and aaaaaaaaaaaaaaaaa + and aaaaaaaaaaaaaaaaaaaaaa + and aaaaaaaaaaaaaaaaaaaaaaaa + and aaaaaaaaaaaaaaaaaaaaaaaaaa + and aaaaaaaaaaaaaaaaaaaaaaaaaaaa +): + ... + + +if ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaas + and aaaaaaaaaaaaaaaaa +): + ... + + +if [2222, 333] and [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +]: + ... + +if [ + aaaaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbb, + cccccccccccccccccccc, + dddddddddddddddddddd, + eeeeeeeeee, +] and [2222, 333]: + pass + +# Break right only applies for boolean operations with a left and right side +if ( + aaaaaaaaaaaaaaaaaaaaaaaaaa + and bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + and ccccccccccccccccc + and [dddddddddddddd, eeeeeeeeee, fffffffffffffff] +): + pass +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__call.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__call.py.snap new file mode 100644 index 0000000000..bac556ce5f --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__call.py.snap @@ -0,0 +1,183 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/call.py +--- +## Input +```py +from unittest.mock import MagicMock + + +def f(*args, **kwargs): + pass + +this_is_a_very_long_argument_asökdhflakjslaslhfdlaffahflahsöfdhasägporejfäalkdsjäfalisjäfdlkasjd = 1 +session = MagicMock() +models = MagicMock() + +f() + +f(1) + +f(x=2) + +f(1, x=2) + +f( + this_is_a_very_long_argument_asökdhflakjslaslhfdlaffahflahsöfdhasägporejfäalkdsjäfalisjäfdlkasjd +) +f( + this_is_a_very_long_keyword_argument_asökdhflakjslaslhfdlaffahflahsöfdhasägporejfäalkdsjäfalisjäfdlkasjd=1 +) + +f( + 1, + mixed_very_long_arguments=1, +) + +f( + this_is_a_very_long_argument_asökdhflakjslaslhfdlaffahflahsöfdhasägporejfäalkdsjäfalisjäfdlkasjd, + these_arguments_have_values_that_need_to_break_because_they_are_too_long1=(100000 - 100000000000), + these_arguments_have_values_that_need_to_break_because_they_are_too_long2="akshfdlakjsdfad" + "asdfasdfa", + these_arguments_have_values_that_need_to_break_because_they_are_too_long3=session, +) + +f( + # dangling comment +) + + +f( + only=1, short=1, arguments=1 +) + +f( + hey_this_is_a_long_call, it_has_funny_attributes_that_breaks_into_three_lines=1 +) + +f( + hey_this_is_a_very_long_call=1, it_has_funny_attributes_asdf_asdf=1, too_long_for_the_line=1, really=True +) + +# TODO(konstin): Call chains/fluent interface (https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#call-chains) +result = ( + session.query(models.Customer.id) + .filter( + models.Customer.account_id == 10000, + models.Customer.email == "user@example.org", + ) + .order_by(models.Customer.id.asc()) + .all() +) +# TODO(konstin): Black has this special case for comment placement where everything stays in one line +f( + "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa" +) + +f( + session, + b=1, + ** # oddly placed end-of-line comment + dict() +) +f( + session, + b=1, + ** + # oddly placed own line comment + dict() +) + +# Don't add a magic trailing comma when there is only one entry +# Minimized from https://github.com/django/django/blob/7eeadc82c2f7d7a778e3bb43c34d642e6275dacf/django/contrib/admin/checks.py#L674-L681 +f( + a.very_long_function_function_that_is_so_long_that_it_expands_the_parent_but_its_only_a_single_argument() +) +``` + +## Output +```py +from unittest.mock import MagicMock + + +def f(*args, **kwargs): + pass + + +this_is_a_very_long_argument_asökdhflakjslaslhfdlaffahflahsöfdhasägporejfäalkdsjäfalisjäfdlkasjd = 1 +session = MagicMock() +models = MagicMock() + +f() + +f(1) + +f(x=2) + +f(1, x=2) + +f( + this_is_a_very_long_argument_asökdhflakjslaslhfdlaffahflahsöfdhasägporejfäalkdsjäfalisjäfdlkasjd +) +f( + this_is_a_very_long_keyword_argument_asökdhflakjslaslhfdlaffahflahsöfdhasägporejfäalkdsjäfalisjäfdlkasjd=1 +) + +f( + 1, + mixed_very_long_arguments=1, +) + +f( + this_is_a_very_long_argument_asökdhflakjslaslhfdlaffahflahsöfdhasägporejfäalkdsjäfalisjäfdlkasjd, + these_arguments_have_values_that_need_to_break_because_they_are_too_long1=( + 100000 - 100000000000 + ), + these_arguments_have_values_that_need_to_break_because_they_are_too_long2="akshfdlakjsdfad" + + "asdfasdfa", + these_arguments_have_values_that_need_to_break_because_they_are_too_long3=session, +) + +f() +# dangling comment + + +f(only=1, short=1, arguments=1) + +f(hey_this_is_a_long_call, it_has_funny_attributes_that_breaks_into_three_lines=1) + +f( + hey_this_is_a_very_long_call=1, + it_has_funny_attributes_asdf_asdf=1, + too_long_for_the_line=1, + really=True, +) + +# TODO(konstin): Call chains/fluent interface (https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#call-chains) +result = session.query(models.Customer.id).filter( + models.Customer.account_id == 10000, + models.Customer.email == "user@example.org", +).order_by(models.Customer.id.asc()).all() +# TODO(konstin): Black has this special case for comment placement where everything stays in one line +f("aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa") + +f( + session, + b=1, + **dict(), # oddly placed end-of-line comment +) +f( + session, + b=1, + **dict(), + # oddly placed own line comment +) + +# Don't add a magic trailing comma when there is only one entry +# Minimized from https://github.com/django/django/blob/7eeadc82c2f7d7a778e3bb43c34d642e6275dacf/django/contrib/admin/checks.py#L674-L681 +f( + a.very_long_function_function_that_is_so_long_that_it_expands_the_parent_but_its_only_a_single_argument() +) +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__compare.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__compare.py.snap new file mode 100644 index 0000000000..9535adb8a0 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__compare.py.snap @@ -0,0 +1,187 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/compare.py +--- +## Input +```py +a == b +a != b +a < b +a <= b +a > b +a >= b +a is b +a is not b +a in b +a not in b + +(a == + # comment + b +) + +(a == # comment + b + ) + +a < b > c == d + +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa < bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb > ccccccccccccccccccccccccccccc == ddddddddddddddddddddd + +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa < [ + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, + ff, +] < [ccccccccccccccccccccccccccccc, dddd] < ddddddddddddddddddddddddddddddddddddddddddd + +return 1 == 2 and ( + name, + description, + self_default, + self_selected, + self_auto_generated, + self_parameters, + self_meta_data, + self_schedule, +) == ( + name, + description, + othr_default, + othr_selected, + othr_auto_generated, + othr_parameters, + othr_meta_data, + othr_schedule, +) + +(name, description, self_default, self_selected, self_auto_generated, self_parameters, self_meta_data, self_schedule) == (name, description, other_default, othr_selected, othr_auto_generated, othr_parameters, othr_meta_data, othr_schedule) +((name, description, self_default, self_selected, self_auto_generated, self_parameters, self_meta_data, self_schedule) == (name, description, other_default, othr_selected, othr_auto_generated, othr_parameters, othr_meta_data, othr_schedule)) + +[ + ( + a + + [ + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + ] + >= c + ) +] +``` + +## Output +```py +a == b +a != b +a < b +a <= b +a > b +a >= b +a is b +a is not b +a in b +a not in b + +( + a + # comment + == b +) + +( + a # comment + == b +) + +a < b > c == d + +( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + < bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + > ccccccccccccccccccccccccccccc + == ddddddddddddddddddddd +) + +( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + < [ + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, + ff, + ] + < [ccccccccccccccccccccccccccccc, dddd] + < ddddddddddddddddddddddddddddddddddddddddddd +) + +return 1 == 2 and ( + name, + description, + self_default, + self_selected, + self_auto_generated, + self_parameters, + self_meta_data, + self_schedule, +) == ( + name, + description, + othr_default, + othr_selected, + othr_auto_generated, + othr_parameters, + othr_meta_data, + othr_schedule, +) + +( + name, + description, + self_default, + self_selected, + self_auto_generated, + self_parameters, + self_meta_data, + self_schedule, +) == ( + name, + description, + other_default, + othr_selected, + othr_auto_generated, + othr_parameters, + othr_meta_data, + othr_schedule, +) +( + ( + name, + description, + self_default, + self_selected, + self_auto_generated, + self_parameters, + self_meta_data, + self_schedule, + ) + == ( + name, + description, + other_default, + othr_selected, + othr_auto_generated, + othr_parameters, + othr_meta_data, + othr_schedule, + ) +) + +[ + ( + a + + [ + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + ] + >= c + ) +] +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__dict.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__dict.py.snap new file mode 100644 index 0000000000..043bd0e952 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__dict.py.snap @@ -0,0 +1,132 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/dict.py +--- +## Input +```py +# before +{ # open + key# key + : # colon + value# value +} # close +# after + + +{**d} + +{**a, # leading +** # middle +b # trailing +} + +{ +** # middle with single item +b +} + +{ + # before + ** # between + b, +} + +{ + **a # comment before preceding node's comma + , + # before + ** # between + b, +} + +{} + +{1:2,} + +{1:2, + 3:4,} + +{asdfsadfalsdkjfhalsdkjfhalskdjfhlaksjdfhlaskjdfhlaskjdfhlaksdjfh: 1, adsfadsflasdflasdfasdfasdasdf: 2} + +mapping = { + A: 0.25 * (10.0 / 12), + B: 0.1 * (10.0 / 12), + C: 0.1 * (10.0 / 12), + D: 0.1 * (10.0 / 12), +} + +# Regression test for formatter panic with comment after parenthesized dict value +# Originally found in https://github.com/bolucat/Firefox/blob/636a717ef025c16434997dc89e42351ef740ee6b/testing/marionette/client/marionette_driver/geckoinstance.py#L109 +a = { + 1: (2), + # comment + 3: True, +} +``` + +## Output +```py +# before +{ + # open + key: value # key # colon # value +} # close +# after + + +{**d} + +{ + **a, # leading + **b, # middle # trailing +} + +{ + **b # middle with single item +} + +{ + # before + **b, # between +} + +{ + **a, # comment before preceding node's comma + # before + **b, # between +} + +{} + +{ + 1: 2, +} + +{ + 1: 2, + 3: 4, +} + +{ + asdfsadfalsdkjfhalsdkjfhalskdjfhlaksjdfhlaskjdfhlaskjdfhlaksdjfh: 1, + adsfadsflasdflasdfasdfasdasdf: 2, +} + +mapping = { + A: 0.25 * (10.0 / 12), + B: 0.1 * (10.0 / 12), + C: 0.1 * (10.0 / 12), + D: 0.1 * (10.0 / 12), +} + +# Regression test for formatter panic with comment after parenthesized dict value +# Originally found in https://github.com/bolucat/Firefox/blob/636a717ef025c16434997dc89e42351ef740ee6b/testing/marionette/client/marionette_driver/geckoinstance.py#L109 +a = { + 1: (2), + # comment + 3: True, +} +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__if.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__if.py.snap new file mode 100644 index 0000000000..f5a195c622 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__if.py.snap @@ -0,0 +1,102 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/if.py +--- +## Input +```py +a1 = 1 if True else 2 + +a2 = "this is a very long text that will make the group break to check that parentheses are added" if True else 2 + +# These comment should be kept in place +b1 = ( + # We return "a" ... + "a" # that's our True value + # ... if this condition matches ... + if True # that's our test + # ... otherwise we return "b" + else "b" # that's our False value +) + +# These only need to be stable, bonus is we also keep the order +c1 = ( + "a" # 1 + if # 2 + True # 3 + else # 4 + "b" # 5 +) +c2 = ( + "a" # 1 + # 2 + if # 3 + # 4 + True # 5 + # 6 + else # 7 + # 8 + "b" # 9 +) + +# regression test: parentheses outside the expression ranges interfering with finding +# the `if` and `else` token finding +d1 = [ + ("a") if # 1 + ("b") else # 2 + ("c") +] +``` + +## Output +```py +a1 = 1 if True else 2 + +a2 = ( + "this is a very long text that will make the group break to check that parentheses are added" + if True + else 2 +) + +# These comment should be kept in place +b1 = ( + # We return "a" ... + "a" # that's our True value + # ... if this condition matches ... + if True # that's our test + # ... otherwise we return "b" + else "b" # that's our False value +) + +# These only need to be stable, bonus is we also keep the order +c1 = ( + "a" # 1 + # 2 + if True # 3 + # 4 + else "b" # 5 +) +c2 = ( + "a" # 1 + # 2 + # 3 + # 4 + if True # 5 + # 6 + # 7 + # 8 + else "b" # 9 +) + +# regression test: parentheses outside the expression ranges interfering with finding +# the `if` and `else` token finding +d1 = [ + ("a") + # 1 + if ("b") + # 2 + else ("c") +] +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__list.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__list.py.snap new file mode 100644 index 0000000000..e21436a052 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__list.py.snap @@ -0,0 +1,60 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/list.py +--- +## Input +```py +# Dangling comment placement in empty lists +# Regression test for https://github.com/python/cpython/blob/03160630319ca26dcbbad65225da4248e54c45ec/Tools/c-analyzer/c_analyzer/datafiles.py#L14-L16 +a1 = [ # a +] +a2 = [ # a + # b +] +a3 = [ + # b +] + +# Add magic trailing comma only if there is more than one entry, but respect it if it's +# already there +b1 = [ + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa +] +b2 = [ + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, +] +b3 = [ + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa +] +``` + +## Output +```py +# Dangling comment placement in empty lists +# Regression test for https://github.com/python/cpython/blob/03160630319ca26dcbbad65225da4248e54c45ec/Tools/c-analyzer/c_analyzer/datafiles.py#L14-L16 +a1 = [ # a +] +a2 = [ # a + # b +] +a3 = [ + # b +] + +# Add magic trailing comma only if there is more than one entry, but respect it if it's +# already there +b1 = [ + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa +] +b2 = [ + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, +] +b3 = [ + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, +] +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__list_comp.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__list_comp.py.snap new file mode 100644 index 0000000000..ee8cb00842 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__list_comp.py.snap @@ -0,0 +1,108 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/list_comp.py +--- +## Input +```py +[i for i in []] + +[i for i in [1,]] + +[ + a # a + for # for + c # c + in # in + e # e +] + +[ + # above a + a # a + # above for + for # for + # above c + c # c + # above in + in # in + # above e + e # e + # above if + if # if + # above f + f # f + # above if2 + if # if2 + # above g + g # g +] + +[ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + [dddddddddddddddddd, eeeeeeeeeeeeeeeeeee] + for + ccccccccccccccccccccccccccccccccccccccc, + ddddddddddddddddddd, [eeeeeeeeeeeeeeeeeeeeee, fffffffffffffffffffffffff] + in + eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeffffffffffffffffffffffffggggggggggggggggggggghhhhhhhhhhhhhhothermoreeand_even_moreddddddddddddddddddddd + if + fffffffffffffffffffffffffffffffffffffffffff < gggggggggggggggggggggggggggggggggggggggggggggg < hhhhhhhhhhhhhhhhhhhhhhhhhh + if + gggggggggggggggggggggggggggggggggggggggggggg +] +``` + +## Output +```py +[i for i in []] + +[ + i + for i in [ + 1, + ] +] + +[ + a # a + for c in e # for # c # in # e +] + +[ + # above a + a # a + # above for + for # for + # above c + c # c + # above in + in # in + # above e + e # e + # above if + if # if + # above f + f # f + # above if2 + if # if2 + # above g + g # g +] + +[ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + + [dddddddddddddddddd, eeeeeeeeeeeeeeeeeee] + for ( + ccccccccccccccccccccccccccccccccccccccc, + ddddddddddddddddddd, + [eeeeeeeeeeeeeeeeeeeeee, fffffffffffffffffffffffff], + ) in eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeffffffffffffffffffffffffggggggggggggggggggggghhhhhhhhhhhhhhothermoreeand_even_moreddddddddddddddddddddd + if fffffffffffffffffffffffffffffffffffffffffff + < gggggggggggggggggggggggggggggggggggggggggggggg + < hhhhhhhhhhhhhhhhhhhhhhhhhh + if gggggggggggggggggggggggggggggggggggggggggggg +] +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__named_expr.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__named_expr.py.snap new file mode 100644 index 0000000000..9cf0484b4b --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__named_expr.py.snap @@ -0,0 +1,38 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/named_expr.py +--- +## Input +```py +y = 1 + +if ( + # 1 + x # 2 + := # 3 + y # 4 +): + pass + +y0 = (y1 := f(x)) + +f(x:=y, z=True) +``` + +## Output +```py +y = 1 + +if ( + # 1 + x := y # 2 # 3 # 4 +): + pass + +y0 = (y1 := f(x)) + +f(x := y, z=True) +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__slice.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__slice.py.snap new file mode 100644 index 0000000000..1ea479d8c8 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__slice.py.snap @@ -0,0 +1,176 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/slice.py +--- +## Input +```py +# Handle comments both when lower and upper exist and when they don't +a1 = "a"[ + # a + 1 # b + : # c + 2 # d +] +a2 = "a"[ + # a + # b + : # c + # d +] + +# Check all places where comments can exist +b1 = "b"[ # a + # b + 1 # c + # d + : # e + # f + 2 # g + # h + : # i + # j + 3 # k + # l +] + +# Handle the spacing from the colon correctly with upper leading comments +c1 = "c"[ + 1 + : # e + # f + 2 +] +c2 = "c"[ + 1 + : # e + 2 +] +c3 = "c"[ + 1 + : + # f + 2 +] +c4 = "c"[ + 1 + : # f + 2 +] + +# End of line comments +d1 = "d"[ # comment + : +] +d2 = "d"[ # comment + 1: +] +d3 = "d"[ + 1 # comment + : +] + +# Spacing around the colon(s) +def a(): + ... + +e00 = "e"[:] +e01 = "e"[:1] +e02 = "e"[: a()] +e10 = "e"[1:] +e11 = "e"[1:1] +e12 = "e"[1 : a()] +e20 = "e"[a() :] +e21 = "e"[a() : 1] +e22 = "e"[a() : a()] +e200 = "e"[a() :: ] +e201 = "e"[a() :: 1] +e202 = "e"[a() :: a()] +e210 = "e"[a() : 1 :] +``` + +## Output +```py +# Handle comments both when lower and upper exist and when they don't +a1 = "a"[ + # a + 1 # b + : # c + 2 # d +] +a2 = "a"[ + # a + # b + : # c + # d +] + +# Check all places where comments can exist +b1 = "b"[ # a + # b + 1 # c + # d + : # e + # f + 2 # g + # h + : # i + # j + 3 # k + # l +] + +# Handle the spacing from the colon correctly with upper leading comments +c1 = "c"[ + 1: # e + # f + 2 +] +c2 = "c"[ + 1: # e + 2 +] +c3 = "c"[ + 1: + # f + 2 +] +c4 = "c"[ + 1: # f + 2 +] + +# End of line comments +d1 = "d"[ # comment + : +] +d2 = "d"[ # comment + 1: +] +d3 = "d"[ + 1 # comment + : +] + + +# Spacing around the colon(s) +def a(): + ... + + +e00 = "e"[:] +e01 = "e"[:1] +e02 = "e"[ : a()] +e10 = "e"[1:] +e11 = "e"[1:1] +e12 = "e"[1 : a()] +e20 = "e"[a() :] +e21 = "e"[a() : 1] +e22 = "e"[a() : a()] +e200 = "e"[a() : :] +e201 = "e"[a() :: 1] +e202 = "e"[a() :: a()] +e210 = "e"[a() : 1 :] +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__starred.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__starred.py.snap new file mode 100644 index 0000000000..57108edf12 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__starred.py.snap @@ -0,0 +1,61 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/starred.py +--- +## Input +```py +call( + # Leading starred comment + * # Trailing star comment + [ + # Leading value commnt + [What, i, this, s, very, long, aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa] + ] # trailing value comment +) + +call( + # Leading starred comment + * ( # Leading value commnt + [What, i, this, s, very, long, aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa] + ) # trailing value comment +) +``` + +## Output +```py +call( + # Leading starred comment + # Trailing star comment + *[ + # Leading value commnt + [ + What, + i, + this, + s, + very, + long, + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, + ] + ] # trailing value comment +) + +call( + # Leading starred comment + # Leading value commnt + *( + [ + What, + i, + this, + s, + very, + long, + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, + ] + ) # trailing value comment +) +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap new file mode 100644 index 0000000000..72128eca3a --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap @@ -0,0 +1,452 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/string.py +--- +## Input +```py +"' test" +'" test' + +"\" test" +'\' test' + +# Prefer single quotes for string with more double quotes +"' \" \" '' \" \" '" + +# Prefer double quotes for string with more single quotes +'\' " " \'\' " " \'' + +# Prefer double quotes for string with equal amount of single and double quotes +'" \' " " \'\'' +"' \" '' \" \"" + +"\\' \"\"" +'\\\' ""' + + +u"Test" +U"Test" + +r"Test" +R"Test" + +'This string will not include \ +backslashes or newline characters.' + +if True: + 'This string will not include \ + backslashes or newline characters.' + +"""Multiline +String \" +""" + +'''Multiline +String \' +''' + +'''Multiline +String "" +''' + +'''Multiline +String """ +''' + +'''Multiline +String "''' + +"""Multiline +String ''' +""" + +"""Multiline +String '""" + +'''Multiline +String \"\"\" +''' + +# String continuation + +"Let's" "start" "with" "a" "simple" "example" + +"Let's" "start" "with" "a" "simple" "example" "now repeat after me:" "I am confident" "I am confident" "I am confident" "I am confident" "I am confident" + +( + "Let's" "start" "with" "a" "simple" "example" "now repeat after me:" "I am confident" "I am confident" "I am confident" "I am confident" "I am confident" +) + +if ( + a + "Let's" + "start" + "with" + "a" + "simple" + "example" + "now repeat after me:" + "I am confident" + "I am confident" + "I am confident" + "I am confident" + "I am confident" +): + pass + +if "Let's" "start" "with" "a" "simple" "example" "now repeat after me:" "I am confident" "I am confident" "I am confident" "I am confident" "I am confident": + pass + +( + # leading + "a" # trailing part comment + + # leading part comment + + "b" # trailing second part comment + # trailing +) + +test_particular = [ + # squares + '1.00000000100000000025', + '1.0000000000000000000000000100000000000000000000000' #... + '00025', + '1.0000000000000000000000000000000000000000000010000' #... + '0000000000000000000000000000000000000000025', +] + +# Parenthesized string continuation with messed up indentation +{ + "key": ( + [], + 'a' + 'b' + 'c' + ) +} +``` + +## Outputs +### Output 1 +``` +indent-style = Spaces, size: 4 +line-width = 88 +quote-style = Double +magic-trailing-comma = Respect +``` + +```py +"' test" +'" test' + +'" test' +"' test" + +# Prefer single quotes for string with more double quotes +"' \" \" '' \" \" '" + +# Prefer double quotes for string with more single quotes +"' \" \" '' \" \" '" + +# Prefer double quotes for string with equal amount of single and double quotes +"\" ' \" \" ''" +"' \" '' \" \"" + +'\\\' ""' +'\\\' ""' + + +"Test" +"Test" + +r"Test" +R"Test" + +"This string will not include \ +backslashes or newline characters." + +if True: + "This string will not include \ + backslashes or newline characters." + +"""Multiline +String \" +""" + +"""Multiline +String \' +""" + +"""Multiline +String "" +""" + +'''Multiline +String """ +''' + +'''Multiline +String "''' + +"""Multiline +String ''' +""" + +"""Multiline +String '""" + +"""Multiline +String \"\"\" +""" + +# String continuation + +"Let's" "start" "with" "a" "simple" "example" + +( + "Let's" + "start" + "with" + "a" + "simple" + "example" + "now repeat after me:" + "I am confident" + "I am confident" + "I am confident" + "I am confident" + "I am confident" +) + +( + "Let's" + "start" + "with" + "a" + "simple" + "example" + "now repeat after me:" + "I am confident" + "I am confident" + "I am confident" + "I am confident" + "I am confident" +) + +if ( + a + + "Let's" + "start" + "with" + "a" + "simple" + "example" + "now repeat after me:" + "I am confident" + "I am confident" + "I am confident" + "I am confident" + "I am confident" +): + pass + +if ( + "Let's" + "start" + "with" + "a" + "simple" + "example" + "now repeat after me:" + "I am confident" + "I am confident" + "I am confident" + "I am confident" + "I am confident" +): + pass + +( + # leading + "a" # trailing part comment + # leading part comment + "b" # trailing second part comment + # trailing +) + +test_particular = [ + # squares + "1.00000000100000000025", + "1.0000000000000000000000000100000000000000000000000" # ... + "00025", + "1.0000000000000000000000000000000000000000000010000" # ... + "0000000000000000000000000000000000000000025", +] + +# Parenthesized string continuation with messed up indentation +{"key": ([], "a" "b" "c")} +``` + + +### Output 2 +``` +indent-style = Spaces, size: 4 +line-width = 88 +quote-style = Single +magic-trailing-comma = Respect +``` + +```py +"' test" +'" test' + +'" test' +"' test" + +# Prefer single quotes for string with more double quotes +'\' " " \'\' " " \'' + +# Prefer double quotes for string with more single quotes +'\' " " \'\' " " \'' + +# Prefer double quotes for string with equal amount of single and double quotes +'" \' " " \'\'' +'\' " \'\' " "' + +'\\\' ""' +'\\\' ""' + + +'Test' +'Test' + +r'Test' +R'Test' + +'This string will not include \ +backslashes or newline characters.' + +if True: + 'This string will not include \ + backslashes or newline characters.' + +'''Multiline +String \" +''' + +'''Multiline +String \' +''' + +'''Multiline +String "" +''' + +'''Multiline +String """ +''' + +'''Multiline +String "''' + +"""Multiline +String ''' +""" + +"""Multiline +String '""" + +'''Multiline +String \"\"\" +''' + +# String continuation + +"Let's" 'start' 'with' 'a' 'simple' 'example' + +( + "Let's" + 'start' + 'with' + 'a' + 'simple' + 'example' + 'now repeat after me:' + 'I am confident' + 'I am confident' + 'I am confident' + 'I am confident' + 'I am confident' +) + +( + "Let's" + 'start' + 'with' + 'a' + 'simple' + 'example' + 'now repeat after me:' + 'I am confident' + 'I am confident' + 'I am confident' + 'I am confident' + 'I am confident' +) + +if ( + a + + "Let's" + 'start' + 'with' + 'a' + 'simple' + 'example' + 'now repeat after me:' + 'I am confident' + 'I am confident' + 'I am confident' + 'I am confident' + 'I am confident' +): + pass + +if ( + "Let's" + 'start' + 'with' + 'a' + 'simple' + 'example' + 'now repeat after me:' + 'I am confident' + 'I am confident' + 'I am confident' + 'I am confident' + 'I am confident' +): + pass + +( + # leading + 'a' # trailing part comment + # leading part comment + 'b' # trailing second part comment + # trailing +) + +test_particular = [ + # squares + '1.00000000100000000025', + '1.0000000000000000000000000100000000000000000000000' # ... + '00025', + '1.0000000000000000000000000000000000000000000010000' # ... + '0000000000000000000000000000000000000000025', +] + +# Parenthesized string continuation with messed up indentation +{'key': ([], 'a' 'b' 'c')} +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__tuple.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__tuple.py.snap new file mode 100644 index 0000000000..e398db039b --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__tuple.py.snap @@ -0,0 +1,279 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tuple.py +--- +## Input +```py +# Non-wrapping parentheses checks +a1 = 1, 2 +a2 = (1, 2) +a3 = (1, 2), 3 +a4 = ((1, 2), 3) + +# Wrapping parentheses checks +b1 = (("Michael", "Ende"), ("Der", "satanarchäolügenialkohöllische", "Wunschpunsch"), ("Beelzebub", "Irrwitzer"), ("Tyrannja", "Vamperl"),) +b2 = ("akjdshflkjahdslkfjlasfdahjlfds", "ljklsadhflakfdajflahfdlajfhafldkjalfj", "ljklsadhflakfdajflahfdlajfhafldkjalf2",) +b3 = ("The", "Night", "of", "Wishes:", "Or", "the", "Satanarchaeolidealcohellish", "Notion", "Potion",) + +# Nested wrapping parentheses check +c1 = (("cicero"), ("Qui", "autem,", "si", "maxime", "hoc", "placeat,", "moderatius", "tamen", "id", "uolunt", "fieri,", "difficilem", "quandam", "temperantiam", "postulant", "in", "eo,", "quod", "semel", "admissum", "coerceri", "reprimique", "non", "potest,", "ut", "propemodum", "iustioribus", "utamur", "illis,", "qui", "omnino", "auocent", "a", "philosophia,", "quam", "his,", "qui", "rebus", "infinitis", "modum", "constituant", "in", "reque", "eo", "meliore,", "quo", "maior", "sit,", "mediocritatem", "desiderent."), ("de", "finibus", "bonorum", "et", "malorum")) + +# Deeply nested parentheses +d1 = ((("3D",),),) +d2 = (((((((((((((((((((((((((((("¯\_(ツ)_/¯",),),),),),),),),),),),),),),),),),),),),),),),),),),),) + +# Join and magic trailing comma +e1 = ( + 1, + 2 +) +e2 = ( + 1, + 2, +) +e3 = ( + 1, +) +e4 = ( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor", + "incididunt" +) + +# Empty tuples and comments +f1 = ( + # empty +) +f2 = () +f3 = ( # end-of-line + # own-line +) # trailing +f4 = ( # end-of-line + # own-line + # own-line 2 +) # trailing + +# Comments in other tuples +g1 = ( # a + # b + 1, # c + # d +) # e +g2 = ( # a + # b + 1, # c + # d + 2, # e + # f +) # g + +# Ensure the correct number of parentheses +h1 = ((((1, 2)))) +h2 = ((((1, "qweiurpoiqwurepqiurpqirpuqoiwrupqoirupqoirupqoiurpqiorupwqiourpqurpqurpqurpqurpqurpqurüqurqpuriq")))) +h3 = 1, "qweiurpoiqwurepqiurpqirpuqoiwrupqoirupqoirupqoiurpqiorupwqiourpqurpqurpqurpqurpqurpqurüqurqpuriq" +``` + +## Output +```py +# Non-wrapping parentheses checks +a1 = 1, 2 +a2 = (1, 2) +a3 = (1, 2), 3 +a4 = ((1, 2), 3) + +# Wrapping parentheses checks +b1 = ( + ("Michael", "Ende"), + ("Der", "satanarchäolügenialkohöllische", "Wunschpunsch"), + ("Beelzebub", "Irrwitzer"), + ("Tyrannja", "Vamperl"), +) +b2 = ( + "akjdshflkjahdslkfjlasfdahjlfds", + "ljklsadhflakfdajflahfdlajfhafldkjalfj", + "ljklsadhflakfdajflahfdlajfhafldkjalf2", +) +b3 = ( + "The", + "Night", + "of", + "Wishes:", + "Or", + "the", + "Satanarchaeolidealcohellish", + "Notion", + "Potion", +) + +# Nested wrapping parentheses check +c1 = ( + ("cicero"), + ( + "Qui", + "autem,", + "si", + "maxime", + "hoc", + "placeat,", + "moderatius", + "tamen", + "id", + "uolunt", + "fieri,", + "difficilem", + "quandam", + "temperantiam", + "postulant", + "in", + "eo,", + "quod", + "semel", + "admissum", + "coerceri", + "reprimique", + "non", + "potest,", + "ut", + "propemodum", + "iustioribus", + "utamur", + "illis,", + "qui", + "omnino", + "auocent", + "a", + "philosophia,", + "quam", + "his,", + "qui", + "rebus", + "infinitis", + "modum", + "constituant", + "in", + "reque", + "eo", + "meliore,", + "quo", + "maior", + "sit,", + "mediocritatem", + "desiderent.", + ), + ("de", "finibus", "bonorum", "et", "malorum"), +) + +# Deeply nested parentheses +d1 = ((("3D",),),) +d2 = ( + ( + ( + ( + ( + ( + ( + ( + ( + ( + ( + ( + ( + ( + ( + ( + ( + ( + ( + ( + ( + ( + ( + ( + ( + ( + ( + ( + "¯\_(ツ)_/¯", + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), +) + +# Join and magic trailing comma +e1 = (1, 2) +e2 = ( + 1, + 2, +) +e3 = (1,) +e4 = ( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor", + "incididunt", +) + +# Empty tuples and comments +f1 = ( + # empty +) +f2 = () +f3 = ( # end-of-line + # own-line +) # trailing +f4 = ( # end-of-line + # own-line + # own-line 2 +) # trailing + +# Comments in other tuples +g1 = ( + # a + # b + 1, # c + # d +) # e +g2 = ( + # a + # b + 1, # c + # d + 2, # e + # f +) # g + +# Ensure the correct number of parentheses +h1 = (1, 2) +h2 = ( + 1, + "qweiurpoiqwurepqiurpqirpuqoiwrupqoirupqoirupqoiurpqiorupwqiourpqurpqurpqurpqurpqurpqurüqurqpuriq", +) +h3 = ( + 1, + "qweiurpoiqwurepqiurpqirpuqoiwrupqoirupqoirupqoiurpqiorupwqiourpqurpqurpqurpqurpqurpqurüqurqpuriq", +) +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__unary.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__unary.py.snap new file mode 100644 index 0000000000..e0325c897b --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__unary.py.snap @@ -0,0 +1,310 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/unary.py +--- +## Input +```py +if not aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb: + pass + +a = True +not a + +b = 10 +-b ++b + +## Leading operand comments + +if not ( + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + + +if ~( + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb): + pass + +if -( + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb): + pass + + +if +( + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb): + pass + +if ( + not + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + + +if ( + ~ + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb): + pass + +if ( + - + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb): + pass + + +if ( + + + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb): + pass + +## Parentheses + +if ( + # unary comment + not + # operand comment + ( + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + ) +): + pass + +if (not ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +) +): + pass + +if aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa & (not ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +) +): + pass + +if ( + not ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + ) + & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +): + pass + + +## Trailing operator comments + +if ( + not # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + + +if ( + ~ # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + +if ( + - # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + + +if ( + + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + + +## Varia + +if not \ + a: + pass + +# Regression: https://github.com/astral-sh/ruff/issues/5338 +if a and not aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: + ... +``` + +## Output +```py +if ( + not aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + +a = True +not a + +b = 10 +-b ++b + +## Leading operand comments + +if not ( + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + + +if ~( + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + +if -( + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + + +if +( + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + +if ( + not + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + + +if ( + ~ + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + +if ( + - + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + + +if ( + + + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + +## Parentheses + +if ( + # unary comment + not ( + # operand comment + # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + ) +): + pass + +if not ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + +if aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa & ( + not ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + ) +): + pass + +if not ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +) & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: + pass + + +## Trailing operator comments + +if ( + not aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa # comment + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + + +if ( + ~aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa # comment + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + +if ( + -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa # comment + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + + +if ( + +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa # comment + + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +): + pass + + +## Varia + +if not a: + pass + +# Regression: https://github.com/astral-sh/ruff/issues/5338 +if ( + a + and not aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +): + ... +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@skip_magic_trailing_comma.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@skip_magic_trailing_comma.py.snap new file mode 100644 index 0000000000..f3613924cc --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@skip_magic_trailing_comma.py.snap @@ -0,0 +1,88 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/skip_magic_trailing_comma.py +--- +## Input +```py +( + "First entry", + "Second entry", + "last with trailing comma", +) + +( + "First entry", + "Second entry", + "last without trailing comma" +) + +( + "First entry", + "Second entry", + "third entry", + "fourth entry", + "fifth entry", + "sixt entry", + "seventh entry", + "eighth entry", +) +``` + +## Outputs +### Output 1 +``` +indent-style = Spaces, size: 4 +line-width = 88 +quote-style = Double +magic-trailing-comma = Respect +``` + +```py +( + "First entry", + "Second entry", + "last with trailing comma", +) + +("First entry", "Second entry", "last without trailing comma") + +( + "First entry", + "Second entry", + "third entry", + "fourth entry", + "fifth entry", + "sixt entry", + "seventh entry", + "eighth entry", +) +``` + + +### Output 2 +``` +indent-style = Spaces, size: 4 +line-width = 88 +quote-style = Double +magic-trailing-comma = Ignore +``` + +```py +("First entry", "Second entry", "last with trailing comma") + +("First entry", "Second entry", "last without trailing comma") + +( + "First entry", + "Second entry", + "third entry", + "fourth entry", + "fifth entry", + "sixt entry", + "seventh entry", + "eighth entry", +) +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__ann_assign.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__ann_assign.py.snap new file mode 100644 index 0000000000..52d136a3be --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__ann_assign.py.snap @@ -0,0 +1,32 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/ann_assign.py +--- +## Input +```py +# Regression test: Don't forget the parentheses in the value when breaking +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: int = a + 1 * a + + +# Regression test: Don't forget the parentheses in the annotation when breaking +class DefaultRunner: + task_runner_cls: TaskRunnerProtocol | typing.Callable[[], typing.Any] = DefaultTaskRunner +``` + +## Output +```py +# Regression test: Don't forget the parentheses in the value when breaking +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: int = ( + a + 1 * a +) + + +# Regression test: Don't forget the parentheses in the annotation when breaking +class DefaultRunner: + task_runner_cls: ( + TaskRunnerProtocol | typing.Callable[[], typing.Any] + ) = DefaultTaskRunner +``` + + + diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__stmt_assign_py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__assign.py.snap similarity index 84% rename from crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__stmt_assign_py.snap rename to crates/ruff_python_formatter/tests/snapshots/format@statement__assign.py.snap index b4237a40c7..efcd0cca34 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__statement__stmt_assign_py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__assign.py.snap @@ -1,6 +1,6 @@ --- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/assign.py --- ## Input ```py @@ -16,8 +16,6 @@ a2 = ( a = asdf = fjhalsdljfalflaflapamsakjsdhflakjdslfjhalsdljfalflaflapamsakjsdhflakjdslfjhalsdljfal = 1 ``` - - ## Output ```py # break left hand side @@ -31,3 +29,4 @@ a = asdf = fjhalsdljfalflaflapamsakjsdhflakjdslfjhalsdljfalflaflapamsakjsdhflakj ``` + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__aug_assign.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__aug_assign.py.snap new file mode 100644 index 0000000000..715c6cd154 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__aug_assign.py.snap @@ -0,0 +1,25 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/aug_assign.py +--- +## Input +```py +tree_depth += 1 + +greeting += "This is very long, formal greeting for whomever is name here. Dear %s, it will break the line" % len( + name +) +``` + +## Output +```py +tree_depth += 1 + +greeting += ( + "This is very long, formal greeting for whomever is name here. Dear %s, it will break the line" + % len(name) +) +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__break.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__break.py.snap new file mode 100644 index 0000000000..1cffa28bec --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__break.py.snap @@ -0,0 +1,24 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/break.py +--- +## Input +```py +# leading comment +while True: # block comment + # inside comment + break # break comment + # post comment +``` + +## Output +```py +# leading comment +while True: # block comment + # inside comment + break # break comment + # post comment +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__class_definition.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__class_definition.py.snap new file mode 100644 index 0000000000..2adff74060 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__class_definition.py.snap @@ -0,0 +1,110 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/class_definition.py +--- +## Input +```py +class Test( + Aaaaaaaaaaaaaaaaa, + Bbbbbbbbbbbbbbbb, + DDDDDDDDDDDDDDDD, + EEEEEEEEEEEEEE, + metaclass=meta, +): + pass + + +class Test((Aaaaaaaaaaaaaaaaa), Bbbbbbbbbbbbbbbb, metaclass=meta): + pass + +class Test( # trailing class comment + Aaaaaaaaaaaaaaaaa, # trailing comment + + # in between comment + + Bbbbbbbbbbbbbbbb, + # another leading comment + DDDDDDDDDDDDDDDD, + EEEEEEEEEEEEEE, + # meta comment + metaclass=meta, # trailing meta comment +): + pass + +class Test((Aaaa)): + ... + + +class Test(aaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbb + cccccccccccccccccccccccc + dddddddddddddddddddddd + eeeeeeeee, ffffffffffffffffff, gggggggggggggggggg): + pass + +class Test(aaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbb * cccccccccccccccccccccccc + dddddddddddddddddddddd + eeeeeeeee, ffffffffffffffffff, gggggggggggggggggg): + pass + +class Test(Aaaa): # trailing comment + pass +``` + +## Output +```py +class Test( + Aaaaaaaaaaaaaaaaa, + Bbbbbbbbbbbbbbbb, + DDDDDDDDDDDDDDDD, + EEEEEEEEEEEEEE, + metaclass=meta, +): + pass + + +class Test((Aaaaaaaaaaaaaaaaa), Bbbbbbbbbbbbbbbb, metaclass=meta): + pass + + +class Test( + # trailing class comment + Aaaaaaaaaaaaaaaaa, # trailing comment + # in between comment + Bbbbbbbbbbbbbbbb, + # another leading comment + DDDDDDDDDDDDDDDD, + EEEEEEEEEEEEEE, + # meta comment + metaclass=meta, # trailing meta comment +): + pass + + +class Test((Aaaa)): + ... + + +class Test( + aaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbb + + cccccccccccccccccccccccc + + dddddddddddddddddddddd + + eeeeeeeee, + ffffffffffffffffff, + gggggggggggggggggg, +): + pass + + +class Test( + aaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbbb * cccccccccccccccccccccccc + + dddddddddddddddddddddd + + eeeeeeeee, + ffffffffffffffffff, + gggggggggggggggggg, +): + pass + + +class Test(Aaaa): # trailing comment + pass +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__delete.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__delete.py.snap new file mode 100644 index 0000000000..9ccbf27444 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__delete.py.snap @@ -0,0 +1,217 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/delete.py +--- +## Input +```py +x = 1 +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa = 1 +a, b, c, d = 1, 2, 3, 4 + +del a, b, c, d +del a, b, c, d # Trailing + +del a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a +del a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a # Trailing + +del ( + a, + a +) + +del ( + # Dangling comment +) + +# Delete something +del x # Deleted something +# Done deleting + +# Delete something +del ( + # Deleting something + x # Deleted something + # Finishing deletes +) # Completed +# Done deleting + +# Delete something +del ( + # Deleting something + x # Deleted something + # Finishing deletes +) # Completed +# Done deleting + +# Delete something +del ( + # Deleting something + x, # Deleted something + # Finishing deletes +) # Completed +# Done deleting + +# Delete something +del ( + # Deleting something + x # Deleted something + # Finishing deletes + # Dangling comment +) # Completed +# Done deleting + +# NOTE: This shouldn't format. See https://github.com/astral-sh/ruff/issues/5630. +# Delete something +del x, aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, b, c, d # Delete these +# Ready to delete + +# Delete something +del ( + x, + # Deleting this + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, + b, + c, + d, + # Deleted +) # Completed +# Done +``` + +## Output +```py +x = 1 +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa = 1 +a, b, c, d = 1, 2, 3, 4 + +del a, b, c, d +del a, b, c, d # Trailing + +del ( + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, +) +del ( + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, + a, +) # Trailing + +del (a, a) + +del ( + # Dangling comment +) + +# Delete something +del x # Deleted something +# Done deleting + +# Delete something +del ( + # Deleting something + x # Deleted something + # Finishing deletes +) # Completed +# Done deleting + +# Delete something +del ( + # Deleting something + x # Deleted something + # Finishing deletes +) # Completed +# Done deleting + +# Delete something +del ( + # Deleting something + x, # Deleted something + # Finishing deletes +) # Completed +# Done deleting + +# Delete something +del ( + # Deleting something + x # Deleted something + # Finishing deletes + # Dangling comment +) # Completed +# Done deleting + +# NOTE: This shouldn't format. See https://github.com/astral-sh/ruff/issues/5630. +# Delete something +del x, aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, b, c, d # Delete these +# Ready to delete + +# Delete something +del ( + x, + # Deleting this + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, + b, + c, + d, + # Deleted +) # Completed +# Done +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__for.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__for.py.snap new file mode 100644 index 0000000000..637372d81f --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__for.py.snap @@ -0,0 +1,82 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/for.py +--- +## Input +```py +for x in y: # trailing test comment + pass # trailing last statement comment + + # trailing for body comment + +# leading else comment + +else: # trailing else comment + pass + + # trailing else body comment + + +for aVeryLongNameThatSpillsOverToTheNextLineBecauseItIsExtremelyLongAndGoesOnAndOnAndOnAndOnAndOnAndOnAndOnAndOnAndOn in anotherVeryLongNameThatSpillsOverToTheNextLineBecauseItIsExtremelyLongAndGoesOnAndOnAndOnAndOnAndOnAndOnAndOnAndOnAndOn: # trailing comment + pass + +else: + ... + +for ( + x, + y, + ) in z: # comment + ... + + +# remove brackets around x,y but keep them around z,w +for (x, y) in (z, w): + ... + + +# type comment +for x in (): # type: int + ... +``` + +## Output +```py +for x in y: # trailing test comment + pass # trailing last statement comment + + # trailing for body comment + +# leading else comment + +else: # trailing else comment + pass + + # trailing else body comment + + +for aVeryLongNameThatSpillsOverToTheNextLineBecauseItIsExtremelyLongAndGoesOnAndOnAndOnAndOnAndOnAndOnAndOnAndOnAndOn in anotherVeryLongNameThatSpillsOverToTheNextLineBecauseItIsExtremelyLongAndGoesOnAndOnAndOnAndOnAndOnAndOnAndOnAndOnAndOn: # trailing comment + pass + +else: + ... + +for ( + x, + y, +) in z: # comment + ... + + +# remove brackets around x,y but keep them around z,w +for x, y in (z, w): + ... + + +# type comment +for x in (): # type: int + ... +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__function.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__function.py.snap new file mode 100644 index 0000000000..341956733f --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__function.py.snap @@ -0,0 +1,517 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/function.py +--- +## Input +```py +# Dangling comments +def test( + # comment + + # another + +): ... + + +# Argument empty line spacing +def test( + # comment + a, + + # another + + b, +): ... + + +### Different function argument wrappings + +def single_line(aaaaaaaaaaaaaaaaaaaaaaaaaaaaa, bbbbbbbbbbbbbbb, ccccccccccccccccc): + pass + +def arguments_on_their_own_line(aaaaaaaaaaaaaaaaaaaaaaaaaaaaa, bbbbbbbbbbbbbbb, ccccccccccc, ddddddddddddd, eeeeeee): + pass + +def argument_per_line(aaaaaaaaaaaaaaaaaaaaaaaaaaaaa, bbbbbbbbbbbbbbb, ccccccccccccccccc, ddddddddddddd, eeeeeeeeeeeeeeee, ffffffffffff): + pass + +def last_pos_only_trailing_comma(a, b, /,): + pass + +def last_pos_no_trailing_comma(a, b, /): + pass + + +def varg_with_leading_comments( + a, b, + # comment + *args +): ... + +def kwarg_with_leading_comments( + a, b, + # comment + **kwargs +): ... + +def argument_with_long_default( + a, + b = ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc + [ + dddddddddddddddddddd, eeeeeeeeeeeeeeeeeeee, ffffffffffffffffffffffff + ], + h = [] +): ... + + +def argument_with_long_type_annotation( + a, + b: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx | yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy | zzzzzzzzzzzzzzzzzzz = [0, 1, 2, 3], + h = [] +): ... + + +def test(): ... + +# Comment +def with_leading_comment(): ... + +# Comment that could be mistaken for a trailing comment of the function declaration when +# looking from the position of the if +# Regression test for https://github.com/python/cpython/blob/ad56340b665c5d8ac1f318964f71697bba41acb7/Lib/logging/__init__.py#L253-L260 +if True: + def f1(): + pass # a +else: + pass + +# Here it's actually a trailing comment +if True: + def f2(): + pass + # a +else: + pass + +# Make sure the star is printed +# Regression test for https://github.com/python/cpython/blob/7199584ac8632eab57612f595a7162ab8d2ebbc0/Lib/warnings.py#L513 +def f(arg1=1, *, kwonlyarg1, kwonlyarg2=2): + pass + + +# Regression test for https://github.com/astral-sh/ruff/issues/5176#issuecomment-1598171989 +def foo( + b=3 + 2 # comment +): + ... + + +# Comments on the slash or the star, both of which don't have a node +def f11( + a, + # positional only comment, leading + /, # positional only comment, trailing + b, +): + pass + +def f12( + a=1, + # positional only comment, leading + /, # positional only comment, trailing + b=2, +): + pass + +def f13( + a, + # positional only comment, leading + /, # positional only comment, trailing +): + pass + +def f21( + a=1, + # keyword only comment, leading + *, # keyword only comment, trailing + b=2, +): + pass + +def f22( + a, + # keyword only comment, leading + *, # keyword only comment, trailing + b, +): + pass + +def f23( + a, + # keyword only comment, leading + *args, # keyword only comment, trailing + b, +): + pass + +def f24( + # keyword only comment, leading + *, # keyword only comment, trailing + a +): + pass + + +def f31( + a=1, + # positional only comment, leading + /, # positional only comment, trailing + b=2, + # keyword only comment, leading + *, # keyword only comment, trailing + c=3, +): + pass + +def f32( + a, + # positional only comment, leading + /, # positional only comment, trailing + b, + # keyword only comment, leading + *, # keyword only comment, trailing + c, +): + pass + +def f33( + a, + # positional only comment, leading + /, # positional only comment, trailing + # keyword only comment, leading + *args, # keyword only comment, trailing + c, +): + pass + + +def f34( + a, + # positional only comment, leading + /, # positional only comment, trailing + # keyword only comment, leading + *, # keyword only comment, trailing + c, +): + pass + +def f35( + # keyword only comment, leading + *, # keyword only comment, trailing + c, +): + pass + +# Multiple trailing comments +def f41( + a, + / # 1 + , # 2 + # 3 + * # 4 + , # 5 + c, +): + pass + +# Multiple trailing comments strangely places. The goal here is only stable formatting, +# the comments are placed to strangely to keep their relative position intact +def f42( + a, + / # 1 + # 2 + , # 3 + # 4 + * # 5 + # 6 + , # 7 + c, +): + pass +``` + +## Output +```py +# Dangling comments +def test( + # comment + # another +): + ... + + +# Argument empty line spacing +def test( + # comment + a, + # another + b, +): + ... + + +### Different function argument wrappings + +def single_line(aaaaaaaaaaaaaaaaaaaaaaaaaaaaa, bbbbbbbbbbbbbbb, ccccccccccccccccc): + pass + + +def arguments_on_their_own_line( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaa, bbbbbbbbbbbbbbb, ccccccccccc, ddddddddddddd, eeeeeee +): + pass + + +def argument_per_line( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaa, + bbbbbbbbbbbbbbb, + ccccccccccccccccc, + ddddddddddddd, + eeeeeeeeeeeeeeee, + ffffffffffff, +): + pass + + +def last_pos_only_trailing_comma( + a, + b, + /, +): + pass + + +def last_pos_no_trailing_comma(a, b, /): + pass + + +def varg_with_leading_comments( + a, + b, + # comment + *args, +): + ... + + +def kwarg_with_leading_comments( + a, + b, + # comment + **kwargs, +): + ... + + +def argument_with_long_default( + a, + b=ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc + + [dddddddddddddddddddd, eeeeeeeeeeeeeeeeeeee, ffffffffffffffffffffffff], + h=[], +): + ... + + +def argument_with_long_type_annotation( + a, + b: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + | yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy + | zzzzzzzzzzzzzzzzzzz = [0, 1, 2, 3], + h=[], +): + ... + + +def test(): + ... + + +# Comment +def with_leading_comment(): + ... + + +# Comment that could be mistaken for a trailing comment of the function declaration when +# looking from the position of the if +# Regression test for https://github.com/python/cpython/blob/ad56340b665c5d8ac1f318964f71697bba41acb7/Lib/logging/__init__.py#L253-L260 +if True: + def f1(): + pass # a +else: + pass + +# Here it's actually a trailing comment +if True: + def f2(): + pass + # a +else: + pass + + +# Make sure the star is printed +# Regression test for https://github.com/python/cpython/blob/7199584ac8632eab57612f595a7162ab8d2ebbc0/Lib/warnings.py#L513 +def f(arg1=1, *, kwonlyarg1, kwonlyarg2=2): + pass + + +# Regression test for https://github.com/astral-sh/ruff/issues/5176#issuecomment-1598171989 +def foo( + b=3 + 2, # comment +): + ... + + +# Comments on the slash or the star, both of which don't have a node +def f11( + a, + # positional only comment, leading + /, # positional only comment, trailing + b, +): + pass + + +def f12( + a=1, + # positional only comment, leading + /, # positional only comment, trailing + b=2, +): + pass + + +def f13( + a, + # positional only comment, leading + /, # positional only comment, trailing +): + pass + + +def f21( + a=1, + # keyword only comment, leading + *, # keyword only comment, trailing + b=2, +): + pass + + +def f22( + a, + # keyword only comment, leading + *, # keyword only comment, trailing + b, +): + pass + + +def f23( + a, + # keyword only comment, leading + *args, # keyword only comment, trailing + b, +): + pass + + +def f24( + # keyword only comment, leading + *, # keyword only comment, trailing + a, +): + pass + + +def f31( + a=1, + # positional only comment, leading + /, # positional only comment, trailing + b=2, + # keyword only comment, leading + *, # keyword only comment, trailing + c=3, +): + pass + + +def f32( + a, + # positional only comment, leading + /, # positional only comment, trailing + b, + # keyword only comment, leading + *, # keyword only comment, trailing + c, +): + pass + + +def f33( + a, + # positional only comment, leading + /, # positional only comment, trailing + # keyword only comment, leading + *args, # keyword only comment, trailing + c, +): + pass + + +def f34( + a, + # positional only comment, leading + /, # positional only comment, trailing + # keyword only comment, leading + *, # keyword only comment, trailing + c, +): + pass + + +def f35( + # keyword only comment, leading + *, # keyword only comment, trailing + c, +): + pass + + +# Multiple trailing comments +def f41( + a, + /, # 1 # 2 + # 3 + *, # 4 # 5 + c, +): + pass + + +# Multiple trailing comments strangely places. The goal here is only stable formatting, +# the comments are placed to strangely to keep their relative position intact +def f42( + a, + /, # 1 + # 2 + # 3 + # 4 + *, # 5 # 7 + # 6 + c, +): + pass +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__if.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__if.py.snap new file mode 100644 index 0000000000..9bbe6803f2 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__if.py.snap @@ -0,0 +1,159 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/if.py +--- +## Input +```py +if x == y: # trailing if condition + pass # trailing `pass` comment + # Root `if` trailing comment + +# Leading elif comment +elif x < y: # trailing elif condition + pass + # `elif` trailing comment + +# Leading else comment +else: # trailing else condition + pass + # `else` trailing comment + + +if x == y: + if y == z: + ... + + if a == b: + ... + else: # trailing comment + ... + + # trailing else comment + +# leading else if comment +elif aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + [ + 11111111111111111111111111, + 2222222222222222222222, + 3333333333 + ]: + ... + + +else: + ... + +# Regression test: Don't drop the trailing comment by associating it with the elif +# instead of the else. +# Originally found in https://github.com/python/cpython/blob/ab3823a97bdeefb0266b3c8d493f7f6223ce3686/Lib/dataclasses.py#L539 + +if "if 1": + pass +elif "elif 1": + pass +# Don't drop this comment 1 +x = 1 + +if "if 2": + pass +elif "elif 2": + pass +else: + pass +# Don't drop this comment 2 +x = 2 + +if "if 3": + pass +else: + pass +# Don't drop this comment 3 +x = 3 + +# Regression test for a following if that could get confused for an elif +# Originally found in https://github.com/gradio-app/gradio/blob/1570b94a02d23d051ae137e0063974fd8a48b34e/gradio/external.py#L478 +if True: + pass +else: # Comment + if False: + pass + pass +``` + +## Output +```py +if x == y: # trailing if condition + pass # trailing `pass` comment + # Root `if` trailing comment + +# Leading elif comment +elif x < y: # trailing elif condition + pass + # `elif` trailing comment + +# Leading else comment +else: # trailing else condition + pass + # `else` trailing comment + + +if x == y: + if y == z: + ... + + if a == b: + ... + else: # trailing comment + ... + + # trailing else comment + +# leading else if comment +elif aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + [ + 11111111111111111111111111, + 2222222222222222222222, + 3333333333, +]: + ... + +else: + ... + +# Regression test: Don't drop the trailing comment by associating it with the elif +# instead of the else. +# Originally found in https://github.com/python/cpython/blob/ab3823a97bdeefb0266b3c8d493f7f6223ce3686/Lib/dataclasses.py#L539 + +if "if 1": + pass +elif "elif 1": + pass +# Don't drop this comment 1 +x = 1 + +if "if 2": + pass +elif "elif 2": + pass +else: + pass +# Don't drop this comment 2 +x = 2 + +if "if 3": + pass +else: + pass +# Don't drop this comment 3 +x = 3 + +# Regression test for a following if that could get confused for an elif +# Originally found in https://github.com/gradio-app/gradio/blob/1570b94a02d23d051ae137e0063974fd8a48b34e/gradio/external.py#L478 +if True: + pass +else: # Comment + if False: + pass + pass +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__import.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__import.py.snap new file mode 100644 index 0000000000..0208606383 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__import.py.snap @@ -0,0 +1,28 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/import.py +--- +## Input +```py +from a import aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa +from a import aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa +from a import aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa as dfgsdfgsd, aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa as sdkjflsdjlahlfd +``` + +## Output +```py +from a import ( + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa +) +from a import ( + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, +) +from a import ( + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa as dfgsdfgsd, + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa as sdkjflsdjlahlfd, +) +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__import_from.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__import_from.py.snap new file mode 100644 index 0000000000..0d8c90572e --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__import_from.py.snap @@ -0,0 +1,46 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/import_from.py +--- +## Input +```py +from a import aksjdhflsakhdflkjsadlfajkslhf +from a import ( + aksjdhflsakhdflkjsadlfajkslhf, +) +from a import ( + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, +) +from a import ( + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, +) +from a import ( + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa as dfgsdfgsd, + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa as sdkjflsdjlahlfd, +) +from aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa import * +``` + +## Output +```py +from a import aksjdhflsakhdflkjsadlfajkslhf +from a import ( + aksjdhflsakhdflkjsadlfajkslhf, +) +from a import ( + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, +) +from a import ( + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, +) +from a import ( + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa as dfgsdfgsd, + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa as sdkjflsdjlahlfd, +) +from aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa import * +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__raise.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__raise.py.snap new file mode 100644 index 0000000000..a90849f43b --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__raise.py.snap @@ -0,0 +1,220 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/raise.py +--- +## Input +```py +raise a from aksjdhflsakhdflkjsadlfajkslhf +raise a from (aksjdhflsakhdflkjsadlfajkslhf,) +raise (aaaaa.aaa(a).a) from (aksjdhflsakhdflkjsadlfajkslhf) + +raise a from (aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa,) +raise a from OsError("aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa") + +# some comment +raise a from aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa # some comment +# some comment + +raise OsError("aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa") from e + + +raise OsError( + # should i stay + "aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa" # mhhh very long + # or should i go +) from e # here is e + +raise OsError("aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa") from OsError("aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa") + +raise OsError( + "aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa" +) from a.aaaaa( + aaa +).a(aaaa) + +raise OsError( + "aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa" +) from a.aaaaa(aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa).a(aaaa) + +raise aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbb + cccccccccccccccccccccc + ddddddddddddddddddddddddd +raise aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbb + (cccccccccccccccccccccc + ddddddddddddddddddddddddd) +raise (aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbb + cccccccccccccccccccccc + ddddddddddddddddddddddddd) + + +raise ( # hey + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + # Holala + + bbbbbbbbbbbbbbbbbbbbbbbbbb # stay + + cccccccccccccccccccccc + ddddddddddddddddddddddddd # where I'm going + # I don't know +) # whaaaaat +# the end + +raise ( # hey 2 + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + # Holala + "bbbbbbbbbbbbbbbbbbbbbbbb" # stay + "ccccccccccccccccccccccc" "dddddddddddddddddddddddd" # where I'm going + # I don't know +) # whaaaaat + +# some comment +raise aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa[aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:bbbbbbbbbbbbbbbbbbbbbbbbbb] + +raise aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa < aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa , + + /// Path to custom typings (stub) modules. + pub(crate) stub_path: Option, + + /// Path to a directory containing one or more virtual environment + /// directories. This is used in conjunction with the "venv" name in + /// the config file to identify the python environment used for resolving + /// third-party modules. + pub(crate) venv_path: Option, + + /// Default venv environment. + pub(crate) venv: Option, +} diff --git a/crates/ruff_python_resolver/src/execution_environment.rs b/crates/ruff_python_resolver/src/execution_environment.rs new file mode 100644 index 0000000000..b969ddc42b --- /dev/null +++ b/crates/ruff_python_resolver/src/execution_environment.rs @@ -0,0 +1,19 @@ +use std::path::PathBuf; + +use crate::python_platform::PythonPlatform; +use crate::python_version::PythonVersion; + +#[derive(Debug)] +pub(crate) struct ExecutionEnvironment { + /// The root directory of the execution environment. + pub(crate) root: PathBuf, + + /// The Python version of the execution environment. + pub(crate) python_version: PythonVersion, + + /// The Python platform of the execution environment. + pub(crate) python_platform: PythonPlatform, + + /// The extra search paths of the execution environment. + pub(crate) extra_paths: Vec, +} diff --git a/crates/ruff_python_resolver/src/host.rs b/crates/ruff_python_resolver/src/host.rs new file mode 100644 index 0000000000..be9b0a5e60 --- /dev/null +++ b/crates/ruff_python_resolver/src/host.rs @@ -0,0 +1,43 @@ +//! Expose the host environment to the resolver. + +use std::path::PathBuf; + +use crate::python_platform::PythonPlatform; +use crate::python_version::PythonVersion; + +/// A trait to expose the host environment to the resolver. +pub(crate) trait Host { + /// The search paths to use when resolving Python modules. + fn python_search_paths(&self) -> Vec; + + /// The Python version to use when resolving Python modules. + fn python_version(&self) -> PythonVersion; + + /// The OS platform to use when resolving Python modules. + fn python_platform(&self) -> PythonPlatform; +} + +/// A host that exposes a fixed set of search paths. +pub(crate) struct StaticHost { + search_paths: Vec, +} + +impl StaticHost { + pub(crate) fn new(search_paths: Vec) -> Self { + Self { search_paths } + } +} + +impl Host for StaticHost { + fn python_search_paths(&self) -> Vec { + self.search_paths.clone() + } + + fn python_version(&self) -> PythonVersion { + PythonVersion::Py312 + } + + fn python_platform(&self) -> PythonPlatform { + PythonPlatform::Darwin + } +} diff --git a/crates/ruff_python_resolver/src/implicit_imports.rs b/crates/ruff_python_resolver/src/implicit_imports.rs new file mode 100644 index 0000000000..693b6572ca --- /dev/null +++ b/crates/ruff_python_resolver/src/implicit_imports.rs @@ -0,0 +1,176 @@ +use std::collections::BTreeMap; +use std::ffi::OsStr; +use std::io; +use std::path::{Path, PathBuf}; + +use crate::{native_module, py_typed}; + +/// A map of the submodules that are present in a namespace package. +/// +/// Namespace packages lack an `__init__.py` file. So when resolving symbols from a namespace +/// package, the symbols must be present as submodules. This map contains the submodules that are +/// present in the namespace package, keyed by their module name. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub(crate) struct ImplicitImports(BTreeMap); + +impl ImplicitImports { + /// Find the "implicit" imports within the namespace package at the given path. + pub(crate) fn find(dir_path: &Path, exclusions: &[&Path]) -> io::Result { + let mut submodules: BTreeMap = BTreeMap::new(); + + // Enumerate all files and directories in the path, expanding links. + for entry in dir_path.read_dir()?.flatten() { + let file_type = entry.file_type()?; + + let path = entry.path(); + if exclusions.contains(&path.as_path()) { + continue; + } + + // TODO(charlie): Support symlinks. + if file_type.is_file() { + // Add implicit file-based modules. + let Some(extension) = path.extension() else { + continue; + }; + + let (file_stem, is_native_lib) = if extension == "py" || extension == "pyi" { + // E.g., `foo.py` becomes `foo`. + let file_stem = path.file_stem().and_then(OsStr::to_str); + let is_native_lib = false; + (file_stem, is_native_lib) + } else if native_module::is_native_module_file_extension(extension) { + // E.g., `foo.abi3.so` becomes `foo`. + let file_stem = native_module::native_module_name(&path); + let is_native_lib = true; + (file_stem, is_native_lib) + } else { + continue; + }; + + let Some(name) = file_stem else { + continue; + }; + + // Always prefer stub files over non-stub files. + if submodules + .get(name) + .map_or(true, |implicit_import| !implicit_import.is_stub_file) + { + submodules.insert( + name.to_string(), + ImplicitImport { + is_stub_file: extension == "pyi", + is_native_lib, + path, + py_typed: None, + }, + ); + } + } else if file_type.is_dir() { + // Add implicit directory-based modules. + let py_file_path = path.join("__init__.py"); + let pyi_file_path = path.join("__init__.pyi"); + + let (path, is_stub_file) = if py_file_path.exists() { + (py_file_path, false) + } else if pyi_file_path.exists() { + (pyi_file_path, true) + } else { + continue; + }; + + let Some(name) = path.file_name().and_then(OsStr::to_str) else { + continue; + }; + submodules.insert( + name.to_string(), + ImplicitImport { + is_stub_file, + is_native_lib: false, + py_typed: py_typed::get_py_typed_info(&path), + path, + }, + ); + } + } + + Ok(Self(submodules)) + } + + /// Filter [`ImplicitImports`] to only those symbols that were imported. + pub(crate) fn filter(&self, imported_symbols: &[String]) -> Option { + if self.is_empty() || imported_symbols.is_empty() { + return None; + } + + let filtered: BTreeMap = self + .iter() + .filter(|(name, _)| imported_symbols.contains(name)) + .map(|(name, implicit_import)| (name.clone(), implicit_import.clone())) + .collect(); + + if filtered.len() == self.len() { + return None; + } + + Some(Self(filtered)) + } + + /// Returns `true` if the [`ImplicitImports`] resolves all the symbols requested by a + /// module descriptor. + pub(crate) fn resolves_namespace_package(&self, imported_symbols: &[String]) -> bool { + if !imported_symbols.is_empty() { + // TODO(charlie): Pyright uses: + // + // ```typescript + // !Array.from(moduleDescriptor.importedSymbols.keys()).some((symbol) => implicitImports.has(symbol))` + // ``` + // + // However, that only checks if _any_ of the symbols are in the implicit imports. + for symbol in imported_symbols { + if !self.has(symbol) { + return false; + } + } + } else if self.is_empty() { + return false; + } + true + } + + /// Returns `true` if the module is present in the namespace package. + pub(crate) fn has(&self, name: &str) -> bool { + self.0.contains_key(name) + } + + /// Returns the number of implicit imports in the namespace package. + pub(crate) fn len(&self) -> usize { + self.0.len() + } + + /// Returns `true` if there are no implicit imports in the namespace package. + pub(crate) fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Returns an iterator over the implicit imports in the namespace package. + pub(crate) fn iter(&self) -> impl Iterator { + self.0.iter() + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct ImplicitImport { + /// Whether the implicit import is a stub file. + pub(crate) is_stub_file: bool, + + /// Whether the implicit import is a native module. + pub(crate) is_native_lib: bool, + + /// The path to the implicit import. + pub(crate) path: PathBuf, + + /// The `py.typed` information for the implicit import, if any. + pub(crate) py_typed: Option, +} diff --git a/crates/ruff_python_resolver/src/import_result.rs b/crates/ruff_python_resolver/src/import_result.rs new file mode 100644 index 0000000000..704781f420 --- /dev/null +++ b/crates/ruff_python_resolver/src/import_result.rs @@ -0,0 +1,121 @@ +//! Interface that describes the output of the import resolver. + +use std::path::PathBuf; + +use crate::implicit_imports::ImplicitImports; +use crate::py_typed::PyTypedInfo; + +#[derive(Debug, Clone, PartialEq, Eq)] +#[allow(clippy::struct_excessive_bools)] +pub(crate) struct ImportResult { + /// Whether the import name was relative (e.g., ".foo"). + pub(crate) is_relative: bool, + + /// Whether the import was resolved to a file or module. + pub(crate) is_import_found: bool, + + /// The path was partially resolved, but the specific submodule + /// defining the import was not found. For example, `foo.bar` was + /// not found, but `foo` was found. + pub(crate) is_partly_resolved: bool, + + /// The import refers to a namespace package (i.e., a folder without + /// an `__init__.py[i]` file at the final level of resolution). By + /// convention, we insert empty `PathBuf` segments into the resolved + /// paths vector to indicate intermediary namespace packages. + pub(crate) is_namespace_package: bool, + + /// The final resolved directory contains an `__init__.py[i]` file. + pub(crate) is_init_file_present: bool, + + /// The import resolved to a stub (`.pyi`) file within a stub package. + pub(crate) is_stub_package: bool, + + /// The import resolved to a built-in, local, or third-party module. + pub(crate) import_type: ImportType, + + /// A vector of resolved absolute paths for each file in the module + /// name. Typically includes a sequence of `__init__.py` files, followed + /// by the Python file defining the import itself, though the exact + /// structure can vary. For example, namespace packages will be represented + /// by empty `PathBuf` segments in the vector. + /// + /// For example, resolving `import foo.bar` might yield `./foo/__init__.py` and `./foo/bar.py`, + /// or `./foo/__init__.py` and `./foo/bar/__init__.py`. + pub(crate) resolved_paths: Vec, + + /// The search path used to resolve the module. + pub(crate) search_path: Option, + + /// The resolved file is a type hint (i.e., a `.pyi` file), rather + /// than a Python (`.py`) file. + pub(crate) is_stub_file: bool, + + /// The resolved file is a native library. + pub(crate) is_native_lib: bool, + + /// The resolved file is a hint hint (i.e., a `.pyi` file) from + /// `typeshed` in the standard library. + pub(crate) is_stdlib_typeshed_file: bool, + + /// The resolved file is a hint hint (i.e., a `.pyi` file) from + /// `typeshed` in third-party stubs. + pub(crate) is_third_party_typeshed_file: bool, + + /// The resolved file is a type hint (i.e., a `.pyi` file) from + /// the configured typing directory. + pub(crate) is_local_typings_file: bool, + + /// A map from file to resolved path, for all implicitly imported + /// modules that are part of a namespace package. + pub(crate) implicit_imports: ImplicitImports, + + /// Any implicit imports whose symbols were explicitly imported (i.e., via + /// a `from x import y` statement). + pub(crate) filtered_implicit_imports: ImplicitImports, + + /// If the import resolved to a type hint (i.e., a `.pyi` file), then + /// a non-type-hint resolution will be stored here. + pub(crate) non_stub_import_result: Option>, + + /// Information extracted from the `py.typed` in the package used to + /// resolve the import, if any. + pub(crate) py_typed_info: Option, + + /// The directory of the package, if any. + pub(crate) package_directory: Option, +} + +impl ImportResult { + /// An import result that indicates that the import was not found. + pub(crate) fn not_found() -> Self { + Self { + is_relative: false, + is_import_found: false, + is_partly_resolved: false, + is_namespace_package: false, + is_init_file_present: false, + is_stub_package: false, + import_type: ImportType::Local, + resolved_paths: vec![], + search_path: None, + is_stub_file: false, + is_native_lib: false, + is_stdlib_typeshed_file: false, + is_third_party_typeshed_file: false, + is_local_typings_file: false, + implicit_imports: ImplicitImports::default(), + filtered_implicit_imports: ImplicitImports::default(), + non_stub_import_result: None, + py_typed_info: None, + package_directory: None, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ImportType { + BuiltIn, + ThirdParty, + Local, +} diff --git a/crates/ruff_python_resolver/src/lib.rs b/crates/ruff_python_resolver/src/lib.rs new file mode 100644 index 0000000000..52be653d6c --- /dev/null +++ b/crates/ruff_python_resolver/src/lib.rs @@ -0,0 +1,950 @@ +#![allow(dead_code)] + +mod config; +mod execution_environment; +mod host; +mod implicit_imports; +mod import_result; +mod module_descriptor; +mod native_module; +mod py_typed; +mod python_platform; +mod python_version; +mod resolver; +mod search; + +#[cfg(test)] +mod tests { + use std::fs::{create_dir_all, File}; + use std::io::{self, Write}; + use std::path::{Path, PathBuf}; + + use log::debug; + use tempfile::TempDir; + + use crate::config::Config; + use crate::execution_environment::ExecutionEnvironment; + use crate::host; + use crate::import_result::{ImportResult, ImportType}; + use crate::module_descriptor::ImportModuleDescriptor; + use crate::python_platform::PythonPlatform; + use crate::python_version::PythonVersion; + use crate::resolver::resolve_import; + + /// Create a file at the given path with the given content. + fn create(path: PathBuf, content: &str) -> io::Result { + if let Some(parent) = path.parent() { + create_dir_all(parent)?; + } + let mut f = File::create(&path)?; + f.write_all(content.as_bytes())?; + f.sync_all()?; + + Ok(path) + } + + /// Create an empty file at the given path. + fn empty(path: PathBuf) -> io::Result { + create(path, "") + } + + /// Create a partial `py.typed` file at the given path. + fn partial(path: PathBuf) -> io::Result { + create(path, "partial\n") + } + + /// Create a `py.typed` file at the given path. + fn typed(path: PathBuf) -> io::Result { + create(path, "# typed") + } + + #[derive(Debug, Default)] + struct ResolverOptions { + extra_paths: Vec, + library: Option, + stub_path: Option, + typeshed_path: Option, + venv_path: Option, + venv: Option, + } + + fn resolve_options( + source_file: impl AsRef, + name: &str, + root: impl Into, + options: ResolverOptions, + ) -> ImportResult { + let ResolverOptions { + extra_paths, + library, + stub_path, + typeshed_path, + venv_path, + venv, + } = options; + + let execution_environment = ExecutionEnvironment { + root: root.into(), + python_version: PythonVersion::Py37, + python_platform: PythonPlatform::Darwin, + extra_paths, + }; + + let module_descriptor = ImportModuleDescriptor { + leading_dots: name.chars().take_while(|c| *c == '.').count(), + name_parts: name + .chars() + .skip_while(|c| *c == '.') + .collect::() + .split('.') + .map(std::string::ToString::to_string) + .collect(), + imported_symbols: Vec::new(), + }; + + let config = Config { + typeshed_path, + stub_path, + venv_path, + venv, + }; + + let host = host::StaticHost::new(if let Some(library) = library { + vec![library] + } else { + Vec::new() + }); + + resolve_import( + source_file.as_ref(), + &execution_environment, + &module_descriptor, + &config, + &host, + ) + } + + fn setup() { + env_logger::builder().is_test(true).try_init().ok(); + } + + macro_rules! assert_debug_snapshot_normalize_paths { + ($value: ident) => {{ + // The debug representation for the backslash are two backslashes (escaping) + let $value = std::format!("{:#?}", $value).replace("\\\\", "/"); + // `insta::assert_snapshot` uses the debug representation of the string, which would + // be a single line containing `\n` + insta::assert_display_snapshot!($value); + }}; + } + + #[test] + fn partial_stub_file_exists() -> io::Result<()> { + setup(); + + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + + let temp_dir = TempDir::new()?; + let library = temp_dir.path().join("lib").join("site-packages"); + + partial(library.join("myLib-stubs/py.typed"))?; + let partial_stub_pyi = empty(library.join("myLib-stubs").join("partialStub.pyi"))?; + let partial_stub_py = empty(library.join("myLib/partialStub.py"))?; + + let result = resolve_options( + partial_stub_py, + "myLib.partialStub", + root, + ResolverOptions { + library: Some(library), + ..Default::default() + }, + ); + + assert!(result.is_import_found); + assert!(result.is_stub_file); + assert_eq!(result.import_type, ImportType::ThirdParty); + assert_eq!( + result.resolved_paths, + // TODO(charlie): Pyright matches on `libraryRoot, 'myLib', 'partialStub.pyi'` here. + // But that file doesn't exist. There's some kind of transform. + vec![PathBuf::new(), partial_stub_pyi] + ); + + Ok(()) + } + + #[test] + fn partial_stub_init_exists() -> io::Result<()> { + setup(); + + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + + let temp_dir = TempDir::new()?; + let library = temp_dir.path().join("lib").join("site-packages"); + + partial(library.join("myLib-stubs/py.typed"))?; + let partial_stub_init_pyi = empty(library.join("myLib-stubs/__init__.pyi"))?; + let partial_stub_init_py = empty(library.join("myLib/__init__.py"))?; + + let result = resolve_options( + partial_stub_init_py, + "myLib", + root, + ResolverOptions { + library: Some(library), + ..Default::default() + }, + ); + + assert!(result.is_import_found); + assert!(result.is_stub_file); + assert_eq!(result.import_type, ImportType::ThirdParty); + assert_eq!( + result.resolved_paths, + // TODO(charlie): Pyright matches on `libraryRoot, 'myLib', '__init__.pyi'` here. + // But that file doesn't exist. There's some kind of transform. + vec![partial_stub_init_pyi] + ); + + Ok(()) + } + + #[test] + fn side_by_side_files() -> io::Result<()> { + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + + let temp_dir = TempDir::new()?; + let library = temp_dir.path().join("lib").join("site-packages"); + + partial(library.join("myLib-stubs/py.typed"))?; + empty(library.join("myLib/partialStub.pyi"))?; + empty(library.join("myLib/partialStub.py"))?; + empty(library.join("myLib/partialStub2.py"))?; + let my_file = empty(root.join("myFile.py"))?; + let side_by_side_stub_file = empty(library.join("myLib-stubs/partialStub.pyi"))?; + let partial_stub_file = empty(library.join("myLib-stubs/partialStub2.pyi"))?; + + // Stub package wins over original package (per PEP 561 rules). + let side_by_side_result = resolve_options( + &my_file, + "myLib.partialStub", + root, + ResolverOptions { + library: Some(library.clone()), + ..Default::default() + }, + ); + assert!(side_by_side_result.is_import_found); + assert!(side_by_side_result.is_stub_file); + assert_eq!( + side_by_side_result.resolved_paths, + vec![PathBuf::new(), side_by_side_stub_file] + ); + + // Side by side stub doesn't completely disable partial stub. + let partial_stub_result = resolve_options( + &my_file, + "myLib.partialStub2", + root, + ResolverOptions { + library: Some(library), + ..Default::default() + }, + ); + assert!(partial_stub_result.is_import_found); + assert!(partial_stub_result.is_stub_file); + assert_eq!( + partial_stub_result.resolved_paths, + vec![PathBuf::new(), partial_stub_file] + ); + + Ok(()) + } + + #[test] + fn stub_package() -> io::Result<()> { + setup(); + + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + + let temp_dir = TempDir::new()?; + let library = temp_dir.path().join("lib").join("site-packages"); + + empty(library.join("myLib-stubs/stub.pyi"))?; + empty(library.join("myLib-stubs/__init__.pyi"))?; + let partial_stub_py = empty(library.join("myLib/partialStub.py"))?; + + let result = resolve_options( + partial_stub_py, + "myLib.partialStub", + root, + ResolverOptions { + library: Some(library), + ..Default::default() + }, + ); + + // If fully typed stub package exists, that wins over the real package. + assert!(!result.is_import_found); + + Ok(()) + } + + #[test] + fn stub_namespace_package() -> io::Result<()> { + setup(); + + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + + let temp_dir = TempDir::new()?; + let library = temp_dir.path().join("lib").join("site-packages"); + + empty(library.join("myLib-stubs/stub.pyi"))?; + let partial_stub_py = empty(library.join("myLib/partialStub.py"))?; + + let result = resolve_options( + partial_stub_py.clone(), + "myLib.partialStub", + root, + ResolverOptions { + library: Some(library), + ..Default::default() + }, + ); + + // If fully typed stub package exists, that wins over the real package. + assert!(result.is_import_found); + assert!(!result.is_stub_file); + assert_eq!(result.resolved_paths, vec![PathBuf::new(), partial_stub_py]); + + Ok(()) + } + + #[test] + fn stub_in_typing_folder_over_partial_stub_package() -> io::Result<()> { + setup(); + + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + let typing_folder = root.join("typing"); + + let temp_dir = TempDir::new()?; + let library = temp_dir.path().join("lib").join("site-packages"); + + partial(library.join("myLib-stubs/py.typed"))?; + empty(library.join("myLib-stubs/__init__.pyi"))?; + let my_lib_pyi = empty(typing_folder.join("myLib.pyi"))?; + let my_lib_init_py = empty(library.join("myLib/__init__.py"))?; + + let result = resolve_options( + my_lib_init_py, + "myLib", + root, + ResolverOptions { + library: Some(library), + stub_path: Some(typing_folder), + ..Default::default() + }, + ); + + // If the package exists in typing folder, that gets picked up first (so we resolve to + // `myLib.pyi`). + assert!(result.is_import_found); + assert!(result.is_stub_file); + assert_eq!(result.resolved_paths, vec![my_lib_pyi]); + + Ok(()) + } + + #[test] + fn partial_stub_package_in_typing_folder() -> io::Result<()> { + setup(); + + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + let typing_folder = root.join("typing"); + + let temp_dir = TempDir::new()?; + let library = temp_dir.path().join("lib").join("site-packages"); + + partial(typing_folder.join("myLib-stubs/py.typed"))?; + let my_lib_stubs_init_pyi = empty(typing_folder.join("myLib-stubs/__init__.pyi"))?; + let my_lib_init_py = empty(library.join("myLib/__init__.py"))?; + + let result = resolve_options( + my_lib_init_py, + "myLib", + root, + ResolverOptions { + library: Some(library), + stub_path: Some(typing_folder), + ..Default::default() + }, + ); + + // If the package exists in typing folder, that gets picked up first (so we resolve to + // `myLib.pyi`). + assert!(result.is_import_found); + assert!(result.is_stub_file); + assert_eq!(result.resolved_paths, vec![my_lib_stubs_init_pyi]); + + Ok(()) + } + + #[test] + fn typeshed_folder() -> io::Result<()> { + setup(); + + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + let typeshed_folder = root.join("ts"); + + let temp_dir = TempDir::new()?; + let library = temp_dir.path().join("lib").join("site-packages"); + + empty(typeshed_folder.join("stubs/myLibPackage/myLib.pyi"))?; + partial(library.join("myLib-stubs/py.typed"))?; + let my_lib_stubs_init_pyi = empty(library.join("myLib-stubs/__init__.pyi"))?; + let my_lib_init_py = empty(library.join("myLib/__init__.py"))?; + + let result = resolve_options( + my_lib_init_py, + "myLib", + root, + ResolverOptions { + library: Some(library), + typeshed_path: Some(typeshed_folder), + ..Default::default() + }, + ); + + // Stub packages win over typeshed. + assert!(result.is_import_found); + assert!(result.is_stub_file); + assert_eq!(result.resolved_paths, vec![my_lib_stubs_init_pyi]); + + Ok(()) + } + + #[test] + fn py_typed_file() -> io::Result<()> { + setup(); + + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + + let temp_dir = TempDir::new()?; + let library = temp_dir.path().join("lib").join("site-packages"); + + empty(library.join("myLib/__init__.py"))?; + partial(library.join("myLib-stubs/py.typed"))?; + let partial_stub_init_pyi = empty(library.join("myLib-stubs/__init__.pyi"))?; + let package_py_typed = typed(library.join("myLib/py.typed"))?; + + let result = resolve_options( + package_py_typed, + "myLib", + root, + ResolverOptions { + library: Some(library), + ..Default::default() + }, + ); + + // Partial stub package always overrides original package. + assert!(result.is_import_found); + assert!(result.is_stub_file); + assert_eq!(result.resolved_paths, vec![partial_stub_init_pyi]); + + Ok(()) + } + + #[test] + fn py_typed_library() -> io::Result<()> { + setup(); + + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + let typeshed_folder = root.join("ts"); + + let temp_dir = TempDir::new()?; + let library = temp_dir.path().join("lib").join("site-packages"); + + typed(library.join("os/py.typed"))?; + let init_py = empty(library.join("os/__init__.py"))?; + let typeshed_init_pyi = empty(typeshed_folder.join("stubs/os/os/__init__.pyi"))?; + + let result = resolve_options( + typeshed_init_pyi, + "os", + root, + ResolverOptions { + library: Some(library), + typeshed_path: Some(typeshed_folder), + ..Default::default() + }, + ); + + assert!(result.is_import_found); + assert_eq!(result.resolved_paths, vec![init_py]); + + Ok(()) + } + + #[test] + fn non_py_typed_library() -> io::Result<()> { + setup(); + + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + let typeshed_folder = root.join("ts"); + + let temp_dir = TempDir::new()?; + let library = temp_dir.path().join("lib").join("site-packages"); + + empty(library.join("os/__init__.py"))?; + let typeshed_init_pyi = empty(typeshed_folder.join("stubs/os/os/__init__.pyi"))?; + + let result = resolve_options( + typeshed_init_pyi.clone(), + "os", + root, + ResolverOptions { + library: Some(library), + typeshed_path: Some(typeshed_folder), + ..Default::default() + }, + ); + + assert!(result.is_import_found); + assert_eq!(result.import_type, ImportType::ThirdParty); + assert_eq!(result.resolved_paths, vec![typeshed_init_pyi]); + + Ok(()) + } + + #[test] + fn import_side_by_side_file_root() -> io::Result<()> { + setup(); + + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + + let file1 = empty(root.join("file1.py"))?; + let file2 = empty(root.join("file2.py"))?; + + let result = resolve_options(file2, "file1", root, ResolverOptions::default()); + + assert!(result.is_import_found); + assert_eq!(result.import_type, ImportType::Local); + assert_eq!(result.resolved_paths, vec![file1]); + + Ok(()) + } + + #[test] + fn import_side_by_side_file_sub_folder() -> io::Result<()> { + setup(); + + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + + let test_init = empty(root.join("test/__init__.py"))?; + let test_file1 = empty(root.join("test/file1.py"))?; + let test_file2 = empty(root.join("test/file2.py"))?; + + let result = resolve_options(test_file2, "test.file1", root, ResolverOptions::default()); + + assert!(result.is_import_found); + assert_eq!(result.import_type, ImportType::Local); + assert_eq!(result.resolved_paths, vec![test_init, test_file1]); + + Ok(()) + } + + #[test] + fn import_side_by_side_file_sub_under_src_folder() -> io::Result<()> { + setup(); + + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + + let nested_init = empty(root.join("src/nested/__init__.py"))?; + let nested_file1 = empty(root.join("src/nested/file1.py"))?; + let nested_file2 = empty(root.join("src/nested/file2.py"))?; + + let result = resolve_options( + nested_file2, + "nested.file1", + root, + ResolverOptions::default(), + ); + + assert!(result.is_import_found); + assert_eq!(result.import_type, ImportType::Local); + assert_eq!(result.resolved_paths, vec![nested_init, nested_file1]); + + Ok(()) + } + + #[test] + fn import_file_sub_under_containing_folder() -> io::Result<()> { + setup(); + + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + + let nested_file1 = empty(root.join("src/nested/file1.py"))?; + let nested_file2 = empty(root.join("src/nested/nested2/file2.py"))?; + + let result = resolve_options(nested_file2, "file1", root, ResolverOptions::default()); + + assert!(result.is_import_found); + assert_eq!(result.import_type, ImportType::Local); + assert_eq!(result.resolved_paths, vec![nested_file1]); + + Ok(()) + } + + #[test] + fn import_side_by_side_file_sub_under_lib_folder() -> io::Result<()> { + setup(); + + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + + let temp_dir = TempDir::new()?; + let library = temp_dir.path().join("lib").join("site-packages"); + + empty(library.join("myLib/file1.py"))?; + let file2 = empty(library.join("myLib/file2.py"))?; + + let result = resolve_options(file2, "file1", root, ResolverOptions::default()); + + debug!("result: {:?}", result); + + assert!(!result.is_import_found); + + Ok(()) + } + + #[test] + fn nested_namespace_package_1() -> io::Result<()> { + // See: https://github.com/microsoft/pyright/issues/5089. + setup(); + + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + + let file = empty(root.join("package1/a/b/c/d.py"))?; + let package1_init = empty(root.join("package1/a/__init__.py"))?; + let package2_init = empty(root.join("package2/a/__init__.py"))?; + + let package1 = root.join("package1"); + let package2 = root.join("package2"); + + let result = resolve_options( + package2_init, + "a.b.c.d", + root, + ResolverOptions { + extra_paths: vec![package1, package2], + ..Default::default() + }, + ); + + assert!(result.is_import_found); + assert_eq!(result.import_type, ImportType::Local); + assert_eq!( + result.resolved_paths, + vec![package1_init, PathBuf::new(), PathBuf::new(), file] + ); + + Ok(()) + } + + #[test] + fn nested_namespace_package_2() -> io::Result<()> { + // See: https://github.com/microsoft/pyright/issues/5089. + setup(); + + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + + let file = empty(root.join("package1/a/b/c/d.py"))?; + let package1_init = empty(root.join("package1/a/b/c/__init__.py"))?; + let package2_init = empty(root.join("package2/a/b/c/__init__.py"))?; + + let package1 = root.join("package1"); + let package2 = root.join("package2"); + + let result = resolve_options( + package2_init, + "a.b.c.d", + root, + ResolverOptions { + extra_paths: vec![package1, package2], + ..Default::default() + }, + ); + + assert!(result.is_import_found); + assert_eq!(result.import_type, ImportType::Local); + assert_eq!( + result.resolved_paths, + vec![PathBuf::new(), PathBuf::new(), package1_init, file] + ); + + Ok(()) + } + + #[test] + fn nested_namespace_package_3() -> io::Result<()> { + // See: https://github.com/microsoft/pyright/issues/5089. + setup(); + + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + + empty(root.join("package1/a/b/c/d.py"))?; + let package2_init = empty(root.join("package2/a/__init__.py"))?; + + let package1 = root.join("package1"); + let package2 = root.join("package2"); + + let result = resolve_options( + package2_init, + "a.b.c.d", + root, + ResolverOptions { + extra_paths: vec![package1, package2], + ..Default::default() + }, + ); + + assert!(!result.is_import_found); + + Ok(()) + } + + #[test] + fn nested_namespace_package_4() -> io::Result<()> { + // See: https://github.com/microsoft/pyright/issues/5089. + setup(); + + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + + empty(root.join("package1/a/b/__init__.py"))?; + empty(root.join("package1/a/b/c.py"))?; + empty(root.join("package2/a/__init__.py"))?; + let package2_a_b_init = empty(root.join("package2/a/b/__init__.py"))?; + + let package1 = root.join("package1"); + let package2 = root.join("package2"); + + let result = resolve_options( + package2_a_b_init, + "a.b.c", + root, + ResolverOptions { + extra_paths: vec![package1, package2], + ..Default::default() + }, + ); + + assert!(!result.is_import_found); + + Ok(()) + } + + // New tests, don't exist upstream. + #[test] + fn relative_import_side_by_side_file_root() -> io::Result<()> { + setup(); + + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + + let file1 = empty(root.join("file1.py"))?; + let file2 = empty(root.join("file2.py"))?; + + let result = resolve_options(file2, ".file1", root, ResolverOptions::default()); + + assert!(result.is_import_found); + assert_eq!(result.import_type, ImportType::Local); + assert_eq!(result.resolved_paths, vec![file1]); + + Ok(()) + } + + #[test] + fn invalid_relative_import_side_by_side_file_root() -> io::Result<()> { + setup(); + + let temp_dir = TempDir::new()?; + let root = temp_dir.path(); + + empty(root.join("file1.py"))?; + let file2 = empty(root.join("file2.py"))?; + + let result = resolve_options(file2, "..file1", root, ResolverOptions::default()); + + assert!(!result.is_import_found); + + Ok(()) + } + + #[test] + fn airflow_standard_library() { + setup(); + + let root = PathBuf::from("./resources/test/airflow"); + let source_file = root.join("airflow/api/common/mark_tasks.py"); + + let result = resolve_options( + source_file, + "os", + root.clone(), + ResolverOptions { + venv_path: Some(root), + venv: Some(PathBuf::from("venv")), + ..Default::default() + }, + ); + + assert_debug_snapshot_normalize_paths!(result); + } + + #[test] + fn airflow_first_party() { + setup(); + + let root = PathBuf::from("./resources/test/airflow"); + let source_file = root.join("airflow/api/common/mark_tasks.py"); + + let result = resolve_options( + source_file, + "airflow.jobs.scheduler_job_runner", + root.clone(), + ResolverOptions { + venv_path: Some(root), + venv: Some(PathBuf::from("venv")), + ..Default::default() + }, + ); + + assert_debug_snapshot_normalize_paths!(result); + } + + #[test] + fn airflow_stub_file() { + setup(); + + let root = PathBuf::from("./resources/test/airflow"); + let source_file = root.join("airflow/api/common/mark_tasks.py"); + + let result = resolve_options( + source_file, + "airflow.compat.functools", + root.clone(), + ResolverOptions { + venv_path: Some(root), + venv: Some(PathBuf::from("venv")), + ..Default::default() + }, + ); + + assert_debug_snapshot_normalize_paths!(result); + } + + #[test] + fn airflow_namespace_package() { + setup(); + + let root = PathBuf::from("./resources/test/airflow"); + let source_file = root.join("airflow/api/common/mark_tasks.py"); + + let result = resolve_options( + source_file, + "airflow.providers.google.cloud.hooks.gcs", + root.clone(), + ResolverOptions { + venv_path: Some(root), + venv: Some(PathBuf::from("venv")), + ..Default::default() + }, + ); + + assert_debug_snapshot_normalize_paths!(result); + } + + #[test] + fn airflow_third_party() { + setup(); + + let root = PathBuf::from("./resources/test/airflow"); + let source_file = root.join("airflow/api/common/mark_tasks.py"); + + let result = resolve_options( + source_file, + "sqlalchemy.orm", + root.clone(), + ResolverOptions { + venv_path: Some(root), + venv: Some(PathBuf::from("venv")), + ..Default::default() + }, + ); + + assert_debug_snapshot_normalize_paths!(result); + } + + #[test] + fn airflow_explicit_native_module() { + setup(); + + let root = PathBuf::from("./resources/test/airflow"); + let source_file = root.join("airflow/api/common/mark_tasks.py"); + + let result = resolve_options( + source_file, + "_watchdog_fsevents", + root.clone(), + ResolverOptions { + venv_path: Some(root), + venv: Some(PathBuf::from("venv")), + ..Default::default() + }, + ); + + assert_debug_snapshot_normalize_paths!(result); + } + + #[test] + fn airflow_implicit_native_module() { + setup(); + + let root = PathBuf::from("./resources/test/airflow"); + let source_file = root.join("airflow/api/common/mark_tasks.py"); + + let result = resolve_options( + source_file, + "orjson", + root.clone(), + ResolverOptions { + venv_path: Some(root), + venv: Some(PathBuf::from("venv")), + ..Default::default() + }, + ); + + assert_debug_snapshot_normalize_paths!(result); + } +} diff --git a/crates/ruff_python_resolver/src/module_descriptor.rs b/crates/ruff_python_resolver/src/module_descriptor.rs new file mode 100644 index 0000000000..7d71efafbc --- /dev/null +++ b/crates/ruff_python_resolver/src/module_descriptor.rs @@ -0,0 +1,16 @@ +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct ImportModuleDescriptor { + pub(crate) leading_dots: usize, + pub(crate) name_parts: Vec, + pub(crate) imported_symbols: Vec, +} + +impl ImportModuleDescriptor { + pub(crate) fn name(&self) -> String { + format!( + "{}{}", + ".".repeat(self.leading_dots), + &self.name_parts.join(".") + ) + } +} diff --git a/crates/ruff_python_resolver/src/native_module.rs b/crates/ruff_python_resolver/src/native_module.rs new file mode 100644 index 0000000000..065473d6a5 --- /dev/null +++ b/crates/ruff_python_resolver/src/native_module.rs @@ -0,0 +1,87 @@ +//! Support for native Python extension modules. + +use std::ffi::OsStr; +use std::io; +use std::path::{Path, PathBuf}; + +/// Returns `true` if the given file extension is that of a native module. +pub(crate) fn is_native_module_file_extension(file_extension: &OsStr) -> bool { + file_extension == "so" || file_extension == "pyd" || file_extension == "dylib" +} + +/// Given a file name, returns the name of the native module it represents. +/// +/// For example, given `foo.abi3.so`, return `foo`. +pub(crate) fn native_module_name(file_name: &Path) -> Option<&str> { + file_name + .file_stem() + .and_then(OsStr::to_str) + .map(|file_stem| { + file_stem + .split_once('.') + .map_or(file_stem, |(file_stem, _)| file_stem) + }) +} + +/// Returns `true` if the given file name is that of a native module with the given name. +pub(crate) fn is_native_module_file_name(module_name: &str, file_name: &Path) -> bool { + // The file name must be that of a native module. + if !file_name + .extension() + .map_or(false, is_native_module_file_extension) + { + return false; + }; + + // The file must represent the module name. + native_module_name(file_name) == Some(module_name) +} + +/// Find the native module within the namespace package at the given path. +pub(crate) fn find_native_module( + module_name: &str, + dir_path: &Path, +) -> io::Result> { + Ok(dir_path + .read_dir()? + .flatten() + .filter(|entry| entry.file_type().map_or(false, |ft| ft.is_file())) + .map(|entry| entry.path()) + .find(|path| is_native_module_file_name(module_name, path))) +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + #[test] + fn module_name() { + assert_eq!( + super::native_module_name(&PathBuf::from("foo.so")), + Some("foo") + ); + + assert_eq!( + super::native_module_name(&PathBuf::from("foo.abi3.so")), + Some("foo") + ); + + assert_eq!( + super::native_module_name(&PathBuf::from("foo.cpython-38-x86_64-linux-gnu.so")), + Some("foo") + ); + + assert_eq!( + super::native_module_name(&PathBuf::from("foo.cp39-win_amd64.pyd")), + Some("foo") + ); + } + + #[test] + fn module_file_extension() { + assert!(super::is_native_module_file_extension("so".as_ref())); + assert!(super::is_native_module_file_extension("pyd".as_ref())); + assert!(super::is_native_module_file_extension("dylib".as_ref())); + assert!(!super::is_native_module_file_extension("py".as_ref())); + } +} diff --git a/crates/ruff_python_resolver/src/py_typed.rs b/crates/ruff_python_resolver/src/py_typed.rs new file mode 100644 index 0000000000..258f801eed --- /dev/null +++ b/crates/ruff_python_resolver/src/py_typed.rs @@ -0,0 +1,40 @@ +//! Support for [PEP 561] (`py.typed` files). +//! +//! [PEP 561]: https://peps.python.org/pep-0561/ + +use std::path::{Path, PathBuf}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct PyTypedInfo { + /// The path to the `py.typed` file. + py_typed_path: PathBuf, + + /// Whether the package is partially typed (as opposed to fully typed). + is_partially_typed: bool, +} + +/// Returns the `py.typed` information for the given directory, if any. +pub(crate) fn get_py_typed_info(dir_path: &Path) -> Option { + let py_typed_path = dir_path.join("py.typed"); + if py_typed_path.is_file() { + // Do a quick sanity check on the size before we attempt to read it. This + // file should always be really small - typically zero bytes in length. + let file_len = py_typed_path.metadata().ok()?.len(); + if file_len < 64 * 1024 { + // PEP 561 doesn't specify the format of "py.typed" in any detail other than + // to say that "If a stub package is partial it MUST include partial\n in a top + // level py.typed file." + let contents = std::fs::read_to_string(&py_typed_path).ok()?; + let is_partially_typed = + contents.contains("partial\n") || contents.contains("partial\r\n"); + Some(PyTypedInfo { + py_typed_path, + is_partially_typed, + }) + } else { + None + } + } else { + None + } +} diff --git a/crates/ruff_python_resolver/src/python_platform.rs b/crates/ruff_python_resolver/src/python_platform.rs new file mode 100644 index 0000000000..b82ebe256c --- /dev/null +++ b/crates/ruff_python_resolver/src/python_platform.rs @@ -0,0 +1,20 @@ +/// Enum to represent a Python platform. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub(crate) enum PythonPlatform { + Darwin, + Linux, + Windows, +} + +impl PythonPlatform { + /// Returns the platform-specific library names. These are the candidate names for the top-level + /// subdirectory within a virtual environment that contains the `site-packages` directory + /// (with a `pythonX.Y` directory in-between). + pub(crate) fn lib_names(&self) -> &[&'static str] { + match self { + PythonPlatform::Darwin => &["lib"], + PythonPlatform::Linux => &["lib", "lib64"], + PythonPlatform::Windows => &["Lib"], + } + } +} diff --git a/crates/ruff_python_resolver/src/python_version.rs b/crates/ruff_python_resolver/src/python_version.rs new file mode 100644 index 0000000000..aeb2a76b75 --- /dev/null +++ b/crates/ruff_python_resolver/src/python_version.rs @@ -0,0 +1,24 @@ +/// Enum to represent a Python version. +#[derive(Debug, Copy, Clone)] +pub(crate) enum PythonVersion { + Py37, + Py38, + Py39, + Py310, + Py311, + Py312, +} + +impl PythonVersion { + /// The directory name (e.g., in a virtual environment) for this Python version. + pub(crate) fn dir(self) -> &'static str { + match self { + PythonVersion::Py37 => "python3.7", + PythonVersion::Py38 => "python3.8", + PythonVersion::Py39 => "python3.9", + PythonVersion::Py310 => "python3.10", + PythonVersion::Py311 => "python3.11", + PythonVersion::Py312 => "python3.12", + } + } +} diff --git a/crates/ruff_python_resolver/src/resolver.rs b/crates/ruff_python_resolver/src/resolver.rs new file mode 100644 index 0000000000..86b2d5e5b8 --- /dev/null +++ b/crates/ruff_python_resolver/src/resolver.rs @@ -0,0 +1,752 @@ +//! Resolves Python imports to their corresponding files on disk. + +use std::ffi::OsStr; +use std::path::{Path, PathBuf}; + +use log::debug; + +use crate::config::Config; +use crate::execution_environment::ExecutionEnvironment; +use crate::implicit_imports::ImplicitImports; +use crate::import_result::{ImportResult, ImportType}; +use crate::module_descriptor::ImportModuleDescriptor; +use crate::{host, native_module, py_typed, search}; + +#[allow(clippy::fn_params_excessive_bools)] +fn resolve_module_descriptor( + root: &Path, + module_descriptor: &ImportModuleDescriptor, + allow_partial: bool, + allow_native_lib: bool, + use_stub_package: bool, + allow_pyi: bool, + look_for_py_typed: bool, +) -> ImportResult { + if use_stub_package { + debug!("Attempting to resolve stub package using root path: {root:?}"); + } else { + debug!("Attempting to resolve using root path: {root:?}"); + } + + // Starting at the specified path, walk the file system to find the specified module. + let mut resolved_paths: Vec = Vec::new(); + let mut dir_path = root.to_path_buf(); + let mut is_namespace_package = false; + let mut is_init_file_present = false; + let mut is_stub_package = false; + let mut is_stub_file = false; + let mut is_native_lib = false; + let mut implicit_imports = None; + let mut package_directory = None; + let mut py_typed_info = None; + + // Ex) `from . import foo` + if module_descriptor.name_parts.is_empty() { + let py_file_path = dir_path.join("__init__.py"); + let pyi_file_path = dir_path.join("__init__.pyi"); + + if allow_pyi && pyi_file_path.is_file() { + debug!("Resolved import with file: {pyi_file_path:?}"); + resolved_paths.push(pyi_file_path.clone()); + } else if py_file_path.is_file() { + debug!("Resolved import with file: {py_file_path:?}"); + resolved_paths.push(py_file_path.clone()); + } else { + debug!("Partially resolved import with directory: {dir_path:?}"); + + // Add an empty path to indicate that the import is partially resolved. + resolved_paths.push(PathBuf::new()); + is_namespace_package = true; + } + + implicit_imports = ImplicitImports::find(&dir_path, &[&py_file_path, &pyi_file_path]).ok(); + } else { + for (i, part) in module_descriptor.name_parts.iter().enumerate() { + let is_first_part = i == 0; + let is_last_part = i == module_descriptor.name_parts.len() - 1; + + // Extend the directory path with the next segment. + let module_dir_path = if use_stub_package && is_first_part { + is_stub_package = true; + dir_path.join(format!("{part}-stubs")) + } else { + dir_path.join(part) + }; + + let found_directory = module_dir_path.is_dir(); + if found_directory { + if is_first_part { + package_directory = Some(module_dir_path.clone()); + } + + // Look for an `__init__.py[i]` in the directory. + let py_file_path = module_dir_path.join("__init__.py"); + let pyi_file_path = module_dir_path.join("__init__.pyi"); + is_init_file_present = false; + + if allow_pyi && pyi_file_path.is_file() { + debug!("Resolved import with file: {pyi_file_path:?}"); + resolved_paths.push(pyi_file_path.clone()); + if is_last_part { + is_stub_file = true; + } + is_init_file_present = true; + } else if py_file_path.is_file() { + debug!("Resolved import with file: {py_file_path:?}"); + resolved_paths.push(py_file_path.clone()); + is_init_file_present = true; + } + + if look_for_py_typed { + py_typed_info = + py_typed_info.or_else(|| py_typed::get_py_typed_info(&module_dir_path)); + } + + // We haven't reached the end of the import, and we found a matching directory. + // Proceed to the next segment. + if !is_last_part { + if !is_init_file_present { + resolved_paths.push(PathBuf::new()); + is_namespace_package = true; + py_typed_info = None; + } + + dir_path = module_dir_path; + continue; + } + + if is_init_file_present { + implicit_imports = + ImplicitImports::find(&module_dir_path, &[&py_file_path, &pyi_file_path]) + .ok(); + break; + } + } + + // We couldn't find a matching directory, or the directory didn't contain an + // `__init__.py[i]` file. Look for an `.py[i]` file with the same name as the + // segment, in lieu of a directory. + let py_file_path = module_dir_path.with_extension("py"); + let pyi_file_path = module_dir_path.with_extension("pyi"); + + if allow_pyi && pyi_file_path.is_file() { + debug!("Resolved import with file: {pyi_file_path:?}"); + resolved_paths.push(pyi_file_path); + if is_last_part { + is_stub_file = true; + } + } else if py_file_path.is_file() { + debug!("Resolved import with file: {py_file_path:?}"); + resolved_paths.push(py_file_path); + } else { + if allow_native_lib && dir_path.is_dir() { + // We couldn't find a `.py[i]` file; search for a native library. + if let Some(module_name) = module_dir_path.file_name().and_then(OsStr::to_str) { + if let Ok(Some(native_lib_path)) = + native_module::find_native_module(module_name, &dir_path) + { + debug!("Resolved import with file: {native_lib_path:?}"); + is_native_lib = true; + resolved_paths.push(native_lib_path); + } + } + } + + if !is_native_lib && found_directory { + debug!("Partially resolved import with directory: {dir_path:?}"); + resolved_paths.push(PathBuf::new()); + if is_last_part { + implicit_imports = + ImplicitImports::find(&dir_path, &[&py_file_path, &pyi_file_path]).ok(); + is_namespace_package = true; + } + } + } + + break; + } + } + + let import_found = if allow_partial { + !resolved_paths.is_empty() + } else { + resolved_paths.len() == module_descriptor.name_parts.len() + }; + + let is_partly_resolved = if resolved_paths.is_empty() { + false + } else { + resolved_paths.len() < module_descriptor.name_parts.len() + }; + + ImportResult { + is_relative: false, + is_import_found: import_found, + is_partly_resolved, + is_namespace_package, + is_init_file_present, + is_stub_package, + import_type: ImportType::Local, + resolved_paths, + search_path: Some(root.into()), + is_stub_file, + is_native_lib, + is_stdlib_typeshed_file: false, + is_third_party_typeshed_file: false, + is_local_typings_file: false, + implicit_imports: implicit_imports.unwrap_or_default(), + filtered_implicit_imports: ImplicitImports::default(), + non_stub_import_result: None, + py_typed_info, + package_directory, + } +} + +/// Resolve an absolute module import based on the import resolution algorithm +/// defined in [PEP 420]. +/// +/// [PEP 420]: https://peps.python.org/pep-0420/ +#[allow(clippy::fn_params_excessive_bools)] +fn resolve_absolute_import( + root: &Path, + module_descriptor: &ImportModuleDescriptor, + allow_partial: bool, + allow_native_lib: bool, + use_stub_package: bool, + allow_pyi: bool, + look_for_py_typed: bool, +) -> ImportResult { + if allow_pyi && use_stub_package { + // Search for packaged stubs first. PEP 561 indicates that package authors can ship + // stubs separately from the package implementation by appending `-stubs` to its + // top-level directory name. + let import_result = resolve_module_descriptor( + root, + module_descriptor, + allow_partial, + false, + true, + true, + true, + ); + + if import_result.package_directory.is_some() { + // If this is a namespace package that wasn't resolved, assume that + // it's a partial stub package and continue looking for a real package. + if !import_result.is_namespace_package || import_result.is_import_found { + return import_result; + } + } + } + + // Search for a "real" package. + resolve_module_descriptor( + root, + module_descriptor, + allow_partial, + allow_native_lib, + false, + allow_pyi, + look_for_py_typed, + ) +} + +/// Resolve an absolute module import based on the import resolution algorithm, +/// taking into account the various competing files to which the import could +/// resolve. +/// +/// For example, prefers local imports over third-party imports, and stubs over +/// non-stubs. +fn resolve_best_absolute_import( + execution_environment: &ExecutionEnvironment, + module_descriptor: &ImportModuleDescriptor, + allow_pyi: bool, + config: &Config, + host: &Host, +) -> Option { + let import_name = module_descriptor.name(); + + // Search for local stub files (using `stub_path`). + if allow_pyi { + if let Some(stub_path) = config.stub_path.as_ref() { + debug!("Looking in stub path: {}", stub_path.display()); + + let mut typings_import = resolve_absolute_import( + stub_path, + module_descriptor, + false, + false, + true, + allow_pyi, + false, + ); + + if typings_import.is_import_found { + // Treat stub files as "local". + typings_import.import_type = ImportType::Local; + typings_import.is_local_typings_file = true; + + // If we resolved to a namespace package, ensure that all imported symbols are + // present in the namespace package's "implicit" imports. + if typings_import.is_namespace_package + && typings_import + .resolved_paths + .last() + .map_or(false, |path| path.as_os_str().is_empty()) + { + if typings_import + .implicit_imports + .resolves_namespace_package(&module_descriptor.imported_symbols) + { + return Some(typings_import); + } + } else { + return Some(typings_import); + } + } + + return None; + } + } + + // Look in the root directory of the execution environment. + debug!( + "Looking in root directory of execution environment: {}", + execution_environment.root.display() + ); + + let mut local_import = resolve_absolute_import( + &execution_environment.root, + module_descriptor, + false, + true, + true, + allow_pyi, + false, + ); + local_import.import_type = ImportType::Local; + + let mut best_result_so_far = Some(local_import); + + // Look in any extra paths. + for extra_path in &execution_environment.extra_paths { + debug!("Looking in extra path: {}", extra_path.display()); + + let mut local_import = resolve_absolute_import( + extra_path, + module_descriptor, + false, + true, + true, + allow_pyi, + false, + ); + local_import.import_type = ImportType::Local; + + best_result_so_far = Some(pick_best_import( + best_result_so_far, + local_import, + module_descriptor, + )); + } + + // Look for third-party imports in Python's `sys` path. + for search_path in search::python_search_paths(config, host) { + debug!("Looking in Python search path: {}", search_path.display()); + + let mut third_party_import = resolve_absolute_import( + &search_path, + module_descriptor, + false, + true, + true, + allow_pyi, + true, + ); + third_party_import.import_type = ImportType::ThirdParty; + + best_result_so_far = Some(pick_best_import( + best_result_so_far, + third_party_import, + module_descriptor, + )); + } + + // If a library is fully `py.typed`, prefer the current result. There's one exception: + // we're executing from `typeshed` itself. In that case, use the `typeshed` lookup below, + // rather than favoring `py.typed` libraries. + if let Some(typeshed_root) = search::typeshed_root(config, host) { + debug!( + "Looking in typeshed root directory: {}", + typeshed_root.display() + ); + if typeshed_root != execution_environment.root { + if best_result_so_far.as_ref().map_or(false, |result| { + result.py_typed_info.is_some() && !result.is_partly_resolved + }) { + return best_result_so_far; + } + } + } + + if allow_pyi && !module_descriptor.name_parts.is_empty() { + // Check for a stdlib typeshed file. + debug!("Looking for typeshed stdlib path: {}", import_name); + if let Some(mut typeshed_stdilib_import) = + find_typeshed_path(module_descriptor, true, config, host) + { + typeshed_stdilib_import.is_stdlib_typeshed_file = true; + return Some(typeshed_stdilib_import); + } + + // Check for a third-party typeshed file. + debug!("Looking for typeshed third-party path: {}", import_name); + if let Some(mut typeshed_third_party_import) = + find_typeshed_path(module_descriptor, false, config, host) + { + typeshed_third_party_import.is_third_party_typeshed_file = true; + + best_result_so_far = Some(pick_best_import( + best_result_so_far, + typeshed_third_party_import, + module_descriptor, + )); + } + } + + // We weren't able to find an exact match, so return the best + // partial match. + best_result_so_far +} + +/// Finds the `typeshed` path for the given module descriptor. +/// +/// Supports both standard library and third-party `typeshed` lookups. +fn find_typeshed_path( + module_descriptor: &ImportModuleDescriptor, + is_std_lib: bool, + config: &Config, + host: &Host, +) -> Option { + if is_std_lib { + debug!("Looking for typeshed `stdlib` path"); + } else { + debug!("Looking for typeshed `stubs` path"); + } + + let mut typeshed_paths = vec![]; + + if is_std_lib { + if let Some(path) = search::stdlib_typeshed_path(config, host) { + typeshed_paths.push(path); + } + } else { + if let Some(paths) = + search::third_party_typeshed_package_paths(module_descriptor, config, host) + { + typeshed_paths.extend(paths); + } + } + + for typeshed_path in typeshed_paths { + if typeshed_path.is_dir() { + let mut import_info = resolve_absolute_import( + &typeshed_path, + module_descriptor, + false, + false, + false, + true, + false, + ); + if import_info.is_import_found { + import_info.import_type = if is_std_lib { + ImportType::BuiltIn + } else { + ImportType::ThirdParty + }; + return Some(import_info); + } + } + } + + debug!("Typeshed path not found"); + None +} + +/// Given a current "best" import and a newly discovered result, returns the +/// preferred result. +fn pick_best_import( + best_import_so_far: Option, + new_import: ImportResult, + module_descriptor: &ImportModuleDescriptor, +) -> ImportResult { + let Some(best_import_so_far) = best_import_so_far else { + return new_import; + }; + + if new_import.is_import_found { + // Prefer traditional over namespace packages. + let so_far_index = best_import_so_far + .resolved_paths + .iter() + .position(|path| !path.as_os_str().is_empty()); + let new_index = new_import + .resolved_paths + .iter() + .position(|path| !path.as_os_str().is_empty()); + if so_far_index != new_index { + match (so_far_index, new_index) { + (None, Some(_)) => return new_import, + (Some(_), None) => return best_import_so_far, + (Some(so_far_index), Some(new_index)) => { + return if so_far_index < new_index { + best_import_so_far + } else { + new_import + } + } + _ => {} + } + } + + // Prefer "found" over "not found". + if !best_import_so_far.is_import_found { + return new_import; + } + + // If both results are namespace imports, prefer the result that resolves all + // imported symbols. + if best_import_so_far.is_namespace_package && new_import.is_namespace_package { + if !module_descriptor.imported_symbols.is_empty() { + if !best_import_so_far + .implicit_imports + .resolves_namespace_package(&module_descriptor.imported_symbols) + { + if new_import + .implicit_imports + .resolves_namespace_package(&module_descriptor.imported_symbols) + { + return new_import; + } + + // Prefer the namespace package that has an `__init__.py[i]` file present in the + // final directory over one that does not. + if best_import_so_far.is_init_file_present && !new_import.is_init_file_present { + return best_import_so_far; + } + if !best_import_so_far.is_init_file_present && new_import.is_init_file_present { + return new_import; + } + } + } + } + + // Prefer "py.typed" over "non-py.typed". + if best_import_so_far.py_typed_info.is_some() && new_import.py_typed_info.is_none() { + return best_import_so_far; + } + if best_import_so_far.py_typed_info.is_none() && best_import_so_far.py_typed_info.is_some() + { + return new_import; + } + + // Prefer stub files (`.pyi`) over non-stub files (`.py`). + if best_import_so_far.is_stub_file && !new_import.is_stub_file { + return best_import_so_far; + } + if !best_import_so_far.is_stub_file && new_import.is_stub_file { + return new_import; + } + + // If we're still tied, prefer a shorter resolution path. + if best_import_so_far.resolved_paths.len() > new_import.resolved_paths.len() { + return new_import; + } + } else if new_import.is_partly_resolved { + let so_far_index = best_import_so_far + .resolved_paths + .iter() + .position(|path| !path.as_os_str().is_empty()); + let new_index = new_import + .resolved_paths + .iter() + .position(|path| !path.as_os_str().is_empty()); + if so_far_index != new_index { + match (so_far_index, new_index) { + (None, Some(_)) => return new_import, + (Some(_), None) => return best_import_so_far, + (Some(so_far_index), Some(new_index)) => { + return if so_far_index < new_index { + best_import_so_far + } else { + new_import + } + } + _ => {} + } + } + } + + best_import_so_far +} + +/// Resolve a relative import. +fn resolve_relative_import( + source_file: &Path, + module_descriptor: &ImportModuleDescriptor, +) -> Option { + // Determine which search path this file is part of. + let mut directory = source_file; + for _ in 0..module_descriptor.leading_dots { + directory = directory.parent()?; + } + + // Now try to match the module parts from the current directory location. + let mut abs_import = resolve_absolute_import( + directory, + module_descriptor, + false, + true, + false, + true, + false, + ); + + if abs_import.is_stub_file { + // If we found a stub for a relative import, only search + // the same folder for the real module. Otherwise, it will + // error out on runtime. + abs_import.non_stub_import_result = Some(Box::new(resolve_absolute_import( + directory, + module_descriptor, + false, + true, + false, + false, + false, + ))); + } + + Some(abs_import) +} + +/// Resolve an absolute or relative import. +fn resolve_import_strict( + source_file: &Path, + execution_environment: &ExecutionEnvironment, + module_descriptor: &ImportModuleDescriptor, + config: &Config, + host: &Host, +) -> ImportResult { + let import_name = module_descriptor.name(); + + if module_descriptor.leading_dots > 0 { + debug!("Resolving relative import for: {import_name}"); + + let relative_import = resolve_relative_import(source_file, module_descriptor); + + if let Some(mut relative_import) = relative_import { + relative_import.is_relative = true; + return relative_import; + } + } else { + debug!("Resolving best absolute import for: {import_name}"); + + let best_import = resolve_best_absolute_import( + execution_environment, + module_descriptor, + true, + config, + host, + ); + + if let Some(mut best_import) = best_import { + if best_import.is_stub_file { + debug!("Resolving best non-stub absolute import for: {import_name}"); + + best_import.non_stub_import_result = Some(Box::new( + resolve_best_absolute_import( + execution_environment, + module_descriptor, + false, + config, + host, + ) + .unwrap_or_else(ImportResult::not_found), + )); + } + return best_import; + } + } + + ImportResult::not_found() +} + +/// Resolves an import, given the current file and the import descriptor. +/// +/// The algorithm is as follows: +/// +/// 1. If the import is relative, convert it to an absolute import. +/// 2. Find the "best" match for the import, allowing stub files. Search local imports, any +/// configured search paths, the Python path, the typeshed path, etc. +/// 3. If a stub file was found, find the "best" match for the import, disallowing stub files. +/// 4. If the import wasn't resolved, try to resolve it in the parent directory, then the parent's +/// parent, and so on, until the import root is reached. +pub(crate) fn resolve_import( + source_file: &Path, + execution_environment: &ExecutionEnvironment, + module_descriptor: &ImportModuleDescriptor, + config: &Config, + host: &Host, +) -> ImportResult { + let import_result = resolve_import_strict( + source_file, + execution_environment, + module_descriptor, + config, + host, + ); + if import_result.is_import_found || module_descriptor.leading_dots > 0 { + return import_result; + } + + // If we weren't able to resolve an absolute import, try resolving it in the + // importing file's directory, then the parent directory, and so on, until the + // import root is reached. + let root = execution_environment.root.as_path(); + if source_file.starts_with(root) { + let mut current = source_file; + while let Some(parent) = current.parent() { + if parent == root { + break; + } + + debug!("Resolving absolute import in parent: {}", parent.display()); + + let mut result = resolve_absolute_import( + parent, + module_descriptor, + false, + false, + false, + true, + false, + ); + + if result.is_import_found { + if let Some(implicit_imports) = result + .implicit_imports + .filter(&module_descriptor.imported_symbols) + { + result.implicit_imports = implicit_imports; + } + return result; + } + + current = parent; + } + } + + ImportResult::not_found() +} diff --git a/crates/ruff_python_resolver/src/search.rs b/crates/ruff_python_resolver/src/search.rs new file mode 100644 index 0000000000..0539c8296a --- /dev/null +++ b/crates/ruff_python_resolver/src/search.rs @@ -0,0 +1,278 @@ +//! Determine the appropriate search paths for the Python environment. + +use std::collections::HashMap; +use std::ffi::OsStr; +use std::path::{Path, PathBuf}; +use std::{fs, io}; + +use log::debug; + +use crate::config::Config; +use crate::host; +use crate::module_descriptor::ImportModuleDescriptor; +use crate::python_version::PythonVersion; + +const SITE_PACKAGES: &str = "site-packages"; + +/// Find the `site-packages` directory for the specified Python version. +fn find_site_packages_path( + lib_path: &Path, + python_version: Option, +) -> Option { + if lib_path.is_dir() { + debug!( + "Found path `{}`; looking for site-packages", + lib_path.display() + ); + } else { + debug!("Did not find `{}`", lib_path.display()); + } + + let site_packages_path = lib_path.join(SITE_PACKAGES); + if site_packages_path.is_dir() { + debug!("Found path `{}`", site_packages_path.display()); + return Some(site_packages_path); + } + + debug!( + "Did not find `{}`, so looking for Python subdirectory", + site_packages_path.display() + ); + + // There's no `site-packages` directory in the library directory; look for a `python3.X` + // directory instead. + let candidate_dirs: Vec = fs::read_dir(lib_path) + .ok()? + .filter_map(|entry| { + let entry = entry.ok()?; + let metadata = entry.metadata().ok()?; + + if metadata.file_type().is_dir() { + let dir_path = entry.path(); + if dir_path + .file_name() + .and_then(OsStr::to_str)? + .starts_with("python3.") + { + if dir_path.join(SITE_PACKAGES).is_dir() { + return Some(dir_path); + } + } + } else if metadata.file_type().is_symlink() { + let symlink_path = fs::read_link(entry.path()).ok()?; + if symlink_path + .file_name() + .and_then(OsStr::to_str)? + .starts_with("python3.") + { + if symlink_path.join(SITE_PACKAGES).is_dir() { + return Some(symlink_path); + } + } + } + + None + }) + .collect(); + + // If a `python3.X` directory does exist (and `3.X` matches the current Python version), + // prefer it over any other Python directories. + if let Some(python_version) = python_version { + if let Some(preferred_dir) = candidate_dirs.iter().find(|dir| { + dir.file_name() + .and_then(OsStr::to_str) + .map_or(false, |name| name == python_version.dir()) + }) { + debug!("Found path `{}`", preferred_dir.display()); + return Some(preferred_dir.join(SITE_PACKAGES)); + } + } + + // Fallback to the first `python3.X` directory that we found. + let default_dir = candidate_dirs.first()?; + debug!("Found path `{}`", default_dir.display()); + Some(default_dir.join(SITE_PACKAGES)) +} + +fn find_paths_from_pth_files(parent_dir: &Path) -> io::Result + '_> { + Ok(parent_dir + .read_dir()? + .flatten() + .filter(|entry| { + // Collect all *.pth files. + let Ok(file_type) = entry.file_type() else { + return false; + }; + file_type.is_file() || file_type.is_symlink() + }) + .map(|entry| entry.path()) + .filter(|path| path.extension() == Some(OsStr::new("pth"))) + .filter(|path| { + // Skip all files that are much larger than expected. + let Ok(metadata) = path.metadata() else { + return false; + }; + let file_len = metadata.len(); + file_len > 0 && file_len < 64 * 1024 + }) + .filter_map(|path| { + let data = fs::read_to_string(&path).ok()?; + for line in data.lines() { + let trimmed_line = line.trim(); + if !trimmed_line.is_empty() + && !trimmed_line.starts_with('#') + && !trimmed_line.starts_with("import") + { + let pth_path = parent_dir.join(trimmed_line); + if pth_path.is_dir() { + return Some(pth_path); + } + } + } + None + })) +} + +/// Find the Python search paths for the given virtual environment. +fn find_python_search_paths(config: &Config, host: &Host) -> Vec { + if let Some(venv_path) = config.venv_path.as_ref() { + if let Some(venv) = config.venv.as_ref() { + let mut found_paths = vec![]; + + for lib_name in host.python_platform().lib_names() { + let lib_path = venv_path.join(venv).join(lib_name); + if let Some(site_packages_path) = find_site_packages_path(&lib_path, None) { + // Add paths from any `.pth` files in each of the `site-packages` directories. + if let Ok(pth_paths) = find_paths_from_pth_files(&site_packages_path) { + found_paths.extend(pth_paths); + } + + // Add the `site-packages` directory to the search path. + found_paths.push(site_packages_path); + } + } + + if !found_paths.is_empty() { + found_paths.sort(); + found_paths.dedup(); + + debug!("Found the following `site-packages` dirs"); + for path in &found_paths { + debug!(" {}", path.display()); + } + + return found_paths; + } + } + } + + // Fall back to the Python interpreter. + host.python_search_paths() +} + +/// Determine the relevant Python search paths. +pub(crate) fn python_search_paths(config: &Config, host: &Host) -> Vec { + // TODO(charlie): Cache search paths. + find_python_search_paths(config, host) +} + +/// Determine the root of the `typeshed` directory. +pub(crate) fn typeshed_root(config: &Config, host: &Host) -> Option { + if let Some(typeshed_path) = config.typeshed_path.as_ref() { + // Did the user specify a typeshed path? + if typeshed_path.is_dir() { + return Some(typeshed_path.clone()); + } + } else { + // If not, we'll look in the Python search paths. + for python_search_path in python_search_paths(config, host) { + let possible_typeshed_path = python_search_path.join("typeshed"); + if possible_typeshed_path.is_dir() { + return Some(possible_typeshed_path); + } + } + } + + None +} + +/// Determine the current `typeshed` subdirectory. +fn typeshed_subdirectory( + is_stdlib: bool, + config: &Config, + host: &Host, +) -> Option { + let typeshed_path = + typeshed_root(config, host)?.join(if is_stdlib { "stdlib" } else { "stubs" }); + if typeshed_path.is_dir() { + Some(typeshed_path) + } else { + None + } +} + +/// Generate a map from PyPI-registered package name to a list of paths +/// containing the package's stubs. +fn build_typeshed_third_party_package_map( + third_party_dir: &Path, +) -> io::Result>> { + let mut package_map = HashMap::new(); + + // Iterate over every directory. + for outer_entry in fs::read_dir(third_party_dir)? { + let outer_entry = outer_entry?; + if outer_entry.file_type()?.is_dir() { + // Iterate over any subdirectory children. + for inner_entry in fs::read_dir(outer_entry.path())? { + let inner_entry = inner_entry?; + + if inner_entry.file_type()?.is_dir() { + package_map + .entry(inner_entry.file_name().to_string_lossy().to_string()) + .or_insert_with(Vec::new) + .push(outer_entry.path()); + } else if inner_entry.file_type()?.is_file() { + if inner_entry + .path() + .extension() + .map_or(false, |extension| extension == "pyi") + { + if let Some(stripped_file_name) = inner_entry + .path() + .file_stem() + .and_then(std::ffi::OsStr::to_str) + .map(std::string::ToString::to_string) + { + package_map + .entry(stripped_file_name) + .or_insert_with(Vec::new) + .push(outer_entry.path()); + } + } + } + } + } + } + + Ok(package_map) +} + +/// Determine the current `typeshed` subdirectory for a third-party package. +pub(crate) fn third_party_typeshed_package_paths( + module_descriptor: &ImportModuleDescriptor, + config: &Config, + host: &Host, +) -> Option> { + let typeshed_path = typeshed_subdirectory(false, config, host)?; + let package_paths = build_typeshed_third_party_package_map(&typeshed_path).ok()?; + let first_name_part = module_descriptor.name_parts.first().map(String::as_str)?; + package_paths.get(first_name_part).cloned() +} + +/// Determine the current `typeshed` subdirectory for the standard library. +pub(crate) fn stdlib_typeshed_path( + config: &Config, + host: &Host, +) -> Option { + typeshed_subdirectory(true, config, host) +} diff --git a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_explicit_native_module.snap b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_explicit_native_module.snap new file mode 100644 index 0000000000..69186a0145 --- /dev/null +++ b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_explicit_native_module.snap @@ -0,0 +1,33 @@ +--- +source: crates/ruff_python_resolver/src/lib.rs +expression: result +--- +ImportResult { + is_relative: false, + is_import_found: true, + is_partly_resolved: false, + is_namespace_package: false, + is_init_file_present: false, + is_stub_package: false, + import_type: ThirdParty, + resolved_paths: [ + "./resources/test/airflow/venv/lib/python3.11/site-packages/_watchdog_fsevents.cpython-311-darwin.so", + ], + search_path: Some( + "./resources/test/airflow/venv/lib/python3.11/site-packages", + ), + is_stub_file: false, + is_native_lib: true, + is_stdlib_typeshed_file: false, + is_third_party_typeshed_file: false, + is_local_typings_file: false, + implicit_imports: ImplicitImports( + {}, + ), + filtered_implicit_imports: ImplicitImports( + {}, + ), + non_stub_import_result: None, + py_typed_info: None, + package_directory: None, +} diff --git a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_first_party.snap b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_first_party.snap new file mode 100644 index 0000000000..47e0ef7c88 --- /dev/null +++ b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_first_party.snap @@ -0,0 +1,37 @@ +--- +source: crates/ruff_python_resolver/src/lib.rs +expression: result +--- +ImportResult { + is_relative: false, + is_import_found: true, + is_partly_resolved: false, + is_namespace_package: false, + is_init_file_present: true, + is_stub_package: false, + import_type: Local, + resolved_paths: [ + "./resources/test/airflow/airflow/__init__.py", + "./resources/test/airflow/airflow/jobs/__init__.py", + "./resources/test/airflow/airflow/jobs/scheduler_job_runner.py", + ], + search_path: Some( + "./resources/test/airflow", + ), + is_stub_file: false, + is_native_lib: false, + is_stdlib_typeshed_file: false, + is_third_party_typeshed_file: false, + is_local_typings_file: false, + implicit_imports: ImplicitImports( + {}, + ), + filtered_implicit_imports: ImplicitImports( + {}, + ), + non_stub_import_result: None, + py_typed_info: None, + package_directory: Some( + "./resources/test/airflow/airflow", + ), +} diff --git a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_implicit_native_module.snap b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_implicit_native_module.snap new file mode 100644 index 0000000000..262aa50fc1 --- /dev/null +++ b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_implicit_native_module.snap @@ -0,0 +1,91 @@ +--- +source: crates/ruff_python_resolver/src/lib.rs +expression: result +--- +ImportResult { + is_relative: false, + is_import_found: true, + is_partly_resolved: false, + is_namespace_package: false, + is_init_file_present: true, + is_stub_package: false, + import_type: ThirdParty, + resolved_paths: [ + "./resources/test/airflow/venv/lib/python3.11/site-packages/orjson/__init__.pyi", + ], + search_path: Some( + "./resources/test/airflow/venv/lib/python3.11/site-packages", + ), + is_stub_file: true, + is_native_lib: false, + is_stdlib_typeshed_file: false, + is_third_party_typeshed_file: false, + is_local_typings_file: false, + implicit_imports: ImplicitImports( + { + "orjson": ImplicitImport { + is_stub_file: false, + is_native_lib: true, + path: "./resources/test/airflow/venv/lib/python3.11/site-packages/orjson/orjson.cpython-311-darwin.so", + py_typed: None, + }, + }, + ), + filtered_implicit_imports: ImplicitImports( + {}, + ), + non_stub_import_result: Some( + ImportResult { + is_relative: false, + is_import_found: true, + is_partly_resolved: false, + is_namespace_package: false, + is_init_file_present: true, + is_stub_package: false, + import_type: ThirdParty, + resolved_paths: [ + "./resources/test/airflow/venv/lib/python3.11/site-packages/orjson/__init__.py", + ], + search_path: Some( + "./resources/test/airflow/venv/lib/python3.11/site-packages", + ), + is_stub_file: false, + is_native_lib: false, + is_stdlib_typeshed_file: false, + is_third_party_typeshed_file: false, + is_local_typings_file: false, + implicit_imports: ImplicitImports( + { + "orjson": ImplicitImport { + is_stub_file: false, + is_native_lib: true, + path: "./resources/test/airflow/venv/lib/python3.11/site-packages/orjson/orjson.cpython-311-darwin.so", + py_typed: None, + }, + }, + ), + filtered_implicit_imports: ImplicitImports( + {}, + ), + non_stub_import_result: None, + py_typed_info: Some( + PyTypedInfo { + py_typed_path: "./resources/test/airflow/venv/lib/python3.11/site-packages/orjson/py.typed", + is_partially_typed: false, + }, + ), + package_directory: Some( + "./resources/test/airflow/venv/lib/python3.11/site-packages/orjson", + ), + }, + ), + py_typed_info: Some( + PyTypedInfo { + py_typed_path: "./resources/test/airflow/venv/lib/python3.11/site-packages/orjson/py.typed", + is_partially_typed: false, + }, + ), + package_directory: Some( + "./resources/test/airflow/venv/lib/python3.11/site-packages/orjson", + ), +} diff --git a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_namespace_package.snap b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_namespace_package.snap new file mode 100644 index 0000000000..1c9132b9bf --- /dev/null +++ b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_namespace_package.snap @@ -0,0 +1,40 @@ +--- +source: crates/ruff_python_resolver/src/lib.rs +expression: result +--- +ImportResult { + is_relative: false, + is_import_found: true, + is_partly_resolved: false, + is_namespace_package: true, + is_init_file_present: true, + is_stub_package: false, + import_type: Local, + resolved_paths: [ + "./resources/test/airflow/airflow/__init__.py", + "", + "./resources/test/airflow/airflow/providers/google/__init__.py", + "./resources/test/airflow/airflow/providers/google/cloud/__init__.py", + "./resources/test/airflow/airflow/providers/google/cloud/hooks/__init__.py", + "./resources/test/airflow/airflow/providers/google/cloud/hooks/gcs.py", + ], + search_path: Some( + "./resources/test/airflow", + ), + is_stub_file: false, + is_native_lib: false, + is_stdlib_typeshed_file: false, + is_third_party_typeshed_file: false, + is_local_typings_file: false, + implicit_imports: ImplicitImports( + {}, + ), + filtered_implicit_imports: ImplicitImports( + {}, + ), + non_stub_import_result: None, + py_typed_info: None, + package_directory: Some( + "./resources/test/airflow/airflow", + ), +} diff --git a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_standard_library.snap b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_standard_library.snap new file mode 100644 index 0000000000..47299f3218 --- /dev/null +++ b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_standard_library.snap @@ -0,0 +1,29 @@ +--- +source: crates/ruff_python_resolver/src/lib.rs +expression: result +--- +ImportResult { + is_relative: false, + is_import_found: false, + is_partly_resolved: false, + is_namespace_package: false, + is_init_file_present: false, + is_stub_package: false, + import_type: Local, + resolved_paths: [], + search_path: None, + is_stub_file: false, + is_native_lib: false, + is_stdlib_typeshed_file: false, + is_third_party_typeshed_file: false, + is_local_typings_file: false, + implicit_imports: ImplicitImports( + {}, + ), + filtered_implicit_imports: ImplicitImports( + {}, + ), + non_stub_import_result: None, + py_typed_info: None, + package_directory: None, +} diff --git a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_stub_file.snap b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_stub_file.snap new file mode 100644 index 0000000000..92d74bcb20 --- /dev/null +++ b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_stub_file.snap @@ -0,0 +1,71 @@ +--- +source: crates/ruff_python_resolver/src/lib.rs +expression: result +--- +ImportResult { + is_relative: false, + is_import_found: true, + is_partly_resolved: false, + is_namespace_package: false, + is_init_file_present: true, + is_stub_package: false, + import_type: Local, + resolved_paths: [ + "./resources/test/airflow/airflow/__init__.py", + "./resources/test/airflow/airflow/compat/__init__.py", + "./resources/test/airflow/airflow/compat/functools.pyi", + ], + search_path: Some( + "./resources/test/airflow", + ), + is_stub_file: true, + is_native_lib: false, + is_stdlib_typeshed_file: false, + is_third_party_typeshed_file: false, + is_local_typings_file: false, + implicit_imports: ImplicitImports( + {}, + ), + filtered_implicit_imports: ImplicitImports( + {}, + ), + non_stub_import_result: Some( + ImportResult { + is_relative: false, + is_import_found: true, + is_partly_resolved: false, + is_namespace_package: false, + is_init_file_present: true, + is_stub_package: false, + import_type: Local, + resolved_paths: [ + "./resources/test/airflow/airflow/__init__.py", + "./resources/test/airflow/airflow/compat/__init__.py", + "./resources/test/airflow/airflow/compat/functools.py", + ], + search_path: Some( + "./resources/test/airflow", + ), + is_stub_file: false, + is_native_lib: false, + is_stdlib_typeshed_file: false, + is_third_party_typeshed_file: false, + is_local_typings_file: false, + implicit_imports: ImplicitImports( + {}, + ), + filtered_implicit_imports: ImplicitImports( + {}, + ), + non_stub_import_result: None, + py_typed_info: None, + package_directory: Some( + "./resources/test/airflow/airflow", + ), + }, + ), + py_typed_info: None, + package_directory: Some( + "./resources/test/airflow/airflow", + ), +} diff --git a/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_third_party.snap b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_third_party.snap new file mode 100644 index 0000000000..138f4f71ca --- /dev/null +++ b/crates/ruff_python_resolver/src/snapshots/ruff_python_resolver__tests__airflow_third_party.snap @@ -0,0 +1,55 @@ +--- +source: crates/ruff_python_resolver/src/lib.rs +expression: result +--- +ImportResult { + is_relative: false, + is_import_found: true, + is_partly_resolved: false, + is_namespace_package: false, + is_init_file_present: true, + is_stub_package: false, + import_type: ThirdParty, + resolved_paths: [ + "./resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/__init__.py", + "./resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/__init__.py", + ], + search_path: Some( + "./resources/test/airflow/venv/lib/python3.11/site-packages", + ), + is_stub_file: false, + is_native_lib: false, + is_stdlib_typeshed_file: false, + is_third_party_typeshed_file: false, + is_local_typings_file: false, + implicit_imports: ImplicitImports( + { + "base": ImplicitImport { + is_stub_file: false, + is_native_lib: false, + path: "./resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/base.py", + py_typed: None, + }, + "dependency": ImplicitImport { + is_stub_file: false, + is_native_lib: false, + path: "./resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/dependency.py", + py_typed: None, + }, + "query": ImplicitImport { + is_stub_file: false, + is_native_lib: false, + path: "./resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy/orm/query.py", + py_typed: None, + }, + }, + ), + filtered_implicit_imports: ImplicitImports( + {}, + ), + non_stub_import_result: None, + py_typed_info: None, + package_directory: Some( + "./resources/test/airflow/venv/lib/python3.11/site-packages/sqlalchemy", + ), +} diff --git a/crates/ruff_python_semantic/Cargo.toml b/crates/ruff_python_semantic/Cargo.toml index 3716d2c103..93c47dae9d 100644 --- a/crates/ruff_python_semantic/Cargo.toml +++ b/crates/ruff_python_semantic/Cargo.toml @@ -2,8 +2,13 @@ name = "ruff_python_semantic" version = "0.0.0" publish = false +authors = { workspace = true } edition = { workspace = true } rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } [lib] diff --git a/crates/ruff_python_semantic/src/analyze/branch_detection.rs b/crates/ruff_python_semantic/src/analyze/branch_detection.rs index 7d1f1c2284..62ffb8e116 100644 --- a/crates/ruff_python_semantic/src/analyze/branch_detection.rs +++ b/crates/ruff_python_semantic/src/analyze/branch_detection.rs @@ -1,6 +1,6 @@ use std::cmp::Ordering; -use rustpython_parser::ast::{self, Excepthandler, Stmt}; +use rustpython_parser::ast::{self, ExceptHandler, Stmt}; use crate::node::{NodeId, Nodes}; @@ -57,7 +57,7 @@ fn alternatives(stmt: &Stmt) -> Vec> { }) => vec![body.iter().chain(orelse.iter()).collect()] .into_iter() .chain(handlers.iter().map(|handler| { - let Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { body, .. }) = + let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { body, .. }) = handler; body.iter().collect() })) diff --git a/crates/ruff_python_semantic/src/analyze/function_type.rs b/crates/ruff_python_semantic/src/analyze/function_type.rs index 89f68a78ff..95b0f3845e 100644 --- a/crates/ruff_python_semantic/src/analyze/function_type.rs +++ b/crates/ruff_python_semantic/src/analyze/function_type.rs @@ -6,10 +6,7 @@ use ruff_python_ast::helpers::map_callable; use crate::model::SemanticModel; use crate::scope::{Scope, ScopeKind}; -const CLASS_METHODS: [&str; 3] = ["__new__", "__init_subclass__", "__class_getitem__"]; -const METACLASS_BASES: [(&str, &str); 2] = [("", "type"), ("abc", "ABCMeta")]; - -#[derive(Copy, Clone)] +#[derive(Debug, Copy, Clone)] pub enum FunctionType { Function, Method, @@ -19,10 +16,10 @@ pub enum FunctionType { /// Classify a function based on its scope, name, and decorators. pub fn classify( - model: &SemanticModel, - scope: &Scope, name: &str, decorator_list: &[Decorator], + scope: &Scope, + semantic: &SemanticModel, classmethod_decorators: &[String], staticmethod_decorators: &[String], ) -> FunctionType { @@ -32,34 +29,40 @@ pub fn classify( if decorator_list.iter().any(|decorator| { // The method is decorated with a static method decorator (like // `@staticmethod`). - model + semantic .resolve_call_path(map_callable(&decorator.expression)) .map_or(false, |call_path| { - call_path.as_slice() == ["", "staticmethod"] - || staticmethod_decorators - .iter() - .any(|decorator| call_path == from_qualified_name(decorator)) - }) - }) { - FunctionType::StaticMethod - } else if CLASS_METHODS.contains(&name) - // Special-case class method, like `__new__`. - || scope.bases.iter().any(|expr| { - // The class itself extends a known metaclass, so all methods are class methods. - model.resolve_call_path(map_callable(expr)).map_or(false, |call_path| { - METACLASS_BASES - .iter() - .any(|(module, member)| call_path.as_slice() == [*module, *member]) - }) - }) - || decorator_list.iter().any(|decorator| { - // The method is decorated with a class method decorator (like `@classmethod`). - model.resolve_call_path(map_callable(&decorator.expression)).map_or(false, |call_path| { - call_path.as_slice() == ["", "classmethod"] || - classmethod_decorators + matches!( + call_path.as_slice(), + ["", "staticmethod"] | ["abc", "abstractstaticmethod"] + ) || staticmethod_decorators .iter() .any(|decorator| call_path == from_qualified_name(decorator)) }) + }) { + FunctionType::StaticMethod + } else if matches!(name, "__new__" | "__init_subclass__" | "__class_getitem__") + // Special-case class method, like `__new__`. + || scope.bases.iter().any(|expr| { + // The class itself extends a known metaclass, so all methods are class methods. + semantic + .resolve_call_path(map_callable(expr)) + .map_or(false, |call_path| { + matches!(call_path.as_slice(), ["", "type"] | ["abc", "ABCMeta"]) + }) + }) + || decorator_list.iter().any(|decorator| { + // The method is decorated with a class method decorator (like `@classmethod`). + semantic + .resolve_call_path(map_callable(&decorator.expression)) + .map_or(false, |call_path| { + matches!( + call_path.as_slice(), + ["", "classmethod"] | ["abc", "abstractclassmethod"] + ) || classmethod_decorators + .iter() + .any(|decorator| call_path == from_qualified_name(decorator)) + }) }) { FunctionType::ClassMethod diff --git a/crates/ruff_python_semantic/src/analyze/logging.rs b/crates/ruff_python_semantic/src/analyze/logging.rs index 9dd4079ed6..48fbc3fb16 100644 --- a/crates/ruff_python_semantic/src/analyze/logging.rs +++ b/crates/ruff_python_semantic/src/analyze/logging.rs @@ -17,10 +17,14 @@ use crate::model::SemanticModel; /// # This is detected to be a logger candidate /// bar.error() /// ``` -pub fn is_logger_candidate(func: &Expr, model: &SemanticModel) -> bool { +pub fn is_logger_candidate(func: &Expr, semantic: &SemanticModel) -> bool { if let Expr::Attribute(ast::ExprAttribute { value, .. }) = func { - let Some(call_path) = (if let Some(call_path) = model.resolve_call_path(value) { - if call_path.first().map_or(false, |module| *module == "logging") || call_path.as_slice() == ["flask", "current_app", "logger"] { + let Some(call_path) = (if let Some(call_path) = semantic.resolve_call_path(value) { + if call_path + .first() + .map_or(false, |module| *module == "logging") + || call_path.as_slice() == ["flask", "current_app", "logger"] + { Some(call_path) } else { None @@ -41,7 +45,7 @@ pub fn is_logger_candidate(func: &Expr, model: &SemanticModel) -> bool { /// If the keywords to a logging call contain `exc_info=True` or `exc_info=sys.exc_info()`, /// return the `Keyword` for `exc_info`. -pub fn exc_info<'a>(keywords: &'a [Keyword], model: &SemanticModel) -> Option<&'a Keyword> { +pub fn exc_info<'a>(keywords: &'a [Keyword], semantic: &SemanticModel) -> Option<&'a Keyword> { let exc_info = find_keyword(keywords, "exc_info")?; // Ex) `logging.error("...", exc_info=True)` @@ -57,8 +61,8 @@ pub fn exc_info<'a>(keywords: &'a [Keyword], model: &SemanticModel) -> Option<&' // Ex) `logging.error("...", exc_info=sys.exc_info())` if let Expr::Call(ast::ExprCall { func, .. }) = &exc_info.value { - if model.resolve_call_path(func).map_or(false, |call_path| { - call_path.as_slice() == ["sys", "exc_info"] + if semantic.resolve_call_path(func).map_or(false, |call_path| { + matches!(call_path.as_slice(), ["sys", "exc_info"]) }) { return Some(exc_info); } diff --git a/crates/ruff_python_semantic/src/analyze/typing.rs b/crates/ruff_python_semantic/src/analyze/typing.rs index 694e97e650..39ea75af86 100644 --- a/crates/ruff_python_semantic/src/analyze/typing.rs +++ b/crates/ruff_python_semantic/src/analyze/typing.rs @@ -1,11 +1,15 @@ //! Analysis rules for the `typing` module. +use num_traits::identities::Zero; use rustpython_parser::ast::{self, Constant, Expr, Operator}; -use num_traits::identities::Zero; use ruff_python_ast::call_path::{from_qualified_name, from_unqualified_name, CallPath}; +use ruff_python_ast::helpers::is_const_false; use ruff_python_stdlib::typing::{ - IMMUTABLE_GENERIC_TYPES, IMMUTABLE_TYPES, PEP_585_GENERICS, PEP_593_SUBSCRIPTS, SUBSCRIPTS, + as_pep_585_generic, has_pep_585_generic, is_immutable_generic_type, + is_immutable_non_generic_type, is_immutable_return_type, is_mutable_return_type, + is_pep_593_generic_member, is_pep_593_generic_type, is_standard_library_generic, + is_standard_library_generic_member, }; use crate::model::SemanticModel; @@ -29,47 +33,40 @@ pub enum SubscriptKind { pub fn match_annotated_subscript<'a>( expr: &Expr, - semantic_model: &SemanticModel, + semantic: &SemanticModel, typing_modules: impl Iterator, extend_generics: &[String], ) -> Option { - if !matches!(expr, Expr::Name(_) | Expr::Attribute(_)) { - return None; - } + semantic.resolve_call_path(expr).and_then(|call_path| { + if is_standard_library_generic(call_path.as_slice()) + || extend_generics + .iter() + .map(|target| from_qualified_name(target)) + .any(|target| call_path == target) + { + return Some(SubscriptKind::AnnotatedSubscript); + } - semantic_model - .resolve_call_path(expr) - .and_then(|call_path| { - if SUBSCRIPTS.contains(&call_path.as_slice()) - || extend_generics - .iter() - .map(|target| from_qualified_name(target)) - .any(|target| call_path == target) - { - return Some(SubscriptKind::AnnotatedSubscript); - } - if PEP_593_SUBSCRIPTS.contains(&call_path.as_slice()) { - return Some(SubscriptKind::PEP593AnnotatedSubscript); - } + if is_pep_593_generic_type(call_path.as_slice()) { + return Some(SubscriptKind::PEP593AnnotatedSubscript); + } - for module in typing_modules { - let module_call_path: CallPath = from_unqualified_name(module); - if call_path.starts_with(&module_call_path) { - for subscript in SUBSCRIPTS.iter() { - if call_path.last() == subscript.last() { - return Some(SubscriptKind::AnnotatedSubscript); - } + for module in typing_modules { + let module_call_path: CallPath = from_unqualified_name(module); + if call_path.starts_with(&module_call_path) { + if let Some(member) = call_path.last() { + if is_standard_library_generic_member(member) { + return Some(SubscriptKind::AnnotatedSubscript); } - for subscript in PEP_593_SUBSCRIPTS.iter() { - if call_path.last() == subscript.last() { - return Some(SubscriptKind::PEP593AnnotatedSubscript); - } + if is_pep_593_generic_member(member) { + return Some(SubscriptKind::PEP593AnnotatedSubscript); } } } + } - None - }) + None + }) } #[derive(Debug, Clone, Eq, PartialEq)] @@ -91,42 +88,29 @@ impl std::fmt::Display for ModuleMember { /// Returns the PEP 585 standard library generic variant for a `typing` module reference, if such /// a variant exists. -pub fn to_pep585_generic(expr: &Expr, semantic_model: &SemanticModel) -> Option { - semantic_model - .resolve_call_path(expr) - .and_then(|call_path| { - let [module, name] = call_path.as_slice() else { +pub fn to_pep585_generic(expr: &Expr, semantic: &SemanticModel) -> Option { + semantic.resolve_call_path(expr).and_then(|call_path| { + let [module, member] = call_path.as_slice() else { return None; }; - PEP_585_GENERICS.iter().find_map( - |((from_module, from_member), (to_module, to_member))| { - if module == from_module && name == from_member { - if to_module.is_empty() { - Some(ModuleMember::BuiltIn(to_member)) - } else { - Some(ModuleMember::Member(to_module, to_member)) - } - } else { - None - } - }, - ) + as_pep_585_generic(module, member).map(|(module, member)| { + if module.is_empty() { + ModuleMember::BuiltIn(member) + } else { + ModuleMember::Member(module, member) + } }) + }) } /// Return whether a given expression uses a PEP 585 standard library generic. -pub fn is_pep585_generic(expr: &Expr, model: &SemanticModel) -> bool { - if let Some(call_path) = model.resolve_call_path(expr) { +pub fn is_pep585_generic(expr: &Expr, semantic: &SemanticModel) -> bool { + semantic.resolve_call_path(expr).map_or(false, |call_path| { let [module, name] = call_path.as_slice() else { return false; }; - for (_, (to_module, to_member)) in PEP_585_GENERICS { - if module == to_module && name == to_member { - return true; - } - } - } - false + has_pep_585_generic(module, name) + }) } #[derive(Debug, Copy, Clone)] @@ -141,33 +125,47 @@ pub enum Pep604Operator { pub fn to_pep604_operator( value: &Expr, slice: &Expr, - semantic_model: &SemanticModel, + semantic: &SemanticModel, ) -> Option { - /// Returns `true` if any argument in the slice is a string. - fn any_arg_is_str(slice: &Expr) -> bool { + /// Returns `true` if any argument in the slice is a quoted annotation). + fn quoted_annotation(slice: &Expr) -> bool { match slice { Expr::Constant(ast::ExprConstant { value: Constant::Str(_), .. }) => true, - Expr::Tuple(ast::ExprTuple { elts, .. }) => elts.iter().any(any_arg_is_str), + Expr::Tuple(ast::ExprTuple { elts, .. }) => elts.iter().any(quoted_annotation), _ => false, } } - // If any of the _arguments_ are forward references, we can't use PEP 604. - // Ex) `Union["str", "int"]` can't be converted to `"str" | "int"`. - if any_arg_is_str(slice) { - return None; + // If the slice is a forward reference (e.g., `Optional["Foo"]`), it can only be rewritten + // if we're in a typing-only context. + // + // This, for example, is invalid, as Python will evaluate `"Foo" | None` at runtime in order to + // populate the function's `__annotations__`: + // ```python + // def f(x: "Foo" | None): ... + // ``` + // + // This, however, is valid: + // ```python + // def f(): + // x: "Foo" | None + // ``` + if quoted_annotation(slice) { + if semantic.execution_context().is_runtime() { + return None; + } } - semantic_model + semantic .resolve_call_path(value) .as_ref() .and_then(|call_path| { - if semantic_model.match_typing_call_path(call_path, "Optional") { + if semantic.match_typing_call_path(call_path, "Optional") { Some(Pep604Operator::Optional) - } else if semantic_model.match_typing_call_path(call_path, "Union") { + } else if semantic.match_typing_call_path(call_path, "Union") { Some(Pep604Operator::Union) } else { None @@ -177,39 +175,32 @@ pub fn to_pep604_operator( /// Return `true` if `Expr` represents a reference to a type annotation that resolves to an /// immutable type. -pub fn is_immutable_annotation(semantic_model: &SemanticModel, expr: &Expr) -> bool { +pub fn is_immutable_annotation(expr: &Expr, semantic: &SemanticModel) -> bool { match expr { Expr::Name(_) | Expr::Attribute(_) => { - semantic_model - .resolve_call_path(expr) - .map_or(false, |call_path| { - IMMUTABLE_TYPES - .iter() - .chain(IMMUTABLE_GENERIC_TYPES) - .any(|target| call_path.as_slice() == *target) - }) + semantic.resolve_call_path(expr).map_or(false, |call_path| { + is_immutable_non_generic_type(call_path.as_slice()) + || is_immutable_generic_type(call_path.as_slice()) + }) } - Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => semantic_model + Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => semantic .resolve_call_path(value) .map_or(false, |call_path| { - if IMMUTABLE_GENERIC_TYPES - .iter() - .any(|target| call_path.as_slice() == *target) - { + if is_immutable_generic_type(call_path.as_slice()) { true - } else if call_path.as_slice() == ["typing", "Union"] { + } else if matches!(call_path.as_slice(), ["typing", "Union"]) { if let Expr::Tuple(ast::ExprTuple { elts, .. }) = slice.as_ref() { elts.iter() - .all(|elt| is_immutable_annotation(semantic_model, elt)) + .all(|elt| is_immutable_annotation(elt, semantic)) } else { false } - } else if call_path.as_slice() == ["typing", "Optional"] { - is_immutable_annotation(semantic_model, slice) - } else if call_path.as_slice() == ["typing", "Annotated"] { + } else if matches!(call_path.as_slice(), ["typing", "Optional"]) { + is_immutable_annotation(slice, semantic) + } else if matches!(call_path.as_slice(), ["typing", "Annotated"]) { if let Expr::Tuple(ast::ExprTuple { elts, .. }) = slice.as_ref() { elts.first() - .map_or(false, |elt| is_immutable_annotation(semantic_model, elt)) + .map_or(false, |elt| is_immutable_annotation(elt, semantic)) } else { false } @@ -222,10 +213,7 @@ pub fn is_immutable_annotation(semantic_model: &SemanticModel, expr: &Expr) -> b op: Operator::BitOr, right, range: _range, - }) => { - is_immutable_annotation(semantic_model, left) - && is_immutable_annotation(semantic_model, right) - } + }) => is_immutable_annotation(left, semantic) && is_immutable_annotation(right, semantic), Expr::Constant(ast::ExprConstant { value: Constant::None, .. @@ -234,57 +222,49 @@ pub fn is_immutable_annotation(semantic_model: &SemanticModel, expr: &Expr) -> b } } -const IMMUTABLE_FUNCS: &[&[&str]] = &[ - &["", "bool"], - &["", "complex"], - &["", "float"], - &["", "frozenset"], - &["", "int"], - &["", "str"], - &["", "tuple"], - &["datetime", "date"], - &["datetime", "datetime"], - &["datetime", "timedelta"], - &["decimal", "Decimal"], - &["fractions", "Fraction"], - &["operator", "attrgetter"], - &["operator", "itemgetter"], - &["operator", "methodcaller"], - &["pathlib", "Path"], - &["types", "MappingProxyType"], - &["re", "compile"], -]; - -/// Return `true` if `func` is a function that returns an immutable object. +/// Return `true` if `func` is a function that returns an immutable value. pub fn is_immutable_func( - semantic_model: &SemanticModel, func: &Expr, + semantic: &SemanticModel, extend_immutable_calls: &[CallPath], ) -> bool { - semantic_model - .resolve_call_path(func) - .map_or(false, |call_path| { - IMMUTABLE_FUNCS + semantic.resolve_call_path(func).map_or(false, |call_path| { + is_immutable_return_type(call_path.as_slice()) + || extend_immutable_calls .iter() - .any(|target| call_path.as_slice() == *target) - || extend_immutable_calls - .iter() - .any(|target| call_path == *target) - }) + .any(|target| call_path == *target) + }) +} + +/// Return `true` if `func` is a function that returns a mutable value. +pub fn is_mutable_func(func: &Expr, semantic: &SemanticModel) -> bool { + semantic + .resolve_call_path(func) + .as_ref() + .map(CallPath::as_slice) + .map_or(false, is_mutable_return_type) +} + +/// Return `true` if `expr` is an expression that resolves to a mutable value. +pub fn is_mutable_expr(expr: &Expr, semantic: &SemanticModel) -> bool { + match expr { + Expr::List(_) + | Expr::Dict(_) + | Expr::Set(_) + | Expr::ListComp(_) + | Expr::DictComp(_) + | Expr::SetComp(_) => true, + Expr::Call(ast::ExprCall { func, .. }) => is_mutable_func(func, semantic), + _ => false, + } } /// Return `true` if [`Expr`] is a guard for a type-checking block. -pub fn is_type_checking_block(stmt: &ast::StmtIf, semantic_model: &SemanticModel) -> bool { +pub fn is_type_checking_block(stmt: &ast::StmtIf, semantic: &SemanticModel) -> bool { let ast::StmtIf { test, .. } = stmt; // Ex) `if False:` - if matches!( - test.as_ref(), - Expr::Constant(ast::ExprConstant { - value: Constant::Bool(false), - .. - }) - ) { + if is_const_false(test) { return true; } @@ -300,12 +280,9 @@ pub fn is_type_checking_block(stmt: &ast::StmtIf, semantic_model: &SemanticModel } // Ex) `if typing.TYPE_CHECKING:` - if semantic_model - .resolve_call_path(test) - .map_or(false, |call_path| { - call_path.as_slice() == ["typing", "TYPE_CHECKING"] - }) - { + if semantic.resolve_call_path(test).map_or(false, |call_path| { + matches!(call_path.as_slice(), ["typing", "TYPE_CHECKING"]) + }) { return true; } diff --git a/crates/ruff_python_semantic/src/analyze/visibility.rs b/crates/ruff_python_semantic/src/analyze/visibility.rs index a94ac4ca5c..e08916f660 100644 --- a/crates/ruff_python_semantic/src/analyze/visibility.rs +++ b/crates/ruff_python_semantic/src/analyze/visibility.rs @@ -14,45 +14,45 @@ pub enum Visibility { } /// Returns `true` if a function is a "static method". -pub fn is_staticmethod(model: &SemanticModel, decorator_list: &[Decorator]) -> bool { +pub fn is_staticmethod(decorator_list: &[Decorator], semantic: &SemanticModel) -> bool { decorator_list.iter().any(|decorator| { - model + semantic .resolve_call_path(map_callable(&decorator.expression)) .map_or(false, |call_path| { - call_path.as_slice() == ["", "staticmethod"] + matches!(call_path.as_slice(), ["", "staticmethod"]) }) }) } /// Returns `true` if a function is a "class method". -pub fn is_classmethod(model: &SemanticModel, decorator_list: &[Decorator]) -> bool { +pub fn is_classmethod(decorator_list: &[Decorator], semantic: &SemanticModel) -> bool { decorator_list.iter().any(|decorator| { - model + semantic .resolve_call_path(map_callable(&decorator.expression)) .map_or(false, |call_path| { - call_path.as_slice() == ["", "classmethod"] + matches!(call_path.as_slice(), ["", "classmethod"]) }) }) } /// Returns `true` if a function definition is an `@overload`. -pub fn is_overload(model: &SemanticModel, decorator_list: &[Decorator]) -> bool { - decorator_list - .iter() - .any(|decorator| model.match_typing_expr(map_callable(&decorator.expression), "overload")) +pub fn is_overload(decorator_list: &[Decorator], semantic: &SemanticModel) -> bool { + decorator_list.iter().any(|decorator| { + semantic.match_typing_expr(map_callable(&decorator.expression), "overload") + }) } /// Returns `true` if a function definition is an `@override` (PEP 698). -pub fn is_override(model: &SemanticModel, decorator_list: &[Decorator]) -> bool { - decorator_list - .iter() - .any(|decorator| model.match_typing_expr(map_callable(&decorator.expression), "override")) +pub fn is_override(decorator_list: &[Decorator], semantic: &SemanticModel) -> bool { + decorator_list.iter().any(|decorator| { + semantic.match_typing_expr(map_callable(&decorator.expression), "override") + }) } /// Returns `true` if a function definition is an abstract method based on its decorators. -pub fn is_abstract(model: &SemanticModel, decorator_list: &[Decorator]) -> bool { +pub fn is_abstract(decorator_list: &[Decorator], semantic: &SemanticModel) -> bool { decorator_list.iter().any(|decorator| { - model + semantic .resolve_call_path(map_callable(&decorator.expression)) .map_or(false, |call_path| { matches!( @@ -73,28 +73,29 @@ pub fn is_abstract(model: &SemanticModel, decorator_list: &[Decorator]) -> bool /// `extra_properties` can be used to check additional non-standard /// `@property`-like decorators. pub fn is_property( - model: &SemanticModel, decorator_list: &[Decorator], extra_properties: &[CallPath], + semantic: &SemanticModel, ) -> bool { decorator_list.iter().any(|decorator| { - model + semantic .resolve_call_path(map_callable(&decorator.expression)) .map_or(false, |call_path| { - call_path.as_slice() == ["", "property"] - || call_path.as_slice() == ["functools", "cached_property"] - || extra_properties - .iter() - .any(|extra_property| extra_property.as_slice() == call_path.as_slice()) + matches!( + call_path.as_slice(), + ["", "property"] | ["functools", "cached_property"] + ) || extra_properties + .iter() + .any(|extra_property| extra_property.as_slice() == call_path.as_slice()) }) }) } /// Returns `true` if a class is an `final`. -pub fn is_final(model: &SemanticModel, decorator_list: &[Decorator]) -> bool { +pub fn is_final(decorator_list: &[Decorator], semantic: &SemanticModel) -> bool { decorator_list .iter() - .any(|decorator| model.match_typing_expr(map_callable(&decorator.expression), "final")) + .any(|decorator| semantic.match_typing_expr(map_callable(&decorator.expression), "final")) } /// Returns `true` if a function is a "magic method". diff --git a/crates/ruff_python_semantic/src/binding.rs b/crates/ruff_python_semantic/src/binding.rs index c3294809d6..862be2b617 100644 --- a/crates/ruff_python_semantic/src/binding.rs +++ b/crates/ruff_python_semantic/src/binding.rs @@ -5,13 +5,12 @@ use ruff_text_size::TextRange; use rustpython_parser::ast::Ranged; use ruff_index::{newtype_index, IndexSlice, IndexVec}; -use ruff_python_ast::helpers; -use ruff_python_ast::source_code::Locator; use crate::context::ExecutionContext; use crate::model::SemanticModel; use crate::node::NodeId; use crate::reference::ReferenceId; +use crate::ScopeId; #[derive(Debug, Clone)] pub struct Binding<'a> { @@ -41,41 +40,75 @@ impl<'a> Binding<'a> { } /// Return `true` if this [`Binding`] represents an explicit re-export - /// (e.g., `import FastAPI as FastAPI`). + /// (e.g., `FastAPI` in `from fastapi import FastAPI as FastAPI`). pub const fn is_explicit_export(&self) -> bool { self.flags.contains(BindingFlags::EXPLICIT_EXPORT) } + /// Return `true` if this [`Binding`] represents an external symbol + /// (e.g., `FastAPI` in `from fastapi import FastAPI`). + pub const fn is_external(&self) -> bool { + self.flags.contains(BindingFlags::EXTERNAL) + } + + /// Return `true` if this [`Binding`] represents an aliased symbol + /// (e.g., `app` in `from fastapi import FastAPI as app`). + pub const fn is_alias(&self) -> bool { + self.flags.contains(BindingFlags::ALIAS) + } + + /// Return `true` if this [`Binding`] represents a `nonlocal`. A [`Binding`] is a `nonlocal` + /// if it's declared by a `nonlocal` statement, or shadows a [`Binding`] declared by a + /// `nonlocal` statement. + pub const fn is_nonlocal(&self) -> bool { + self.flags.contains(BindingFlags::NONLOCAL) + } + + /// Return `true` if this [`Binding`] represents a `global`. A [`Binding`] is a `global` if it's + /// declared by a `global` statement, or shadows a [`Binding`] declared by a `global` statement. + pub const fn is_global(&self) -> bool { + self.flags.contains(BindingFlags::GLOBAL) + } + + /// Return `true` if this [`Binding`] represents an unbound variable + /// (e.g., `x` in `x = 1; del x`). + pub const fn is_unbound(&self) -> bool { + matches!( + self.kind, + BindingKind::Annotation | BindingKind::Deletion | BindingKind::UnboundException(_) + ) + } + /// Return `true` if this binding redefines the given binding. pub fn redefines(&self, existing: &'a Binding) -> bool { match &self.kind { - BindingKind::Importation(Importation { qualified_name }) => { - if let BindingKind::SubmoduleImportation(SubmoduleImportation { + BindingKind::Import(Import { qualified_name }) => { + if let BindingKind::SubmoduleImport(SubmoduleImport { qualified_name: existing, }) = &existing.kind { return qualified_name == existing; } } - BindingKind::FromImportation(FromImportation { qualified_name }) => { - if let BindingKind::SubmoduleImportation(SubmoduleImportation { + BindingKind::FromImport(FromImport { qualified_name }) => { + if let BindingKind::SubmoduleImport(SubmoduleImport { qualified_name: existing, }) = &existing.kind { return qualified_name == existing; } } - BindingKind::SubmoduleImportation(SubmoduleImportation { qualified_name }) => { + BindingKind::SubmoduleImport(SubmoduleImport { qualified_name }) => { match &existing.kind { - BindingKind::Importation(Importation { + BindingKind::Import(Import { qualified_name: existing, }) - | BindingKind::SubmoduleImportation(SubmoduleImportation { + | BindingKind::SubmoduleImport(SubmoduleImport { qualified_name: existing, }) => { return qualified_name == existing; } - BindingKind::FromImportation(FromImportation { + BindingKind::FromImport(FromImport { qualified_name: existing, }) => { return qualified_name == existing; @@ -83,33 +116,30 @@ impl<'a> Binding<'a> { _ => {} } } - BindingKind::Annotation => { - return false; - } - BindingKind::FutureImportation => { + BindingKind::Deletion + | BindingKind::Annotation + | BindingKind::FutureImport + | BindingKind::Builtin => { return false; } _ => {} } matches!( existing.kind, - BindingKind::ClassDefinition - | BindingKind::FunctionDefinition - | BindingKind::Builtin - | BindingKind::Importation(..) - | BindingKind::FromImportation(..) - | BindingKind::SubmoduleImportation(..) + BindingKind::ClassDefinition(_) + | BindingKind::FunctionDefinition(_) + | BindingKind::Import(_) + | BindingKind::FromImport(_) + | BindingKind::SubmoduleImport(_) ) } /// Returns the fully-qualified symbol name, if this symbol was imported from another module. pub fn qualified_name(&self) -> Option<&str> { match &self.kind { - BindingKind::Importation(Importation { qualified_name }) => Some(qualified_name), - BindingKind::FromImportation(FromImportation { qualified_name }) => { - Some(qualified_name) - } - BindingKind::SubmoduleImportation(SubmoduleImportation { qualified_name }) => { + BindingKind::Import(Import { qualified_name }) => Some(qualified_name), + BindingKind::FromImport(FromImport { qualified_name }) => Some(qualified_name), + BindingKind::SubmoduleImport(SubmoduleImport { qualified_name }) => { Some(qualified_name) } _ => None, @@ -120,11 +150,11 @@ impl<'a> Binding<'a> { /// symbol was imported from another module. pub fn module_name(&self) -> Option<&str> { match &self.kind { - BindingKind::Importation(Importation { qualified_name }) - | BindingKind::SubmoduleImportation(SubmoduleImportation { qualified_name }) => { + BindingKind::Import(Import { qualified_name }) + | BindingKind::SubmoduleImport(SubmoduleImport { qualified_name }) => { Some(qualified_name.split('.').next().unwrap_or(qualified_name)) } - BindingKind::FromImportation(FromImportation { qualified_name }) => Some( + BindingKind::FromImport(FromImport { qualified_name }) => Some( qualified_name .rsplit_once('.') .map_or(qualified_name, |(module, _)| module), @@ -133,22 +163,10 @@ impl<'a> Binding<'a> { } } - /// Returns the appropriate visual range for highlighting this binding. - pub fn trimmed_range(&self, semantic_model: &SemanticModel, locator: &Locator) -> TextRange { - match self.kind { - BindingKind::ClassDefinition | BindingKind::FunctionDefinition => { - self.source.map_or(self.range, |source| { - helpers::identifier_range(semantic_model.stmts[source], locator) - }) - } - _ => self.range, - } - } - /// Returns the range of the binding's parent. - pub fn parent_range(&self, semantic_model: &SemanticModel) -> Option { + pub fn parent_range(&self, semantic: &SemanticModel) -> Option { self.source - .map(|node_id| semantic_model.stmts[node_id]) + .map(|node_id| semantic.stmts[node_id]) .and_then(|parent| { if parent.is_import_from_stmt() { Some(parent.range()) @@ -167,9 +185,47 @@ bitflags! { /// /// For example, the binding could be `FastAPI` in: /// ```python - /// import FastAPI as FastAPI + /// from fastapi import FastAPI as FastAPI /// ``` const EXPLICIT_EXPORT = 1 << 0; + + /// The binding represents an external symbol, like an import or a builtin. + /// + /// For example, the binding could be `FastAPI` in: + /// ```python + /// from fastapi import FastAPI + /// ``` + const EXTERNAL = 1 << 1; + + /// The binding is an aliased symbol. + /// + /// For example, the binding could be `app` in: + /// ```python + /// from fastapi import FastAPI as app + /// ``` + const ALIAS = 1 << 2; + + /// The binding is `nonlocal` to the declaring scope. This could be a binding created by + /// a `nonlocal` statement, or a binding that shadows such a binding. + /// + /// For example, both of the bindings in the following function are `nonlocal`: + /// ```python + /// def f(): + /// nonlocal x + /// x = 1 + /// ``` + const NONLOCAL = 1 << 3; + + /// The binding is `global`. This could be a binding created by a `global` statement, or a + /// binding that shadows such a binding. + /// + /// For example, both of the bindings in the following function are `global`: + /// ```python + /// def f(): + /// global x + /// x = 1 + /// ``` + const GLOBAL = 1 << 4; } } @@ -190,15 +246,10 @@ impl nohash_hasher::IsEnabled for BindingId {} pub struct Bindings<'a>(IndexVec>); impl<'a> Bindings<'a> { - /// Pushes a new binding and returns its id + /// Pushes a new [`Binding`] and returns its [`BindingId`]. pub fn push(&mut self, binding: Binding<'a>) -> BindingId { self.0.push(binding) } - - /// Returns the id that will be assigned when pushing the next binding - pub fn next_id(&self) -> BindingId { - self.0.next_index() - } } impl<'a> Deref for Bindings<'a> { @@ -221,14 +272,6 @@ impl<'a> FromIterator> for Bindings<'a> { } } -#[derive(Debug, Clone)] -pub struct StarImportation<'a> { - /// The level of the import. `None` or `Some(0)` indicate an absolute import. - pub level: Option, - /// The module being imported. `None` indicates a wildcard import. - pub module: Option<&'a str>, -} - #[derive(Debug, Clone)] pub struct Export<'a> { /// The names of the bindings exported via `__all__`. @@ -239,7 +282,7 @@ pub struct Export<'a> { /// Ex) `import foo` would be keyed on "foo". /// Ex) `import foo as bar` would be keyed on "bar". #[derive(Debug, Clone)] -pub struct Importation<'a> { +pub struct Import<'a> { /// The full name of the module being imported. /// Ex) Given `import foo`, `qualified_name` would be "foo". /// Ex) Given `import foo as bar`, `qualified_name` would be "foo". @@ -250,7 +293,7 @@ pub struct Importation<'a> { /// Ex) `from foo import bar` would be keyed on "bar". /// Ex) `from foo import bar as baz` would be keyed on "baz". #[derive(Debug, Clone)] -pub struct FromImportation { +pub struct FromImport { /// The full name of the member being imported. /// Ex) Given `from foo import bar`, `qualified_name` would be "foo.bar". /// Ex) Given `from foo import bar as baz`, `qualified_name` would be "foo.bar". @@ -260,7 +303,7 @@ pub struct FromImportation { /// A binding for a submodule imported from a module, keyed on the name of the parent module. /// Ex) `import foo.bar` would be keyed on "foo". #[derive(Debug, Clone)] -pub struct SubmoduleImportation<'a> { +pub struct SubmoduleImport<'a> { /// The full name of the submodule being imported. /// Ex) Given `import foo.bar`, `qualified_name` would be "foo.bar". pub qualified_name: &'a str, @@ -319,7 +362,7 @@ pub enum BindingKind<'a> { /// def foo(): /// nonlocal x /// ``` - Nonlocal, + Nonlocal(ScopeId), /// A binding for a builtin, like `print` or `bool`. Builtin, @@ -329,14 +372,14 @@ pub enum BindingKind<'a> { /// class Foo: /// ... /// ``` - ClassDefinition, + ClassDefinition(ScopeId), /// A binding for a function, like `foo` in: /// ```python /// def foo(): /// ... /// ``` - FunctionDefinition, + FunctionDefinition(ScopeId), /// A binding for an `__all__` export, like `__all__` in: /// ```python @@ -348,25 +391,47 @@ pub enum BindingKind<'a> { /// ```python /// from __future__ import annotations /// ``` - FutureImportation, + FutureImport, /// A binding for a straight `import`, like `foo` in: /// ```python /// import foo /// ``` - Importation(Importation<'a>), + Import(Import<'a>), /// A binding for a member imported from a module, like `bar` in: /// ```python /// from foo import bar /// ``` - FromImportation(FromImportation), + FromImport(FromImport), /// A binding for a submodule imported from a module, like `bar` in: /// ```python /// import foo.bar /// ``` - SubmoduleImportation(SubmoduleImportation<'a>), + SubmoduleImport(SubmoduleImport<'a>), + + /// A binding for a deletion, like `x` in: + /// ```python + /// del x + /// ``` + Deletion, + + /// A binding to unbind the local variable, like `x` in: + /// ```python + /// try: + /// ... + /// except Exception as x: + /// ... + /// ``` + /// + /// After the `except` block, `x` is unbound, despite the lack + /// of an explicit `del` statement. + /// + /// + /// Stores the ID of the binding that was shadowed in the enclosing + /// scope, if any. + UnboundException(Option), } bitflags! { diff --git a/crates/ruff_python_semantic/src/definition.rs b/crates/ruff_python_semantic/src/definition.rs index df75490f5e..c5cbbefa90 100644 --- a/crates/ruff_python_semantic/src/definition.rs +++ b/crates/ruff_python_semantic/src/definition.rs @@ -115,7 +115,7 @@ impl<'a> Definitions<'a> { /// /// Members are assumed to be pushed in traversal order, such that parents are pushed before /// their children. - pub fn push_member(&mut self, member: Member<'a>) -> DefinitionId { + pub(crate) fn push_member(&mut self, member: Member<'a>) -> DefinitionId { self.0.push(Definition::Member(member)) } @@ -202,8 +202,8 @@ impl<'a> Deref for Definitions<'a> { } impl<'a> IntoIterator for Definitions<'a> { - type Item = Definition<'a>; type IntoIter = std::vec::IntoIter; + type Item = Definition<'a>; fn into_iter(self) -> Self::IntoIter { self.0.into_iter() diff --git a/crates/ruff_python_semantic/src/globals.rs b/crates/ruff_python_semantic/src/globals.rs index dd4b2c460a..876ecfb628 100644 --- a/crates/ruff_python_semantic/src/globals.rs +++ b/crates/ruff_python_semantic/src/globals.rs @@ -49,11 +49,11 @@ impl<'a> Globals<'a> { builder.finish() } - pub fn get(&self, name: &str) -> Option<&TextRange> { + pub(crate) fn get(&self, name: &str) -> Option<&TextRange> { self.0.get(name) } - pub fn iter(&self) -> impl Iterator + '_ { + pub(crate) fn iter(&self) -> impl Iterator + '_ { self.0.iter() } } diff --git a/crates/ruff_python_semantic/src/lib.rs b/crates/ruff_python_semantic/src/lib.rs index e876f26181..3b7ce9a7a5 100644 --- a/crates/ruff_python_semantic/src/lib.rs +++ b/crates/ruff_python_semantic/src/lib.rs @@ -1,9 +1,20 @@ pub mod analyze; -pub mod binding; -pub mod context; -pub mod definition; -pub mod globals; -pub mod model; -pub mod node; -pub mod reference; -pub mod scope; +mod binding; +mod context; +mod definition; +mod globals; +mod model; +mod node; +mod reference; +mod scope; +mod star_import; + +pub use binding::*; +pub use context::*; +pub use definition::*; +pub use globals::*; +pub use model::*; +pub use node::*; +pub use reference::*; +pub use scope::*; +pub use star_import::*; diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index 0edd9451ca..38bd3c4783 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -10,11 +10,11 @@ use smallvec::smallvec; use ruff_python_ast::call_path::{collect_call_path, from_unqualified_name, CallPath}; use ruff_python_ast::helpers::from_relative_import; use ruff_python_stdlib::path::is_python_stub_file; -use ruff_python_stdlib::typing::TYPING_EXTENSIONS; +use ruff_python_stdlib::typing::is_typing_extension; use crate::binding::{ - Binding, BindingFlags, BindingId, BindingKind, Bindings, Exceptions, FromImportation, - Importation, SubmoduleImportation, + Binding, BindingFlags, BindingId, BindingKind, Bindings, Exceptions, FromImport, Import, + SubmoduleImport, }; use crate::context::ExecutionContext; use crate::definition::{Definition, DefinitionId, Definitions, Member, Module}; @@ -25,34 +25,97 @@ use crate::scope::{Scope, ScopeId, ScopeKind, Scopes}; /// A semantic model for a Python module, to enable querying the module's semantic information. pub struct SemanticModel<'a> { - pub typing_modules: &'a [String], - pub module_path: Option<&'a [String]>, - // Stack of all visited statements, along with the identifier of the current statement. + typing_modules: &'a [String], + module_path: Option<&'a [String]>, + + /// Stack of all visited statements. pub stmts: Nodes<'a>, - pub stmt_id: Option, - // Stack of current expressions. - pub exprs: Vec<&'a Expr>, - // Stack of all scopes, along with the identifier of the current scope. + + /// The identifier of the current statement. + stmt_id: Option, + + /// Stack of current expressions. + exprs: Vec<&'a Expr>, + + /// Stack of all scopes, along with the identifier of the current scope. pub scopes: Scopes<'a>, pub scope_id: ScopeId, - pub dead_scopes: Vec, - // Stack of all definitions created in any scope, at any point in execution, along with the - // identifier of the current definition. + + /// Stack of all definitions created in any scope, at any point in execution. pub definitions: Definitions<'a>, + + /// The ID of the current definition. pub definition_id: DefinitionId, - // A stack of all bindings created in any scope, at any point in execution. + + /// A stack of all bindings created in any scope, at any point in execution. pub bindings: Bindings<'a>, - // Stack of all references created in any scope, at any point in execution. + + /// Stack of all references created in any scope, at any point in execution. references: References, - // Arena of global bindings. + + /// Arena of global bindings. globals: GlobalsArena<'a>, - // Map from binding index to indexes of bindings that shadow it in other scopes. + + /// Map from binding ID to binding ID that it shadows (in another scope). + /// + /// For example, given: + /// ```python + /// import x + /// + /// def f(): + /// x = 1 + /// ``` + /// + /// In this case, the binding created by `x = 1` shadows the binding created by `import x`, + /// despite the fact that they're in different scopes. pub shadowed_bindings: HashMap>, - // Body iteration; used to peek at siblings. + + /// Map from binding index to indexes of bindings that annotate it (in the same scope). + /// + /// For example, given: + /// ```python + /// x = 1 + /// x: int + /// ``` + /// + /// In this case, the binding created by `x = 1` is annotated by the binding created by + /// `x: int`. We don't consider the latter binding to _shadow_ the former, because it doesn't + /// change the value of the binding, and so we don't store in on the scope. But we _do_ want to + /// track the annotation in some form, since it's a reference to `x`. + /// + /// Note that, given: + /// ```python + /// x: int + /// ``` + /// + /// In this case, we _do_ store the binding created by `x: int` directly on the scope, and not + /// as a delayed annotation. Annotations are thus treated as bindings only when they are the + /// first binding in a scope; any annotations that follow are treated as "delayed" annotations. + delayed_annotations: HashMap, BuildNoHashHasher>, + + /// Map from binding ID to the IDs of all scopes in which it is declared a `global` or + /// `nonlocal`. + /// + /// For example, given: + /// ```python + /// x = 1 + /// + /// def f(): + /// global x + /// ``` + /// + /// In this case, the binding created by `x = 1` is rebound within the scope created by `f` + /// by way of the `global x` statement. + rebinding_scopes: HashMap, BuildNoHashHasher>, + + /// Body iteration; used to peek at siblings. pub body: &'a [Stmt], pub body_index: usize, - // Internal, derivative state. + + /// Flags for the semantic model. pub flags: SemanticModelFlags, + + /// Exceptions that have been handled by the current scope. pub handled_exceptions: Vec, } @@ -66,13 +129,14 @@ impl<'a> SemanticModel<'a> { exprs: Vec::default(), scopes: Scopes::default(), scope_id: ScopeId::global(), - dead_scopes: Vec::default(), definitions: Definitions::for_module(module), definition_id: DefinitionId::module(), bindings: Bindings::default(), references: References::default(), globals: GlobalsArena::default(), shadowed_bindings: IntMap::default(), + delayed_annotations: IntMap::default(), + rebinding_scopes: IntMap::default(), body: &[], body_index: 0, flags: SemanticModelFlags::new(path), @@ -80,6 +144,18 @@ impl<'a> SemanticModel<'a> { } } + /// Return the [`Binding`] for the given [`BindingId`]. + #[inline] + pub fn binding(&self, id: BindingId) -> &Binding { + &self.bindings[id] + } + + /// Resolve the [`Reference`] for the given [`ReferenceId`]. + #[inline] + pub fn reference(&self, id: ReferenceId) -> &Reference { + &self.references[id] + } + /// Return `true` if the `Expr` is a reference to `typing.${target}`. pub fn match_typing_expr(&self, expr: &Expr, target: &str) -> bool { self.resolve_call_path(expr).map_or(false, |call_path| { @@ -97,7 +173,7 @@ impl<'a> SemanticModel<'a> { return true; } - if TYPING_EXTENSIONS.contains(target) { + if is_typing_extension(target) { if call_path.as_slice() == ["typing_extensions", target] { return true; } @@ -165,8 +241,9 @@ impl<'a> SemanticModel<'a> { .map_or(false, |binding| binding.kind.is_builtin()) } - /// Return `true` if `member` is unbound. - pub fn is_unbound(&self, member: &str) -> bool { + /// Return `true` if `member` is an "available" symbol, i.e., a symbol that has not been bound + /// in the current scope, or in any containing scope. + pub fn is_available(&self, member: &str) -> bool { self.find_binding(member) .map_or(true, |binding| binding.kind.is_builtin()) } @@ -177,20 +254,22 @@ impl<'a> SemanticModel<'a> { // should prefer it over local resolutions. if self.in_forward_reference() { if let Some(binding_id) = self.scopes.global().get(symbol) { - // Mark the binding as used. - let context = self.execution_context(); - let reference_id = self.references.push(ScopeId::global(), range, context); - self.bindings[binding_id].references.push(reference_id); - - // Mark any submodule aliases as used. - if let Some(binding_id) = - self.resolve_submodule(symbol, ScopeId::global(), binding_id) - { + if !self.bindings[binding_id].is_unbound() { + // Mark the binding as used. + let context = self.execution_context(); let reference_id = self.references.push(ScopeId::global(), range, context); self.bindings[binding_id].references.push(reference_id); - } - return ResolvedRead::Resolved(binding_id); + // Mark any submodule aliases as used. + if let Some(binding_id) = + self.resolve_submodule(symbol, ScopeId::global(), binding_id) + { + let reference_id = self.references.push(ScopeId::global(), range, context); + self.bindings[binding_id].references.push(reference_id); + } + + return ResolvedRead::Resolved(binding_id); + } } } @@ -226,21 +305,79 @@ impl<'a> SemanticModel<'a> { self.bindings[binding_id].references.push(reference_id); } - // But if it's a type annotation, don't treat it as resolved, unless we're in a - // forward reference. For example, given: - // - // ```python - // name: str - // print(name) - // ``` - // - // The `name` in `print(name)` should be treated as unresolved, but the `name` in - // `name: str` should be treated as used. - if !self.in_forward_reference() && self.bindings[binding_id].kind.is_annotation() { - continue; - } + match self.bindings[binding_id].kind { + // If it's a type annotation, don't treat it as resolved. For example, given: + // + // ```python + // name: str + // print(name) + // ``` + // + // The `name` in `print(name)` should be treated as unresolved, but the `name` in + // `name: str` should be treated as used. + BindingKind::Annotation => continue, - return ResolvedRead::Resolved(binding_id); + // If it's a deletion, don't treat it as resolved, since the name is now + // unbound. For example, given: + // + // ```python + // x = 1 + // del x + // print(x) + // ``` + // + // The `x` in `print(x)` should be treated as unresolved. + // + // Similarly, given: + // + // ```python + // try: + // pass + // except ValueError as x: + // pass + // + // print(x) + // + // The `x` in `print(x)` should be treated as unresolved. + BindingKind::Deletion | BindingKind::UnboundException(None) => { + return ResolvedRead::UnboundLocal(binding_id) + } + + // If we hit an unbound exception that shadowed a bound name, resole to the + // bound name. For example, given: + // + // ```python + // x = 1 + // + // try: + // pass + // except ValueError as x: + // pass + // + // print(x) + // ``` + // + // The `x` in `print(x)` should resolve to the `x` in `x = 1`. + BindingKind::UnboundException(Some(binding_id)) => { + // Mark the binding as used. + let context = self.execution_context(); + let reference_id = self.references.push(self.scope_id, range, context); + self.bindings[binding_id].references.push(reference_id); + + // Mark any submodule aliases as used. + if let Some(binding_id) = + self.resolve_submodule(symbol, scope_id, binding_id) + { + let reference_id = self.references.push(self.scope_id, range, context); + self.bindings[binding_id].references.push(reference_id); + } + + return ResolvedRead::Resolved(binding_id); + } + + // Otherwise, treat it as resolved. + _ => return ResolvedRead::Resolved(binding_id), + } } // Allow usages of `__module__` and `__qualname__` within class scopes, e.g.: @@ -269,12 +406,82 @@ impl<'a> SemanticModel<'a> { } if import_starred { - ResolvedRead::StarImport + ResolvedRead::WildcardImport } else { ResolvedRead::NotFound } } + /// Lookup a symbol in the current scope. This is a carbon copy of [`Self::resolve_read`], but + /// doesn't add any read references to the resolved symbol. + pub fn lookup_symbol(&self, symbol: &str) -> Option { + if self.in_forward_reference() { + if let Some(binding_id) = self.scopes.global().get(symbol) { + if !self.bindings[binding_id].is_unbound() { + return Some(binding_id); + } + } + } + + let mut seen_function = false; + for (index, scope_id) in self.scopes.ancestor_ids(self.scope_id).enumerate() { + let scope = &self.scopes[scope_id]; + if scope.kind.is_class() { + if seen_function && matches!(symbol, "__class__") { + return None; + } + if index > 0 { + continue; + } + } + + if let Some(binding_id) = scope.get(symbol) { + match self.bindings[binding_id].kind { + BindingKind::Annotation => continue, + BindingKind::Deletion | BindingKind::UnboundException(None) => return None, + BindingKind::UnboundException(Some(binding_id)) => return Some(binding_id), + _ => return Some(binding_id), + } + } + + if index == 0 && scope.kind.is_class() { + if matches!(symbol, "__module__" | "__qualname__") { + return None; + } + } + + seen_function |= scope.kind.is_any_function(); + } + + None + } + + /// Lookup a qualified attribute in the current scope. + /// + /// For example, given `["Class", "method"`], resolve the `BindingKind::ClassDefinition` + /// associated with `Class`, then the `BindingKind::FunctionDefinition` associated with + /// `Class#method`. + pub fn lookup_attribute(&'a self, value: &'a Expr) -> Option { + let call_path = collect_call_path(value)?; + + // Find the symbol in the current scope. + let (symbol, attribute) = call_path.split_first()?; + let mut binding_id = self.lookup_symbol(symbol)?; + + // Recursively resolve class attributes, e.g., `foo.bar.baz` in. + let mut tail = attribute; + while let Some((symbol, rest)) = tail.split_first() { + // Find the next symbol in the class scope. + let BindingKind::ClassDefinition(scope_id) = self.binding(binding_id).kind else { + return None; + }; + binding_id = self.scopes[scope_id].get(symbol)?; + tail = rest; + } + + Some(binding_id) + } + /// Given a `BindingId`, return the `BindingId` of the submodule import that it aliases. fn resolve_submodule( &self, @@ -322,7 +529,7 @@ impl<'a> SemanticModel<'a> { let head = call_path.first()?; let binding = self.find_binding(head)?; match &binding.kind { - BindingKind::Importation(Importation { + BindingKind::Import(Import { qualified_name: name, }) => { if name.starts_with('.') { @@ -339,7 +546,7 @@ impl<'a> SemanticModel<'a> { Some(source_path) } } - BindingKind::SubmoduleImportation(SubmoduleImportation { + BindingKind::SubmoduleImport(SubmoduleImport { qualified_name: name, }) => { let name = name.split('.').next().unwrap_or(name); @@ -347,7 +554,7 @@ impl<'a> SemanticModel<'a> { source_path.extend(call_path.into_iter().skip(1)); Some(source_path) } - BindingKind::FromImportation(FromImportation { + BindingKind::FromImport(FromImport { qualified_name: name, }) => { if name.starts_with('.') { @@ -398,14 +605,14 @@ impl<'a> SemanticModel<'a> { // Ex) Given `module="sys"` and `object="exit"`: // `import sys` -> `sys.exit` // `import sys as sys2` -> `sys2.exit` - BindingKind::Importation(Importation { qualified_name }) => { + BindingKind::Import(Import { qualified_name }) => { if qualified_name == &module { if let Some(source) = binding.source { // Verify that `sys` isn't bound in an inner scope. if self .scopes() .take(scope_index) - .all(|scope| scope.get(name).is_none()) + .all(|scope| !scope.has(name)) { return Some(ImportedName { name: format!("{name}.{member}"), @@ -419,7 +626,7 @@ impl<'a> SemanticModel<'a> { // Ex) Given `module="os.path"` and `object="join"`: // `from os.path import join` -> `join` // `from os.path import join as join2` -> `join2` - BindingKind::FromImportation(FromImportation { qualified_name }) => { + BindingKind::FromImport(FromImport { qualified_name }) => { if let Some((target_module, target_member)) = qualified_name.split_once('.') { if target_module == module && target_member == member { @@ -428,7 +635,7 @@ impl<'a> SemanticModel<'a> { if self .scopes() .take(scope_index) - .all(|scope| scope.get(name).is_none()) + .all(|scope| !scope.has(name)) { return Some(ImportedName { name: (*name).to_string(), @@ -442,14 +649,14 @@ impl<'a> SemanticModel<'a> { } // Ex) Given `module="os"` and `object="name"`: // `import os.path ` -> `os.name` - BindingKind::SubmoduleImportation(SubmoduleImportation { .. }) => { + BindingKind::SubmoduleImport(SubmoduleImport { .. }) => { if name == module { if let Some(source) = binding.source { // Verify that `os` isn't bound in an inner scope. if self .scopes() .take(scope_index) - .all(|scope| scope.get(name).is_none()) + .all(|scope| !scope.has(name)) { return Some(ImportedName { name: format!("{name}.{member}"), @@ -499,7 +706,6 @@ impl<'a> SemanticModel<'a> { /// Pop the current [`Scope`] off the stack. pub fn pop_scope(&mut self) { - self.dead_scopes.push(self.scope_id); self.scope_id = self.scopes[self.scope_id] .parent .expect("Attempted to pop without scope"); @@ -591,9 +797,11 @@ impl<'a> SemanticModel<'a> { pub fn set_globals(&mut self, globals: Globals<'a>) { // If any global bindings don't already exist in the global scope, add them. for (name, range) in globals.iter() { - if self.global_scope().get(name).map_or(true, |binding_id| { - self.bindings[binding_id].kind.is_annotation() - }) { + if self + .global_scope() + .get(name) + .map_or(true, |binding_id| self.bindings[binding_id].is_unbound()) + { let id = self.bindings.push(Binding { kind: BindingKind::Assignment, range: *range, @@ -616,6 +824,26 @@ impl<'a> SemanticModel<'a> { self.globals[global_id].get(name).copied() } + /// Given a `name` that has been declared `nonlocal`, return the [`ScopeId`] and [`BindingId`] + /// to which it refers. + /// + /// Unlike `global` declarations, for which the scope is unambiguous, Python requires that + /// `nonlocal` declarations refer to the closest enclosing scope that contains a binding for + /// the given name. + pub fn nonlocal(&self, name: &str) -> Option<(ScopeId, BindingId)> { + self.scopes + .ancestor_ids(self.scope_id) + .skip(1) + .find_map(|scope_id| { + let scope = &self.scopes[scope_id]; + if scope.kind.is_module() || scope.kind.is_class() { + None + } else { + scope.get(name).map(|binding_id| (scope_id, binding_id)) + } + }) + } + /// Return `true` if the given [`ScopeId`] matches that of the current scope. pub fn is_current_scope(&self, scope_id: ScopeId) -> bool { self.scope_id == scope_id @@ -670,15 +898,38 @@ impl<'a> SemanticModel<'a> { self.bindings[binding_id].references.push(reference_id); } - /// Resolve a [`ReferenceId`]. - pub fn reference(&self, reference_id: ReferenceId) -> &Reference { - self.references.resolve(reference_id) + /// Add a [`BindingId`] to the list of delayed annotations for the given [`BindingId`]. + pub fn add_delayed_annotation(&mut self, binding_id: BindingId, annotation_id: BindingId) { + self.delayed_annotations + .entry(binding_id) + .or_insert_with(Vec::new) + .push(annotation_id); + } + + /// Return the list of delayed annotations for the given [`BindingId`]. + pub fn delayed_annotations(&self, binding_id: BindingId) -> Option<&[BindingId]> { + self.delayed_annotations.get(&binding_id).map(Vec::as_slice) + } + + /// Mark the given [`BindingId`] as rebound in the given [`ScopeId`] (i.e., declared as + /// `global` or `nonlocal`). + pub fn add_rebinding_scope(&mut self, binding_id: BindingId, scope_id: ScopeId) { + self.rebinding_scopes + .entry(binding_id) + .or_insert_with(Vec::new) + .push(scope_id); + } + + /// Return the list of [`ScopeId`]s in which the given [`BindingId`] is rebound (i.e., declared + /// as `global` or `nonlocal`). + pub fn rebinding_scopes(&self, binding_id: BindingId) -> Option<&[ScopeId]> { + self.rebinding_scopes.get(&binding_id).map(Vec::as_slice) } /// Return the [`ExecutionContext`] of the current scope. pub const fn execution_context(&self) -> ExecutionContext { if self.in_type_checking_block() - || self.in_annotation() + || self.in_typing_only_annotation() || self.in_complex_string_type_definition() || self.in_simple_string_type_definition() { @@ -723,7 +974,18 @@ impl<'a> SemanticModel<'a> { /// Return `true` if the context is in a type annotation. pub const fn in_annotation(&self) -> bool { - self.flags.contains(SemanticModelFlags::ANNOTATION) + self.in_typing_only_annotation() || self.in_runtime_annotation() + } + + /// Return `true` if the context is in a typing-only type annotation. + pub const fn in_typing_only_annotation(&self) -> bool { + self.flags + .contains(SemanticModelFlags::TYPING_ONLY_ANNOTATION) + } + + /// Return `true` if the context is in a runtime-required type annotation. + pub const fn in_runtime_annotation(&self) -> bool { + self.flags.contains(SemanticModelFlags::RUNTIME_ANNOTATION) } /// Return `true` if the context is in a type definition. @@ -774,7 +1036,7 @@ impl<'a> SemanticModel<'a> { pub const fn in_forward_reference(&self) -> bool { self.in_simple_string_type_definition() || self.in_complex_string_type_definition() - || (self.in_future_type_definition() && self.in_annotation()) + || (self.in_future_type_definition() && self.in_typing_only_annotation()) } /// Return `true` if the context is in an exception handler. @@ -827,19 +1089,105 @@ impl<'a> SemanticModel<'a> { pub const fn future_annotations(&self) -> bool { self.flags.contains(SemanticModelFlags::FUTURE_ANNOTATIONS) } + + /// Return an iterator over all bindings shadowed by the given [`BindingId`], within the + /// containing scope, and across scopes. + pub fn shadowed_bindings( + &self, + scope_id: ScopeId, + binding_id: BindingId, + ) -> impl Iterator + '_ { + let mut first = true; + let mut binding_id = binding_id; + std::iter::from_fn(move || { + // First, check whether this binding is shadowing another binding in a different scope. + if std::mem::take(&mut first) { + if let Some(shadowed_id) = self.shadowed_bindings.get(&binding_id).copied() { + return Some(ShadowedBinding { + binding_id, + shadowed_id, + same_scope: false, + }); + } + } + + // Otherwise, check whether this binding is shadowing another binding in the same scope. + if let Some(shadowed_id) = self.scopes[scope_id].shadowed_binding(binding_id) { + let next = ShadowedBinding { + binding_id, + shadowed_id, + same_scope: true, + }; + + // Advance to the next binding in the scope. + first = true; + binding_id = shadowed_id; + + return Some(next); + } + + None + }) + } +} + +pub struct ShadowedBinding { + /// The binding that is shadowing another binding. + binding_id: BindingId, + /// The binding that is being shadowed. + shadowed_id: BindingId, + /// Whether the shadowing and shadowed bindings are in the same scope. + same_scope: bool, +} + +impl ShadowedBinding { + pub const fn binding_id(&self) -> BindingId { + self.binding_id + } + + pub const fn shadowed_id(&self) -> BindingId { + self.shadowed_id + } + + pub const fn same_scope(&self) -> bool { + self.same_scope + } } bitflags! { /// Flags indicating the current context of the analysis. #[derive(Debug, Default, Copy, Clone, Eq, PartialEq)] pub struct SemanticModelFlags: u16 { - /// The context is in a type annotation. + /// The context is in a typing-time-only type annotation. /// /// For example, the context could be visiting `int` in: /// ```python - /// x: int = 1 + /// def foo() -> int: + /// x: int = 1 /// ``` - const ANNOTATION = 1 << 0; + /// + /// In this case, Python doesn't require that the type annotation be evaluated at runtime. + /// + /// If `from __future__ import annotations` is used, all annotations are evaluated at + /// typing time. Otherwise, all function argument annotations are evaluated at runtime, as + /// are any annotated assignments in module or class scopes. + const TYPING_ONLY_ANNOTATION = 1 << 0; + + /// The context is in a runtime type annotation. + /// + /// For example, the context could be visiting `int` in: + /// ```python + /// def foo(x: int) -> int: + /// ... + /// ``` + /// + /// In this case, Python requires that the type annotation be evaluated at runtime, + /// as it needs to be available on the function's `__annotations__` attribute. + /// + /// If `from __future__ import annotations` is used, all annotations are evaluated at + /// typing time. Otherwise, all function argument annotations are evaluated at runtime, as + /// are any annotated assignments in module or class scopes. + const RUNTIME_ANNOTATION = 1 << 1; /// The context is in a type definition. /// @@ -853,7 +1201,7 @@ bitflags! { /// All type annotations are also type definitions, but the converse is not true. /// In our example, `int` is a type definition but not a type annotation, as it /// doesn't appear in a type annotation context, but rather in a type definition. - const TYPE_DEFINITION = 1 << 1; + const TYPE_DEFINITION = 1 << 2; /// The context is in a (deferred) "simple" string type definition. /// @@ -864,7 +1212,7 @@ bitflags! { /// /// "Simple" string type definitions are those that consist of a single string literal, /// as opposed to an implicitly concatenated string literal. - const SIMPLE_STRING_TYPE_DEFINITION = 1 << 2; + const SIMPLE_STRING_TYPE_DEFINITION = 1 << 3; /// The context is in a (deferred) "complex" string type definition. /// @@ -875,7 +1223,7 @@ bitflags! { /// /// "Complex" string type definitions are those that consist of a implicitly concatenated /// string literals. These are uncommon but valid. - const COMPLEX_STRING_TYPE_DEFINITION = 1 << 3; + const COMPLEX_STRING_TYPE_DEFINITION = 1 << 4; /// The context is in a (deferred) `__future__` type definition. /// @@ -888,7 +1236,7 @@ bitflags! { /// /// `__future__`-style type annotations are only enabled if the `annotations` feature /// is enabled via `from __future__ import annotations`. - const FUTURE_TYPE_DEFINITION = 1 << 4; + const FUTURE_TYPE_DEFINITION = 1 << 5; /// The context is in an exception handler. /// @@ -899,7 +1247,7 @@ bitflags! { /// except Exception: /// x: int = 1 /// ``` - const EXCEPTION_HANDLER = 1 << 5; + const EXCEPTION_HANDLER = 1 << 6; /// The context is in an f-string. /// @@ -907,7 +1255,7 @@ bitflags! { /// ```python /// f'{x}' /// ``` - const F_STRING = 1 << 6; + const F_STRING = 1 << 7; /// The context is in a nested f-string. /// @@ -915,7 +1263,7 @@ bitflags! { /// ```python /// f'{f"{x}"}' /// ``` - const NESTED_F_STRING = 1 << 7; + const NESTED_F_STRING = 1 << 8; /// The context is in a boolean test. /// @@ -927,7 +1275,7 @@ bitflags! { /// /// The implication is that the actual value returned by the current expression is /// not used, only its truthiness. - const BOOLEAN_TEST = 1 << 8; + const BOOLEAN_TEST = 1 << 9; /// The context is in a `typing::Literal` annotation. /// @@ -936,7 +1284,7 @@ bitflags! { /// def f(x: Literal["A", "B", "C"]): /// ... /// ``` - const LITERAL = 1 << 9; + const LITERAL = 1 << 10; /// The context is in a subscript expression. /// @@ -944,7 +1292,7 @@ bitflags! { /// ```python /// x["a"]["b"] /// ``` - const SUBSCRIPT = 1 << 10; + const SUBSCRIPT = 1 << 11; /// The context is in a type-checking block. /// @@ -956,8 +1304,7 @@ bitflags! { /// if TYPE_CHECKING: /// x: int = 1 /// ``` - const TYPE_CHECKING_BLOCK = 1 << 11; - + const TYPE_CHECKING_BLOCK = 1 << 12; /// The context has traversed past the "top-of-file" import boundary. /// @@ -970,7 +1317,7 @@ bitflags! { /// /// x: int = 1 /// ``` - const IMPORT_BOUNDARY = 1 << 12; + const IMPORT_BOUNDARY = 1 << 13; /// The context has traversed past the `__future__` import boundary. /// @@ -985,7 +1332,7 @@ bitflags! { /// /// Python considers it a syntax error to import from `__future__` after /// any other non-`__future__`-importing statements. - const FUTURES_BOUNDARY = 1 << 13; + const FUTURES_BOUNDARY = 1 << 14; /// `__future__`-style type annotations are enabled in this context. /// @@ -997,7 +1344,7 @@ bitflags! { /// def f(x: int) -> int: /// ... /// ``` - const FUTURE_ANNOTATIONS = 1 << 14; + const FUTURE_ANNOTATIONS = 1 << 15; } } @@ -1023,14 +1370,62 @@ pub struct Snapshot { #[derive(Debug)] pub enum ResolvedRead { /// The read reference is resolved to a specific binding. + /// + /// For example, given: + /// ```python + /// x = 1 + /// print(x) + /// ``` + /// + /// The `x` in `print(x)` is resolved to the binding of `x` in `x = 1`. Resolved(BindingId), + /// The read reference is resolved to a context-specific, implicit global (e.g., `__class__` /// within a class scope). + /// + /// For example, given: + /// ```python + /// class C: + /// print(__class__) + /// ``` + /// + /// The `__class__` in `print(__class__)` is resolved to the implicit global `__class__`. ImplicitGlobal, - /// The read reference is unresolved, but at least one of the containing scopes contains a star - /// import. - StarImport, + + /// The read reference is unresolved, but at least one of the containing scopes contains a + /// wildcard import. + /// + /// For example, given: + /// ```python + /// from x import * + /// + /// print(y) + /// ``` + /// + /// The `y` in `print(y)` is unresolved, but the containing scope contains a wildcard import, + /// so `y` _may_ be resolved to a symbol imported by the wildcard import. + WildcardImport, + + /// The read reference is resolved, but to an unbound local variable. + /// + /// For example, given: + /// ```python + /// x = 1 + /// del x + /// print(x) + /// ``` + /// + /// The `x` in `print(x)` is an unbound local. + UnboundLocal(BindingId), + /// The read reference is definitively unresolved. + /// + /// For example, given: + /// ```python + /// print(x) + /// ``` + /// + /// The `x` in `print(x)` is definitively unresolved. NotFound, } diff --git a/crates/ruff_python_semantic/src/node.rs b/crates/ruff_python_semantic/src/node.rs index dd71fd5f1d..6cdafd55b4 100644 --- a/crates/ruff_python_semantic/src/node.rs +++ b/crates/ruff_python_semantic/src/node.rs @@ -37,7 +37,7 @@ impl<'a> Nodes<'a> { /// Inserts a new node into the node tree and returns its unique id. /// /// Panics if a node with the same pointer already exists. - pub fn insert(&mut self, stmt: &'a Stmt, parent: Option) -> NodeId { + pub(crate) fn insert(&mut self, stmt: &'a Stmt, parent: Option) -> NodeId { let next_id = self.nodes.next_index(); if let Some(existing_id) = self.node_to_id.insert(RefEquality(stmt), next_id) { panic!("Node already exists with id {existing_id:?}"); @@ -61,23 +61,23 @@ impl<'a> Nodes<'a> { self.nodes[node_id].parent } - /// Return the depth of the node. - #[inline] - pub fn depth(&self, node_id: NodeId) -> u32 { - self.nodes[node_id].depth - } - - /// Returns an iterator over all [`NodeId`] ancestors, starting from the given [`NodeId`]. - pub fn ancestor_ids(&self, node_id: NodeId) -> impl Iterator + '_ { - std::iter::successors(Some(node_id), |&node_id| self.nodes[node_id].parent) - } - /// Return the parent of the given node. pub fn parent(&self, node: &'a Stmt) -> Option<&'a Stmt> { let node_id = self.node_to_id.get(&RefEquality(node))?; let parent_id = self.nodes[*node_id].parent?; Some(self[parent_id]) } + + /// Return the depth of the node. + #[inline] + pub(crate) fn depth(&self, node_id: NodeId) -> u32 { + self.nodes[node_id].depth + } + + /// Returns an iterator over all [`NodeId`] ancestors, starting from the given [`NodeId`]. + pub(crate) fn ancestor_ids(&self, node_id: NodeId) -> impl Iterator + '_ { + std::iter::successors(Some(node_id), |&node_id| self.nodes[node_id].parent) + } } impl<'a> Index for Nodes<'a> { diff --git a/crates/ruff_python_semantic/src/reference.rs b/crates/ruff_python_semantic/src/reference.rs index cac8bb8f39..d19b03194a 100644 --- a/crates/ruff_python_semantic/src/reference.rs +++ b/crates/ruff_python_semantic/src/reference.rs @@ -1,6 +1,7 @@ use ruff_text_size::TextRange; +use std::ops::Deref; -use ruff_index::{newtype_index, IndexVec}; +use ruff_index::{newtype_index, IndexSlice, IndexVec}; use crate::context::ExecutionContext; use crate::scope::ScopeId; @@ -35,11 +36,11 @@ pub struct ReferenceId; /// The references of a program indexed by [`ReferenceId`]. #[derive(Debug, Default)] -pub struct References(IndexVec); +pub(crate) struct References(IndexVec); impl References { - /// Pushes a new read reference and returns its unique id. - pub fn push( + /// Pushes a new [`Reference`] and returns its [`ReferenceId`]. + pub(crate) fn push( &mut self, scope_id: ScopeId, range: TextRange, @@ -51,9 +52,12 @@ impl References { context, }) } +} - /// Returns the [`Reference`] with the given id. - pub fn resolve(&self, id: ReferenceId) -> &Reference { - &self.0[id] +impl Deref for References { + type Target = IndexSlice; + + fn deref(&self) -> &Self::Target { + &self.0 } } diff --git a/crates/ruff_python_semantic/src/scope.rs b/crates/ruff_python_semantic/src/scope.rs index ba34332f22..ca5547f35a 100644 --- a/crates/ruff_python_semantic/src/scope.rs +++ b/crates/ruff_python_semantic/src/scope.rs @@ -8,26 +8,43 @@ use rustpython_parser::ast; use ruff_index::{newtype_index, Idx, IndexSlice, IndexVec}; -use crate::binding::{BindingId, StarImportation}; +use crate::binding::BindingId; use crate::globals::GlobalsId; +use crate::star_import::StarImport; #[derive(Debug)] pub struct Scope<'a> { + /// The kind of scope. pub kind: ScopeKind<'a>, + + /// The parent scope, if any. pub parent: Option, + /// A list of star imports in this scope. These represent _module_ imports (e.g., `sys` in /// `from sys import *`), rather than individual bindings (e.g., individual members in `sys`). - star_imports: Vec>, + star_imports: Vec>, + /// A map from bound name to binding ID. bindings: FxHashMap<&'a str, BindingId>, + /// A map from binding ID to bound name. binding_name: FxHashMap, + /// A map from binding ID to binding ID that it shadows. + /// + /// For example: + /// ```python + /// def f(): + /// x = 1 + /// x = 2 + /// ``` + /// + /// In this case, the binding created by `x = 2` shadows the binding created by `x = 1`. shadowed_bindings: HashMap>, - /// A list of all names that have been deleted in this scope. - deleted_symbols: Vec<&'a str>, + /// Index into the globals arena, if the scope contains any globally-declared symbols. globals_id: Option, + /// Flags for the [`Scope`]. flags: ScopeFlags, } @@ -41,7 +58,6 @@ impl<'a> Scope<'a> { bindings: FxHashMap::default(), binding_name: FxHashMap::default(), shadowed_bindings: IntMap::default(), - deleted_symbols: Vec::default(), globals_id: None, flags: ScopeFlags::empty(), } @@ -55,7 +71,6 @@ impl<'a> Scope<'a> { bindings: FxHashMap::default(), binding_name: FxHashMap::default(), shadowed_bindings: IntMap::default(), - deleted_symbols: Vec::default(), globals_id: None, flags: ScopeFlags::empty(), } @@ -82,45 +97,53 @@ impl<'a> Scope<'a> { } } - /// Removes the binding with the given name. - pub fn delete(&mut self, name: &'a str) -> Option { - self.deleted_symbols.push(name); - self.bindings.remove(name) - } - /// Returns `true` if this scope has a binding with the given name. pub fn has(&self, name: &str) -> bool { self.bindings.contains_key(name) } - /// Returns `true` if the scope declares a symbol with the given name. - /// - /// Unlike [`Scope::has`], the name may no longer be bound to a value (e.g., it could be - /// deleted). - pub fn declares(&self, name: &str) -> bool { - self.has(name) || self.deleted_symbols.contains(&name) - } - - /// Returns the ids of all bindings defined in this scope. + /// Returns the IDs of all bindings defined in this scope. pub fn binding_ids(&self) -> impl Iterator + '_ { self.bindings.values().copied() } - /// Returns a tuple of the name and id of all bindings defined in this scope. + /// Returns a tuple of the name and ID of all bindings defined in this scope. pub fn bindings(&self) -> impl Iterator + '_ { self.bindings.iter().map(|(&name, &id)| (name, id)) } - /// Returns an iterator over all [bindings](BindingId) bound to the given name, including + /// Like [`Scope::get`], but returns all bindings with the given name, including /// those that were shadowed by later bindings. - pub fn bindings_for_name(&self, name: &str) -> impl Iterator + '_ { + pub fn get_all(&self, name: &str) -> impl Iterator + '_ { std::iter::successors(self.bindings.get(name).copied(), |id| { self.shadowed_bindings.get(id).copied() }) } + /// Like [`Scope::binding_ids`], but returns all bindings that were added to the scope, + /// including those that were shadowed by later bindings. + pub fn all_binding_ids(&self) -> impl Iterator + '_ { + self.bindings.values().copied().flat_map(|id| { + std::iter::successors(Some(id), |id| self.shadowed_bindings.get(id).copied()) + }) + } + + /// Like [`Scope::bindings`], but returns all bindings added to the scope, including those that + /// were shadowed by later bindings. + pub fn all_bindings(&self) -> impl Iterator + '_ { + self.bindings.iter().flat_map(|(&name, &id)| { + std::iter::successors(Some(id), |id| self.shadowed_bindings.get(id).copied()) + .map(move |id| (name, id)) + }) + } + + /// Returns the ID of the binding that the given binding shadows, if any. + pub fn shadowed_binding(&self, id: BindingId) -> Option { + self.shadowed_bindings.get(&id).copied() + } + /// Adds a reference to a star import (e.g., `from sys import *`) to this scope. - pub fn add_star_import(&mut self, import: StarImportation<'a>) { + pub fn add_star_import(&mut self, import: StarImport<'a>) { self.star_imports.push(import); } @@ -130,17 +153,17 @@ impl<'a> Scope<'a> { } /// Returns an iterator over all star imports (e.g., `from sys import *`) in this scope. - pub fn star_imports(&self) -> impl Iterator> { + pub fn star_imports(&self) -> impl Iterator> { self.star_imports.iter() } /// Set the globals pointer for this scope. - pub fn set_globals_id(&mut self, globals: GlobalsId) { + pub(crate) fn set_globals_id(&mut self, globals: GlobalsId) { self.globals_id = Some(globals); } /// Returns the globals pointer for this scope. - pub fn globals_id(&self) -> Option { + pub(crate) fn globals_id(&self) -> Option { self.globals_id } @@ -207,17 +230,17 @@ pub struct Scopes<'a>(IndexVec>); impl<'a> Scopes<'a> { /// Returns a reference to the global scope - pub fn global(&self) -> &Scope<'a> { + pub(crate) fn global(&self) -> &Scope<'a> { &self[ScopeId::global()] } /// Returns a mutable reference to the global scope - pub fn global_mut(&mut self) -> &mut Scope<'a> { + pub(crate) fn global_mut(&mut self) -> &mut Scope<'a> { &mut self[ScopeId::global()] } /// Pushes a new scope and returns its unique id - pub fn push_scope(&mut self, kind: ScopeKind<'a>, parent: ScopeId) -> ScopeId { + pub(crate) fn push_scope(&mut self, kind: ScopeKind<'a>, parent: ScopeId) -> ScopeId { let next_id = ScopeId::new(self.0.len()); self.0.push(Scope::local(kind, parent)); next_id @@ -244,6 +267,7 @@ impl Default for Scopes<'_> { impl<'a> Deref for Scopes<'a> { type Target = IndexSlice>; + fn deref(&self) -> &Self::Target { &self.0 } diff --git a/crates/ruff_python_semantic/src/star_import.rs b/crates/ruff_python_semantic/src/star_import.rs new file mode 100644 index 0000000000..53055a53b8 --- /dev/null +++ b/crates/ruff_python_semantic/src/star_import.rs @@ -0,0 +1,7 @@ +#[derive(Debug, Clone)] +pub struct StarImport<'a> { + /// The level of the import. `None` or `Some(0)` indicate an absolute import. + pub level: Option, + /// The module being imported. `None` indicates a wildcard import. + pub module: Option<&'a str>, +} diff --git a/crates/ruff_python_stdlib/Cargo.toml b/crates/ruff_python_stdlib/Cargo.toml index c698a0adf5..167b9fad04 100644 --- a/crates/ruff_python_stdlib/Cargo.toml +++ b/crates/ruff_python_stdlib/Cargo.toml @@ -2,11 +2,14 @@ name = "ruff_python_stdlib" version = "0.0.0" publish = false +authors = { workspace = true } edition = { workspace = true } rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } [lib] [dependencies] -once_cell = { workspace = true } -rustc-hash = { workspace = true } diff --git a/crates/ruff_python_stdlib/src/future.rs b/crates/ruff_python_stdlib/src/future.rs index 0e7bbb3c0c..b1adaadedf 100644 --- a/crates/ruff_python_stdlib/src/future.rs +++ b/crates/ruff_python_stdlib/src/future.rs @@ -1,13 +1,17 @@ -/// A copy of `__future__.all_feature_names`. -pub const ALL_FEATURE_NAMES: &[&str] = &[ - "nested_scopes", - "generators", - "division", - "absolute_import", - "with_statement", - "print_function", - "unicode_literals", - "barry_as_FLUFL", - "generator_stop", - "annotations", -]; +/// Returns `true` if `name` is a valid `__future__` feature name, as defined by +/// `__future__.all_feature_names`. +pub fn is_feature_name(name: &str) -> bool { + matches!( + name, + "nested_scopes" + | "generators" + | "division" + | "absolute_import" + | "with_statement" + | "print_function" + | "unicode_literals" + | "barry_as_FLUFL" + | "generator_stop" + | "annotations" + ) +} diff --git a/crates/ruff_python_stdlib/src/identifiers.rs b/crates/ruff_python_stdlib/src/identifiers.rs index 2dca484a3c..169959bee2 100644 --- a/crates/ruff_python_stdlib/src/identifiers.rs +++ b/crates/ruff_python_stdlib/src/identifiers.rs @@ -1,4 +1,4 @@ -use crate::keyword::KWLIST; +use crate::keyword::is_keyword; /// Returns `true` if a string is a valid Python identifier (e.g., variable /// name). @@ -18,7 +18,7 @@ pub fn is_identifier(name: &str) -> bool { } // Is the identifier a keyword? - if KWLIST.contains(&name) { + if is_keyword(name) { return false; } @@ -52,7 +52,7 @@ pub fn is_module_name(name: &str) -> bool { } // Is the identifier a keyword? - if KWLIST.contains(&name) { + if is_keyword(name) { return false; } @@ -70,7 +70,7 @@ pub fn is_migration_name(name: &str) -> bool { } // Is the identifier a keyword? - if KWLIST.contains(&name) { + if is_keyword(name) { return false; } diff --git a/crates/ruff_python_stdlib/src/keyword.rs b/crates/ruff_python_stdlib/src/keyword.rs index ddaec03c80..7f361c0b69 100644 --- a/crates/ruff_python_stdlib/src/keyword.rs +++ b/crates/ruff_python_stdlib/src/keyword.rs @@ -1,7 +1,41 @@ // See: https://github.com/python/cpython/blob/9d692841691590c25e6cf5b2250a594d3bf54825/Lib/keyword.py#L18 -pub(crate) const KWLIST: [&str; 35] = [ - "False", "None", "True", "and", "as", "assert", "async", "await", "break", "class", "continue", - "def", "del", "elif", "else", "except", "finally", "for", "from", "global", "if", "import", - "in", "is", "lambda", "nonlocal", "not", "or", "pass", "raise", "return", "try", "while", - "with", "yield", -]; +pub(crate) fn is_keyword(name: &str) -> bool { + matches!( + name, + "False" + | "None" + | "True" + | "and" + | "as" + | "assert" + | "async" + | "await" + | "break" + | "class" + | "continue" + | "def" + | "del" + | "elif" + | "else" + | "except" + | "finally" + | "for" + | "from" + | "global" + | "if" + | "import" + | "in" + | "is" + | "lambda" + | "nonlocal" + | "not" + | "or" + | "pass" + | "raise" + | "return" + | "try" + | "while" + | "with" + | "yield", + ) +} diff --git a/crates/ruff_python_stdlib/src/path.rs b/crates/ruff_python_stdlib/src/path.rs index 733c18bb3f..cad9219687 100644 --- a/crates/ruff_python_stdlib/src/path.rs +++ b/crates/ruff_python_stdlib/src/path.rs @@ -17,11 +17,16 @@ pub fn is_python_stub_file(path: &Path) -> bool { path.extension().map_or(false, |ext| ext == "pyi") } +/// Return `true` if the [`Path`] appears to be that of a Jupyter notebook (`.ipynb`). +pub fn is_jupyter_notebook(path: &Path) -> bool { + path.extension().map_or(false, |ext| ext == "ipynb") +} + #[cfg(test)] mod tests { use std::path::Path; - use crate::path::is_python_file; + use crate::path::{is_jupyter_notebook, is_python_file}; #[test] fn inclusions() { @@ -37,4 +42,13 @@ mod tests { let path = Path::new("foo/bar/baz"); assert!(!is_python_file(path)); } + + #[test] + fn test_is_jupyter_notebook() { + let path = Path::new("foo/bar/baz.ipynb"); + assert!(is_jupyter_notebook(path)); + + let path = Path::new("foo/bar/baz.py"); + assert!(!is_jupyter_notebook(path)); + } } diff --git a/crates/ruff_python_stdlib/src/str.rs b/crates/ruff_python_stdlib/src/str.rs index 1c1e6ffae7..2b7b90b64b 100644 --- a/crates/ruff_python_stdlib/src/str.rs +++ b/crates/ruff_python_stdlib/src/str.rs @@ -1,14 +1,67 @@ -/// See: -pub const TRIPLE_QUOTE_PREFIXES: &[&str] = &[ - "u\"\"\"", "u'''", "r\"\"\"", "r'''", "U\"\"\"", "U'''", "R\"\"\"", "R'''", "\"\"\"", "'''", -]; -pub const SINGLE_QUOTE_PREFIXES: &[&str] = &[ - "u\"", "u'", "r\"", "r'", "U\"", "U'", "R\"", "R'", "\"", "'", -]; -pub const TRIPLE_QUOTE_SUFFIXES: &[&str] = &["\"\"\"", "'''"]; -pub const SINGLE_QUOTE_SUFFIXES: &[&str] = &["\"", "'"]; +/// Return `true` if a string is lowercase. +/// +/// A string is lowercase if all alphabetic characters in the string are lowercase. +/// +/// ## Examples +/// +/// ```rust +/// use ruff_python_stdlib::str::is_lowercase; +/// +/// assert!(is_lowercase("abc")); +/// assert!(is_lowercase("a_b_c")); +/// assert!(is_lowercase("a2c")); +/// assert!(!is_lowercase("aBc")); +/// assert!(!is_lowercase("ABC")); +/// assert!(is_lowercase("")); +/// assert!(is_lowercase("_")); +/// ``` +pub fn is_lowercase(s: &str) -> bool { + s.chars().all(|c| !c.is_alphabetic() || c.is_lowercase()) +} -pub fn is_lower(s: &str) -> bool { +/// Return `true` if a string is uppercase. +/// +/// A string is uppercase if all alphabetic characters in the string are uppercase. +/// +/// ## Examples +/// +/// ```rust +/// use ruff_python_stdlib::str::is_uppercase; +/// +/// assert!(is_uppercase("ABC")); +/// assert!(is_uppercase("A_B_C")); +/// assert!(is_uppercase("A2C")); +/// assert!(!is_uppercase("aBc")); +/// assert!(!is_uppercase("abc")); +/// assert!(is_uppercase("")); +/// assert!(is_uppercase("_")); +/// ``` +pub fn is_uppercase(s: &str) -> bool { + s.chars().all(|c| !c.is_alphabetic() || c.is_uppercase()) +} + +/// Return `true` if a string is _cased_ as lowercase. +/// +/// A string is cased as lowercase if it contains at least one lowercase character and no uppercase +/// characters. +/// +/// This differs from `str::is_lowercase` in that it returns `false` for empty strings and strings +/// that contain only underscores or other non-alphabetic characters. +/// +/// ## Examples +/// +/// ```rust +/// use ruff_python_stdlib::str::is_cased_lowercase; +/// +/// assert!(is_cased_lowercase("abc")); +/// assert!(is_cased_lowercase("a_b_c")); +/// assert!(is_cased_lowercase("a2c")); +/// assert!(!is_cased_lowercase("aBc")); +/// assert!(!is_cased_lowercase("ABC")); +/// assert!(!is_cased_lowercase("")); +/// assert!(!is_cased_lowercase("_")); +/// ``` +pub fn is_cased_lowercase(s: &str) -> bool { let mut cased = false; for c in s.chars() { if c.is_uppercase() { @@ -20,7 +73,28 @@ pub fn is_lower(s: &str) -> bool { cased } -pub fn is_upper(s: &str) -> bool { +/// Return `true` if a string is _cased_ as uppercase. +/// +/// A string is cased as uppercase if it contains at least one uppercase character and no lowercase +/// characters. +/// +/// This differs from `str::is_uppercase` in that it returns `false` for empty strings and strings +/// that contain only underscores or other non-alphabetic characters. +/// +/// ## Examples +/// +/// ```rust +/// use ruff_python_stdlib::str::is_cased_uppercase; +/// +/// assert!(is_cased_uppercase("ABC")); +/// assert!(is_cased_uppercase("A_B_C")); +/// assert!(is_cased_uppercase("A2C")); +/// assert!(!is_cased_uppercase("aBc")); +/// assert!(!is_cased_uppercase("abc")); +/// assert!(!is_cased_uppercase("")); +/// assert!(!is_cased_uppercase("_")); +/// ``` +pub fn is_cased_uppercase(s: &str) -> bool { let mut cased = false; for c in s.chars() { if c.is_lowercase() { @@ -31,30 +105,3 @@ pub fn is_upper(s: &str) -> bool { } cased } - -#[cfg(test)] -mod tests { - use crate::str::{is_lower, is_upper}; - - #[test] - fn test_is_lower() { - assert!(is_lower("abc")); - assert!(is_lower("a_b_c")); - assert!(is_lower("a2c")); - assert!(!is_lower("aBc")); - assert!(!is_lower("ABC")); - assert!(!is_lower("")); - assert!(!is_lower("_")); - } - - #[test] - fn test_is_upper() { - assert!(is_upper("ABC")); - assert!(is_upper("A_B_C")); - assert!(is_upper("A2C")); - assert!(!is_upper("aBc")); - assert!(!is_upper("abc")); - assert!(!is_upper("")); - assert!(!is_upper("_")); - } -} diff --git a/crates/ruff_python_stdlib/src/sys.rs b/crates/ruff_python_stdlib/src/sys.rs index 791b7db2d9..c6d28db289 100644 --- a/crates/ruff_python_stdlib/src/sys.rs +++ b/crates/ruff_python_stdlib/src/sys.rs @@ -1,1107 +1,276 @@ //! This file is generated by `scripts/generate_known_standard_library.py` -use once_cell::sync::Lazy; -use rustc_hash::{FxHashMap, FxHashSet}; -// See: https://pycqa.github.io/isort/docs/configuration/options.html#known-standard-library -pub static KNOWN_STANDARD_LIBRARY: Lazy>> = - Lazy::new(|| { - FxHashMap::from_iter([ - ( - (3, 7), - FxHashSet::from_iter([ - "_ast", - "_dummy_thread", - "_thread", - "abc", - "aifc", - "argparse", - "array", - "ast", - "asynchat", - "asyncio", - "asyncore", - "atexit", - "audioop", - "base64", - "bdb", - "binascii", - "binhex", - "bisect", - "builtins", - "bz2", - "cProfile", - "calendar", - "cgi", - "cgitb", - "chunk", - "cmath", - "cmd", - "code", - "codecs", - "codeop", - "collections", - "colorsys", - "compileall", - "concurrent", - "configparser", - "contextlib", - "contextvars", - "copy", - "copyreg", - "crypt", - "csv", - "ctypes", - "curses", - "dataclasses", - "datetime", - "dbm", - "decimal", - "difflib", - "dis", - "distutils", - "doctest", - "dummy_threading", - "email", - "encodings", - "ensurepip", - "enum", - "errno", - "faulthandler", - "fcntl", - "filecmp", - "fileinput", - "fnmatch", - "formatter", - "fractions", - "ftplib", - "functools", - "gc", - "getopt", - "getpass", - "gettext", - "glob", - "grp", - "gzip", - "hashlib", - "heapq", - "hmac", - "html", - "http", - "imaplib", - "imghdr", - "imp", - "importlib", - "inspect", - "io", - "ipaddress", - "itertools", - "json", - "keyword", - "lib2to3", - "linecache", - "locale", - "logging", - "lzma", - "macpath", - "mailbox", - "mailcap", - "marshal", - "math", - "mimetypes", - "mmap", - "modulefinder", - "msilib", - "msvcrt", - "multiprocessing", - "netrc", - "nis", - "nntplib", - "ntpath", - "numbers", - "operator", - "optparse", - "os", - "ossaudiodev", - "parser", - "pathlib", - "pdb", - "pickle", - "pickletools", - "pipes", - "pkgutil", - "platform", - "plistlib", - "poplib", - "posix", - "posixpath", - "pprint", - "profile", - "pstats", - "pty", - "pwd", - "py_compile", - "pyclbr", - "pydoc", - "queue", - "quopri", - "random", - "re", - "readline", - "reprlib", - "resource", - "rlcompleter", - "runpy", - "sched", - "secrets", - "select", - "selectors", - "shelve", - "shlex", - "shutil", - "signal", - "site", - "smtpd", - "smtplib", - "sndhdr", - "socket", - "socketserver", - "spwd", - "sqlite3", - "sre", - "sre_compile", - "sre_constants", - "sre_parse", - "ssl", - "stat", - "statistics", - "string", - "stringprep", - "struct", - "subprocess", - "sunau", - "symbol", - "symtable", - "sys", - "sysconfig", - "syslog", - "tabnanny", - "tarfile", - "telnetlib", - "tempfile", - "termios", - "test", - "textwrap", - "threading", - "time", - "timeit", - "tkinter", - "token", - "tokenize", - "trace", - "traceback", - "tracemalloc", - "tty", - "turtle", - "turtledemo", - "types", - "typing", - "unicodedata", - "unittest", - "urllib", - "uu", - "uuid", - "venv", - "warnings", - "wave", - "weakref", - "webbrowser", - "winreg", - "winsound", - "wsgiref", - "xdrlib", - "xml", - "xmlrpc", - "zipapp", - "zipfile", - "zipimport", - "zlib", - ]), - ), - ( - (3, 8), - FxHashSet::from_iter([ - "_ast", - "_dummy_thread", - "_thread", - "abc", - "aifc", - "argparse", - "array", - "ast", - "asynchat", - "asyncio", - "asyncore", - "atexit", - "audioop", - "base64", - "bdb", - "binascii", - "binhex", - "bisect", - "builtins", - "bz2", - "cProfile", - "calendar", - "cgi", - "cgitb", - "chunk", - "cmath", - "cmd", - "code", - "codecs", - "codeop", - "collections", - "colorsys", - "compileall", - "concurrent", - "configparser", - "contextlib", - "contextvars", - "copy", - "copyreg", - "crypt", - "csv", - "ctypes", - "curses", - "dataclasses", - "datetime", - "dbm", - "decimal", - "difflib", - "dis", - "distutils", - "doctest", - "dummy_threading", - "email", - "encodings", - "ensurepip", - "enum", - "errno", - "faulthandler", - "fcntl", - "filecmp", - "fileinput", - "fnmatch", - "formatter", - "fractions", - "ftplib", - "functools", - "gc", - "getopt", - "getpass", - "gettext", - "glob", - "grp", - "gzip", - "hashlib", - "heapq", - "hmac", - "html", - "http", - "imaplib", - "imghdr", - "imp", - "importlib", - "inspect", - "io", - "ipaddress", - "itertools", - "json", - "keyword", - "lib2to3", - "linecache", - "locale", - "logging", - "lzma", - "mailbox", - "mailcap", - "marshal", - "math", - "mimetypes", - "mmap", - "modulefinder", - "msilib", - "msvcrt", - "multiprocessing", - "netrc", - "nis", - "nntplib", - "ntpath", - "numbers", - "operator", - "optparse", - "os", - "ossaudiodev", - "parser", - "pathlib", - "pdb", - "pickle", - "pickletools", - "pipes", - "pkgutil", - "platform", - "plistlib", - "poplib", - "posix", - "posixpath", - "pprint", - "profile", - "pstats", - "pty", - "pwd", - "py_compile", - "pyclbr", - "pydoc", - "queue", - "quopri", - "random", - "re", - "readline", - "reprlib", - "resource", - "rlcompleter", - "runpy", - "sched", - "secrets", - "select", - "selectors", - "shelve", - "shlex", - "shutil", - "signal", - "site", - "smtpd", - "smtplib", - "sndhdr", - "socket", - "socketserver", - "spwd", - "sqlite3", - "sre", - "sre_compile", - "sre_constants", - "sre_parse", - "ssl", - "stat", - "statistics", - "string", - "stringprep", - "struct", - "subprocess", - "sunau", - "symbol", - "symtable", - "sys", - "sysconfig", - "syslog", - "tabnanny", - "tarfile", - "telnetlib", - "tempfile", - "termios", - "test", - "textwrap", - "threading", - "time", - "timeit", - "tkinter", - "token", - "tokenize", - "trace", - "traceback", - "tracemalloc", - "tty", - "turtle", - "turtledemo", - "types", - "typing", - "unicodedata", - "unittest", - "urllib", - "uu", - "uuid", - "venv", - "warnings", - "wave", - "weakref", - "webbrowser", - "winreg", - "winsound", - "wsgiref", - "xdrlib", - "xml", - "xmlrpc", - "zipapp", - "zipfile", - "zipimport", - "zlib", - ]), - ), - ( - (3, 9), - FxHashSet::from_iter([ - "_ast", - "_thread", - "abc", - "aifc", - "argparse", - "array", - "ast", - "asynchat", - "asyncio", - "asyncore", - "atexit", - "audioop", - "base64", - "bdb", - "binascii", - "binhex", - "bisect", - "builtins", - "bz2", - "cProfile", - "calendar", - "cgi", - "cgitb", - "chunk", - "cmath", - "cmd", - "code", - "codecs", - "codeop", - "collections", - "colorsys", - "compileall", - "concurrent", - "configparser", - "contextlib", - "contextvars", - "copy", - "copyreg", - "crypt", - "csv", - "ctypes", - "curses", - "dataclasses", - "datetime", - "dbm", - "decimal", - "difflib", - "dis", - "distutils", - "doctest", - "email", - "encodings", - "ensurepip", - "enum", - "errno", - "faulthandler", - "fcntl", - "filecmp", - "fileinput", - "fnmatch", - "formatter", - "fractions", - "ftplib", - "functools", - "gc", - "getopt", - "getpass", - "gettext", - "glob", - "graphlib", - "grp", - "gzip", - "hashlib", - "heapq", - "hmac", - "html", - "http", - "imaplib", - "imghdr", - "imp", - "importlib", - "inspect", - "io", - "ipaddress", - "itertools", - "json", - "keyword", - "lib2to3", - "linecache", - "locale", - "logging", - "lzma", - "mailbox", - "mailcap", - "marshal", - "math", - "mimetypes", - "mmap", - "modulefinder", - "msilib", - "msvcrt", - "multiprocessing", - "netrc", - "nis", - "nntplib", - "ntpath", - "numbers", - "operator", - "optparse", - "os", - "ossaudiodev", - "parser", - "pathlib", - "pdb", - "pickle", - "pickletools", - "pipes", - "pkgutil", - "platform", - "plistlib", - "poplib", - "posix", - "posixpath", - "pprint", - "profile", - "pstats", - "pty", - "pwd", - "py_compile", - "pyclbr", - "pydoc", - "queue", - "quopri", - "random", - "re", - "readline", - "reprlib", - "resource", - "rlcompleter", - "runpy", - "sched", - "secrets", - "select", - "selectors", - "shelve", - "shlex", - "shutil", - "signal", - "site", - "smtpd", - "smtplib", - "sndhdr", - "socket", - "socketserver", - "spwd", - "sqlite3", - "sre", - "sre_compile", - "sre_constants", - "sre_parse", - "ssl", - "stat", - "statistics", - "string", - "stringprep", - "struct", - "subprocess", - "sunau", - "symbol", - "symtable", - "sys", - "sysconfig", - "syslog", - "tabnanny", - "tarfile", - "telnetlib", - "tempfile", - "termios", - "test", - "textwrap", - "threading", - "time", - "timeit", - "tkinter", - "token", - "tokenize", - "trace", - "traceback", - "tracemalloc", - "tty", - "turtle", - "turtledemo", - "types", - "typing", - "unicodedata", - "unittest", - "urllib", - "uu", - "uuid", - "venv", - "warnings", - "wave", - "weakref", - "webbrowser", - "winreg", - "winsound", - "wsgiref", - "xdrlib", - "xml", - "xmlrpc", - "zipapp", - "zipfile", - "zipimport", - "zlib", - "zoneinfo", - ]), - ), - ( - (3, 10), - FxHashSet::from_iter([ - "_ast", - "_thread", - "abc", - "aifc", - "argparse", - "array", - "ast", - "asynchat", - "asyncio", - "asyncore", - "atexit", - "audioop", - "base64", - "bdb", - "binascii", - "binhex", - "bisect", - "builtins", - "bz2", - "cProfile", - "calendar", - "cgi", - "cgitb", - "chunk", - "cmath", - "cmd", - "code", - "codecs", - "codeop", - "collections", - "colorsys", - "compileall", - "concurrent", - "configparser", - "contextlib", - "contextvars", - "copy", - "copyreg", - "crypt", - "csv", - "ctypes", - "curses", - "dataclasses", - "datetime", - "dbm", - "decimal", - "difflib", - "dis", - "distutils", - "doctest", - "email", - "encodings", - "ensurepip", - "enum", - "errno", - "faulthandler", - "fcntl", - "filecmp", - "fileinput", - "fnmatch", - "fractions", - "ftplib", - "functools", - "gc", - "getopt", - "getpass", - "gettext", - "glob", - "graphlib", - "grp", - "gzip", - "hashlib", - "heapq", - "hmac", - "html", - "http", - "idlelib", - "imaplib", - "imghdr", - "imp", - "importlib", - "inspect", - "io", - "ipaddress", - "itertools", - "json", - "keyword", - "lib2to3", - "linecache", - "locale", - "logging", - "lzma", - "mailbox", - "mailcap", - "marshal", - "math", - "mimetypes", - "mmap", - "modulefinder", - "msilib", - "msvcrt", - "multiprocessing", - "netrc", - "nis", - "nntplib", - "ntpath", - "numbers", - "operator", - "optparse", - "os", - "ossaudiodev", - "pathlib", - "pdb", - "pickle", - "pickletools", - "pipes", - "pkgutil", - "platform", - "plistlib", - "poplib", - "posix", - "posixpath", - "pprint", - "profile", - "pstats", - "pty", - "pwd", - "py_compile", - "pyclbr", - "pydoc", - "queue", - "quopri", - "random", - "re", - "readline", - "reprlib", - "resource", - "rlcompleter", - "runpy", - "sched", - "secrets", - "select", - "selectors", - "shelve", - "shlex", - "shutil", - "signal", - "site", - "smtpd", - "smtplib", - "sndhdr", - "socket", - "socketserver", - "spwd", - "sqlite3", - "sre", - "sre_compile", - "sre_constants", - "sre_parse", - "ssl", - "stat", - "statistics", - "string", - "stringprep", - "struct", - "subprocess", - "sunau", - "symtable", - "sys", - "sysconfig", - "syslog", - "tabnanny", - "tarfile", - "telnetlib", - "tempfile", - "termios", - "test", - "textwrap", - "threading", - "time", - "timeit", - "tkinter", - "token", - "tokenize", - "trace", - "traceback", - "tracemalloc", - "tty", - "turtle", - "turtledemo", - "types", - "typing", - "unicodedata", - "unittest", - "urllib", - "uu", - "uuid", - "venv", - "warnings", - "wave", - "weakref", - "webbrowser", - "winreg", - "winsound", - "wsgiref", - "xdrlib", - "xml", - "xmlrpc", - "zipapp", - "zipfile", - "zipimport", - "zlib", - "zoneinfo", - ]), - ), - ( - (3, 11), - FxHashSet::from_iter([ - "_ast", - "_thread", - "abc", - "aifc", - "argparse", - "array", - "ast", - "asynchat", - "asyncio", - "asyncore", - "atexit", - "audioop", - "base64", - "bdb", - "binascii", - "bisect", - "builtins", - "bz2", - "cProfile", - "calendar", - "cgi", - "cgitb", - "chunk", - "cmath", - "cmd", - "code", - "codecs", - "codeop", - "collections", - "colorsys", - "compileall", - "concurrent", - "configparser", - "contextlib", - "contextvars", - "copy", - "copyreg", - "crypt", - "csv", - "ctypes", - "curses", - "dataclasses", - "datetime", - "dbm", - "decimal", - "difflib", - "dis", - "distutils", - "doctest", - "email", - "encodings", - "ensurepip", - "enum", - "errno", - "faulthandler", - "fcntl", - "filecmp", - "fileinput", - "fnmatch", - "fractions", - "ftplib", - "functools", - "gc", - "getopt", - "getpass", - "gettext", - "glob", - "graphlib", - "grp", - "gzip", - "hashlib", - "heapq", - "hmac", - "html", - "http", - "idlelib", - "imaplib", - "imghdr", - "imp", - "importlib", - "inspect", - "io", - "ipaddress", - "itertools", - "json", - "keyword", - "lib2to3", - "linecache", - "locale", - "logging", - "lzma", - "mailbox", - "mailcap", - "marshal", - "math", - "mimetypes", - "mmap", - "modulefinder", - "msilib", - "msvcrt", - "multiprocessing", - "netrc", - "nis", - "nntplib", - "ntpath", - "numbers", - "operator", - "optparse", - "os", - "ossaudiodev", - "pathlib", - "pdb", - "pickle", - "pickletools", - "pipes", - "pkgutil", - "platform", - "plistlib", - "poplib", - "posix", - "posixpath", - "pprint", - "profile", - "pstats", - "pty", - "pwd", - "py_compile", - "pyclbr", - "pydoc", - "queue", - "quopri", - "random", - "re", - "readline", - "reprlib", - "resource", - "rlcompleter", - "runpy", - "sched", - "secrets", - "select", - "selectors", - "shelve", - "shlex", - "shutil", - "signal", - "site", - "smtpd", - "smtplib", - "sndhdr", - "socket", - "socketserver", - "spwd", - "sqlite3", - "sre", - "sre_compile", - "sre_constants", - "sre_parse", - "ssl", - "stat", - "statistics", - "string", - "stringprep", - "struct", - "subprocess", - "sunau", - "symtable", - "sys", - "sysconfig", - "syslog", - "tabnanny", - "tarfile", - "telnetlib", - "tempfile", - "termios", - "test", - "textwrap", - "threading", - "time", - "timeit", - "tkinter", - "token", - "tokenize", - "tomllib", - "trace", - "traceback", - "tracemalloc", - "tty", - "turtle", - "turtledemo", - "types", - "typing", - "unicodedata", - "unittest", - "urllib", - "uu", - "uuid", - "venv", - "warnings", - "wave", - "weakref", - "webbrowser", - "winreg", - "winsound", - "wsgiref", - "xdrlib", - "xml", - "xmlrpc", - "zipapp", - "zipfile", - "zipimport", - "zlib", - "zoneinfo", - ]), - ), - ]) - }); +pub fn is_known_standard_library(minor_version: u32, module: &str) -> bool { + matches!( + (minor_version, module), + ( + _, + "_ast" + | "_thread" + | "abc" + | "aifc" + | "argparse" + | "array" + | "ast" + | "asyncio" + | "atexit" + | "audioop" + | "base64" + | "bdb" + | "binascii" + | "bisect" + | "builtins" + | "bz2" + | "cProfile" + | "calendar" + | "cgi" + | "cgitb" + | "chunk" + | "cmath" + | "cmd" + | "code" + | "codecs" + | "codeop" + | "collections" + | "colorsys" + | "compileall" + | "concurrent" + | "configparser" + | "contextlib" + | "contextvars" + | "copy" + | "copyreg" + | "crypt" + | "csv" + | "ctypes" + | "curses" + | "dataclasses" + | "datetime" + | "dbm" + | "decimal" + | "difflib" + | "dis" + | "doctest" + | "email" + | "encodings" + | "ensurepip" + | "enum" + | "errno" + | "faulthandler" + | "fcntl" + | "filecmp" + | "fileinput" + | "fnmatch" + | "fractions" + | "ftplib" + | "functools" + | "gc" + | "getopt" + | "getpass" + | "gettext" + | "glob" + | "grp" + | "gzip" + | "hashlib" + | "heapq" + | "hmac" + | "html" + | "http" + | "imaplib" + | "imghdr" + | "importlib" + | "inspect" + | "io" + | "ipaddress" + | "itertools" + | "json" + | "keyword" + | "lib2to3" + | "linecache" + | "locale" + | "logging" + | "lzma" + | "mailbox" + | "mailcap" + | "marshal" + | "math" + | "mimetypes" + | "mmap" + | "modulefinder" + | "msilib" + | "msvcrt" + | "multiprocessing" + | "netrc" + | "nis" + | "nntplib" + | "ntpath" + | "numbers" + | "operator" + | "optparse" + | "os" + | "ossaudiodev" + | "pathlib" + | "pdb" + | "pickle" + | "pickletools" + | "pipes" + | "pkgutil" + | "platform" + | "plistlib" + | "poplib" + | "posix" + | "posixpath" + | "pprint" + | "profile" + | "pstats" + | "pty" + | "pwd" + | "py_compile" + | "pyclbr" + | "pydoc" + | "queue" + | "quopri" + | "random" + | "re" + | "readline" + | "reprlib" + | "resource" + | "rlcompleter" + | "runpy" + | "sched" + | "secrets" + | "select" + | "selectors" + | "shelve" + | "shlex" + | "shutil" + | "signal" + | "site" + | "smtplib" + | "sndhdr" + | "socket" + | "socketserver" + | "spwd" + | "sqlite3" + | "sre" + | "sre_compile" + | "sre_constants" + | "sre_parse" + | "ssl" + | "stat" + | "statistics" + | "string" + | "stringprep" + | "struct" + | "subprocess" + | "sunau" + | "symtable" + | "sys" + | "sysconfig" + | "syslog" + | "tabnanny" + | "tarfile" + | "telnetlib" + | "tempfile" + | "termios" + | "test" + | "textwrap" + | "threading" + | "time" + | "timeit" + | "tkinter" + | "token" + | "tokenize" + | "trace" + | "traceback" + | "tracemalloc" + | "tty" + | "turtle" + | "turtledemo" + | "types" + | "typing" + | "unicodedata" + | "unittest" + | "urllib" + | "uu" + | "uuid" + | "venv" + | "warnings" + | "wave" + | "weakref" + | "webbrowser" + | "winreg" + | "winsound" + | "wsgiref" + | "xdrlib" + | "xml" + | "xmlrpc" + | "zipapp" + | "zipfile" + | "zipimport" + | "zlib" + ) | ( + 7, + "_dummy_thread" + | "asynchat" + | "asyncore" + | "binhex" + | "distutils" + | "dummy_threading" + | "formatter" + | "imp" + | "macpath" + | "parser" + | "smtpd" + | "symbol" + ) | ( + 8, + "_dummy_thread" + | "asynchat" + | "asyncore" + | "binhex" + | "distutils" + | "dummy_threading" + | "formatter" + | "imp" + | "parser" + | "smtpd" + | "symbol" + ) | ( + 9, + "asynchat" + | "asyncore" + | "binhex" + | "distutils" + | "formatter" + | "graphlib" + | "imp" + | "parser" + | "smtpd" + | "symbol" + | "zoneinfo" + ) | ( + 10, + "asynchat" + | "asyncore" + | "binhex" + | "distutils" + | "graphlib" + | "idlelib" + | "imp" + | "smtpd" + | "zoneinfo" + ) | ( + 11, + "asynchat" + | "asyncore" + | "distutils" + | "graphlib" + | "idlelib" + | "imp" + | "smtpd" + | "tomllib" + | "zoneinfo" + ) | (12, "graphlib" | "idlelib" | "tomllib" | "zoneinfo") + ) +} diff --git a/crates/ruff_python_stdlib/src/typing.rs b/crates/ruff_python_stdlib/src/typing.rs index 48895413f6..796f7c3a07 100644 --- a/crates/ruff_python_stdlib/src/typing.rs +++ b/crates/ruff_python_stdlib/src/typing.rs @@ -1,279 +1,414 @@ -use once_cell::sync::Lazy; -use rustc_hash::{FxHashMap, FxHashSet}; +/// Returns `true` if a name is a member of Python's `typing_extensions` module. +/// +/// See: +pub fn is_typing_extension(member: &str) -> bool { + matches!( + member, + "Annotated" + | "Any" + | "AsyncContextManager" + | "AsyncGenerator" + | "AsyncIterable" + | "AsyncIterator" + | "Awaitable" + | "ChainMap" + | "ClassVar" + | "Concatenate" + | "ContextManager" + | "Coroutine" + | "Counter" + | "DefaultDict" + | "Deque" + | "Final" + | "Literal" + | "LiteralString" + | "NamedTuple" + | "Never" + | "NewType" + | "NotRequired" + | "OrderedDict" + | "ParamSpec" + | "ParamSpecArgs" + | "ParamSpecKwargs" + | "Protocol" + | "Required" + | "Self" + | "TYPE_CHECKING" + | "Text" + | "Type" + | "TypeAlias" + | "TypeGuard" + | "TypeVar" + | "TypeVarTuple" + | "TypedDict" + | "Unpack" + | "assert_never" + | "assert_type" + | "clear_overloads" + | "final" + | "get_type_hints" + | "get_args" + | "get_origin" + | "get_overloads" + | "is_typeddict" + | "overload" + | "override" + | "reveal_type" + | "runtime_checkable" + ) +} -// See: https://pypi.org/project/typing-extensions/ -pub static TYPING_EXTENSIONS: Lazy> = Lazy::new(|| { - FxHashSet::from_iter([ - "Annotated", - "Any", - "AsyncContextManager", - "AsyncGenerator", - "AsyncIterable", - "AsyncIterator", - "Awaitable", - "ChainMap", - "ClassVar", - "Concatenate", - "ContextManager", - "Coroutine", - "Counter", - "DefaultDict", - "Deque", - "Final", - "Literal", - "LiteralString", - "NamedTuple", - "Never", - "NewType", - "NotRequired", - "OrderedDict", - "ParamSpec", - "ParamSpecArgs", - "ParamSpecKwargs", - "Protocol", - "Required", - "Self", - "TYPE_CHECKING", - "Text", - "Type", - "TypeAlias", - "TypeGuard", - "TypeVar", - "TypeVarTuple", - "TypedDict", - "Unpack", - "assert_never", - "assert_type", - "clear_overloads", - "final", - "get_type_hints", - "get_args", - "get_origin", - "get_overloads", - "is_typeddict", - "overload", - "override", - "reveal_type", - "runtime_checkable", - ]) -}); +/// Returns `true` if a call path is a generic from the Python standard library (e.g. `list`, which +/// can be used as `list[int]`). +/// +/// See: +pub fn is_standard_library_generic(call_path: &[&str]) -> bool { + matches!( + call_path, + ["", "dict" | "frozenset" | "list" | "set" | "tuple" | "type"] + | [ + "collections" | "typing" | "typing_extensions", + "ChainMap" | "Counter" + ] + | ["collections" | "typing", "OrderedDict"] + | ["collections", "defaultdict" | "deque"] + | [ + "collections", + "abc", + "AsyncGenerator" + | "AsyncIterable" + | "AsyncIterator" + | "Awaitable" + | "ByteString" + | "Callable" + | "Collection" + | "Container" + | "Coroutine" + | "Generator" + | "ItemsView" + | "Iterable" + | "Iterator" + | "KeysView" + | "Mapping" + | "MappingView" + | "MutableMapping" + | "MutableSequence" + | "MutableSet" + | "Reversible" + | "Sequence" + | "Set" + | "ValuesView" + ] + | [ + "contextlib", + "AbstractAsyncContextManager" | "AbstractContextManager" + ] + | ["re" | "typing", "Match" | "Pattern"] + | [ + "typing", + "AbstractSet" + | "AsyncContextManager" + | "AsyncGenerator" + | "AsyncIterator" + | "Awaitable" + | "BinaryIO" + | "ByteString" + | "Callable" + | "ClassVar" + | "Collection" + | "Concatenate" + | "Container" + | "ContextManager" + | "Coroutine" + | "DefaultDict" + | "Deque" + | "Dict" + | "Final" + | "FrozenSet" + | "Generator" + | "Generic" + | "IO" + | "ItemsView" + | "Iterable" + | "Iterator" + | "KeysView" + | "List" + | "Mapping" + | "MutableMapping" + | "MutableSequence" + | "MutableSet" + | "Optional" + | "Reversible" + | "Sequence" + | "Set" + | "TextIO" + | "Tuple" + | "Type" + | "TypeGuard" + | "Union" + | "Unpack" + | "ValuesView" + ] + | ["typing", "io", "BinaryIO" | "IO" | "TextIO"] + | ["typing", "re", "Match" | "Pattern"] + | [ + "typing_extensions", + "AsyncContextManager" + | "AsyncGenerator" + | "AsyncIterable" + | "AsyncIterator" + | "Awaitable" + | "ClassVar" + | "Concatenate" + | "ContextManager" + | "Coroutine" + | "DefaultDict" + | "Deque" + | "Type" + ] + | [ + "weakref", + "WeakKeyDictionary" | "WeakSet" | "WeakValueDictionary" + ] + ) +} -// See: https://docs.python.org/3/library/typing.html -pub const SUBSCRIPTS: &[&[&str]] = &[ - // builtins - &["", "dict"], - &["", "frozenset"], - &["", "list"], - &["", "set"], - &["", "tuple"], - &["", "type"], - // `collections` - &["collections", "ChainMap"], - &["collections", "Counter"], - &["collections", "OrderedDict"], - &["collections", "defaultdict"], - &["collections", "deque"], - // `collections.abc` - &["collections", "abc", "AsyncGenerator"], - &["collections", "abc", "AsyncIterable"], - &["collections", "abc", "AsyncIterator"], - &["collections", "abc", "Awaitable"], - &["collections", "abc", "ByteString"], - &["collections", "abc", "Callable"], - &["collections", "abc", "Collection"], - &["collections", "abc", "Container"], - &["collections", "abc", "Coroutine"], - &["collections", "abc", "Generator"], - &["collections", "abc", "ItemsView"], - &["collections", "abc", "Iterable"], - &["collections", "abc", "Iterator"], - &["collections", "abc", "KeysView"], - &["collections", "abc", "Mapping"], - &["collections", "abc", "MappingView"], - &["collections", "abc", "MutableMapping"], - &["collections", "abc", "MutableSequence"], - &["collections", "abc", "MutableSet"], - &["collections", "abc", "Reversible"], - &["collections", "abc", "Sequence"], - &["collections", "abc", "Set"], - &["collections", "abc", "ValuesView"], - // `contextlib` - &["contextlib", "AbstractAsyncContextManager"], - &["contextlib", "AbstractContextManager"], - // `re` - &["re", "Match"], - &["re", "Pattern"], - // `typing` - &["typing", "AbstractSet"], - &["typing", "AsyncContextManager"], - &["typing", "AsyncGenerator"], - &["typing", "AsyncIterator"], - &["typing", "Awaitable"], - &["typing", "BinaryIO"], - &["typing", "ByteString"], - &["typing", "Callable"], - &["typing", "ChainMap"], - &["typing", "ClassVar"], - &["typing", "Collection"], - &["typing", "Concatenate"], - &["typing", "Container"], - &["typing", "ContextManager"], - &["typing", "Coroutine"], - &["typing", "Counter"], - &["typing", "DefaultDict"], - &["typing", "Deque"], - &["typing", "Dict"], - &["typing", "Final"], - &["typing", "FrozenSet"], - &["typing", "Generator"], - &["typing", "Generic"], - &["typing", "IO"], - &["typing", "ItemsView"], - &["typing", "Iterable"], - &["typing", "Iterator"], - &["typing", "KeysView"], - &["typing", "List"], - &["typing", "Mapping"], - &["typing", "Match"], - &["typing", "MutableMapping"], - &["typing", "MutableSequence"], - &["typing", "MutableSet"], - &["typing", "Optional"], - &["typing", "OrderedDict"], - &["typing", "Pattern"], - &["typing", "Reversible"], - &["typing", "Sequence"], - &["typing", "Set"], - &["typing", "TextIO"], - &["typing", "Tuple"], - &["typing", "Type"], - &["typing", "TypeGuard"], - &["typing", "Union"], - &["typing", "Unpack"], - &["typing", "ValuesView"], - // `typing.io` - &["typing", "io", "BinaryIO"], - &["typing", "io", "IO"], - &["typing", "io", "TextIO"], - // `typing.re` - &["typing", "re", "Match"], - &["typing", "re", "Pattern"], - // `typing_extensions` - &["typing_extensions", "AsyncContextManager"], - &["typing_extensions", "AsyncGenerator"], - &["typing_extensions", "AsyncIterable"], - &["typing_extensions", "AsyncIterator"], - &["typing_extensions", "Awaitable"], - &["typing_extensions", "ChainMap"], - &["typing_extensions", "ClassVar"], - &["typing_extensions", "Concatenate"], - &["typing_extensions", "ContextManager"], - &["typing_extensions", "Coroutine"], - &["typing_extensions", "Counter"], - &["typing_extensions", "DefaultDict"], - &["typing_extensions", "Deque"], - &["typing_extensions", "Type"], - // `weakref` - &["weakref", "WeakKeyDictionary"], - &["weakref", "WeakSet"], - &["weakref", "WeakValueDictionary"], -]; +/// Returns `true` if a call path is a [PEP 593] generic (e.g. `Annotated`). +/// +/// See: +/// +/// [PEP 593]: https://peps.python.org/pep-0593/ +pub fn is_pep_593_generic_type(call_path: &[&str]) -> bool { + matches!(call_path, ["typing" | "typing_extensions", "Annotated"]) +} -// See: https://docs.python.org/3/library/typing.html -pub const PEP_593_SUBSCRIPTS: &[&[&str]] = &[ - // `typing` - &["typing", "Annotated"], - // `typing_extensions` - &["typing_extensions", "Annotated"], -]; +/// Returns `true` if a name matches that of a generic from the Python standard library (e.g. +/// `list` or `Set`). +/// +/// See: +pub fn is_standard_library_generic_member(member: &str) -> bool { + // Constructed by taking every pattern from `is_standard_library_generic`, removing all but + // the last element in each pattern, and de-duplicating the values. + matches!( + member, + "dict" + | "AbstractAsyncContextManager" + | "AbstractContextManager" + | "AbstractSet" + | "AsyncContextManager" + | "AsyncGenerator" + | "AsyncIterable" + | "AsyncIterator" + | "Awaitable" + | "BinaryIO" + | "ByteString" + | "Callable" + | "ChainMap" + | "ClassVar" + | "Collection" + | "Concatenate" + | "Container" + | "ContextManager" + | "Coroutine" + | "Counter" + | "DefaultDict" + | "Deque" + | "Dict" + | "Final" + | "FrozenSet" + | "Generator" + | "Generic" + | "IO" + | "ItemsView" + | "Iterable" + | "Iterator" + | "KeysView" + | "List" + | "Mapping" + | "MappingView" + | "Match" + | "MutableMapping" + | "MutableSequence" + | "MutableSet" + | "Optional" + | "OrderedDict" + | "Pattern" + | "Reversible" + | "Sequence" + | "Set" + | "TextIO" + | "Tuple" + | "Type" + | "TypeGuard" + | "Union" + | "Unpack" + | "ValuesView" + | "WeakKeyDictionary" + | "WeakSet" + | "WeakValueDictionary" + | "defaultdict" + | "deque" + | "frozenset" + | "list" + | "set" + | "tuple" + | "type" + ) +} + +/// Returns `true` if a name matches that of a generic from [PEP 593] (e.g. `Annotated`). +/// +/// See: +/// +/// [PEP 593]: https://peps.python.org/pep-0593/ +pub fn is_pep_593_generic_member(member: &str) -> bool { + // Constructed by taking every pattern from `is_pep_593_generic`, removing all but + // the last element in each pattern, and de-duplicating the values. + matches!(member, "Annotated") +} + +/// Returns `true` if a call path represents that of an immutable, non-generic type from the Python +/// standard library (e.g. `int` or `str`). +pub fn is_immutable_non_generic_type(call_path: &[&str]) -> bool { + matches!( + call_path, + ["collections", "abc", "Sized"] + | ["typing", "LiteralString" | "Sized"] + | [ + "", + "bool" + | "bytes" + | "complex" + | "float" + | "frozenset" + | "int" + | "object" + | "range" + | "str" + ] + ) +} + +/// Returns `true` if a call path represents that of an immutable, generic type from the Python +/// standard library (e.g. `tuple`). +pub fn is_immutable_generic_type(call_path: &[&str]) -> bool { + matches!( + call_path, + ["", "tuple"] + | [ + "collections", + "abc", + "ByteString" + | "Collection" + | "Container" + | "Iterable" + | "Mapping" + | "Reversible" + | "Sequence" + | "Set" + ] + | [ + "typing", + "AbstractSet" + | "ByteString" + | "Callable" + | "Collection" + | "Container" + | "FrozenSet" + | "Iterable" + | "Literal" + | "Mapping" + | "Never" + | "NoReturn" + | "Reversible" + | "Sequence" + | "Tuple" + ] + ) +} + +/// Returns `true` if a call path represents a function from the Python standard library that +/// returns a mutable value (e.g., `dict`). +pub fn is_mutable_return_type(call_path: &[&str]) -> bool { + matches!( + call_path, + ["", "dict" | "list" | "set"] + | [ + "collections", + "Counter" | "OrderedDict" | "defaultdict" | "deque" + ] + ) +} + +/// Returns `true` if a call path represents a function from the Python standard library that +/// returns a immutable value (e.g., `bool`). +pub fn is_immutable_return_type(call_path: &[&str]) -> bool { + matches!( + call_path, + ["datetime", "date" | "datetime" | "timedelta"] + | ["decimal", "Decimal"] + | ["fractions", "Fraction"] + | ["operator", "attrgetter" | "itemgetter" | "methodcaller"] + | ["pathlib", "Path"] + | ["types", "MappingProxyType"] + | ["re", "compile"] + | [ + "", + "bool" | "complex" | "float" | "frozenset" | "int" | "str" | "tuple" + ] + ) +} type ModuleMember = (&'static str, &'static str); -type SymbolReplacement = (ModuleMember, ModuleMember); +/// Given a typing member, returns the module and member name for a generic from the Python standard +/// library (e.g., `list` for `typing.List`), if such a generic was introduced by [PEP 585]. +/// +/// [PEP 585]: https://peps.python.org/pep-0585/ +pub fn as_pep_585_generic(module: &str, member: &str) -> Option { + match (module, member) { + ("typing", "Dict") => Some(("", "dict")), + ("typing", "FrozenSet") => Some(("", "frozenset")), + ("typing", "List") => Some(("", "list")), + ("typing", "Set") => Some(("", "set")), + ("typing", "Tuple") => Some(("", "tuple")), + ("typing", "Type") => Some(("", "type")), + ("typing_extensions", "Type") => Some(("", "type")), + ("typing", "Deque") => Some(("collections", "deque")), + ("typing_extensions", "Deque") => Some(("collections", "deque")), + ("typing", "DefaultDict") => Some(("collections", "defaultdict")), + ("typing_extensions", "DefaultDict") => Some(("collections", "defaultdict")), + _ => None, + } +} -// See: https://peps.python.org/pep-0585/ -pub const PEP_585_GENERICS: &[SymbolReplacement] = &[ - (("typing", "Dict"), ("", "dict")), - (("typing", "FrozenSet"), ("", "frozenset")), - (("typing", "List"), ("", "list")), - (("typing", "Set"), ("", "set")), - (("typing", "Tuple"), ("", "tuple")), - (("typing", "Type"), ("", "type")), - (("typing_extensions", "Type"), ("", "type")), - (("typing", "Deque"), ("collections", "deque")), - (("typing_extensions", "Deque"), ("collections", "deque")), - (("typing", "DefaultDict"), ("collections", "defaultdict")), - ( - ("typing_extensions", "DefaultDict"), - ("collections", "defaultdict"), - ), -]; +/// Given a typing member, returns `true` if a generic equivalent exists in the Python standard +/// library (e.g., `list` for `typing.List`), as introduced by [PEP 585]. +/// +/// [PEP 585]: https://peps.python.org/pep-0585/ +pub fn has_pep_585_generic(module: &str, member: &str) -> bool { + // Constructed by taking every pattern from `as_pep_585_generic`, removing all but + // the last element in each pattern, and de-duplicating the values. + matches!( + (module, member), + ("", "dict" | "frozenset" | "list" | "set" | "tuple" | "type") + | ("collections", "deque" | "defaultdict") + ) +} -// See: https://github.com/JelleZijlstra/autotyping/blob/0adba5ba0eee33c1de4ad9d0c79acfd737321dd9/autotyping/autotyping.py#L69-L91 -pub static SIMPLE_MAGIC_RETURN_TYPES: Lazy> = - Lazy::new(|| { - FxHashMap::from_iter([ - ("__str__", "str"), - ("__repr__", "str"), - ("__len__", "int"), - ("__length_hint__", "int"), - ("__init__", "None"), - ("__del__", "None"), - ("__bool__", "bool"), - ("__bytes__", "bytes"), - ("__format__", "str"), - ("__contains__", "bool"), - ("__complex__", "complex"), - ("__int__", "int"), - ("__float__", "float"), - ("__index__", "int"), - ("__setattr__", "None"), - ("__delattr__", "None"), - ("__setitem__", "None"), - ("__delitem__", "None"), - ("__set__", "None"), - ("__instancecheck__", "bool"), - ("__subclasscheck__", "bool"), - ]) - }); - -pub const IMMUTABLE_TYPES: &[&[&str]] = &[ - &["", "bool"], - &["", "bytes"], - &["", "complex"], - &["", "float"], - &["", "frozenset"], - &["", "int"], - &["", "object"], - &["", "range"], - &["", "str"], - &["collections", "abc", "Sized"], - &["typing", "LiteralString"], - &["typing", "Sized"], -]; - -pub const IMMUTABLE_GENERIC_TYPES: &[&[&str]] = &[ - &["", "tuple"], - &["collections", "abc", "ByteString"], - &["collections", "abc", "Collection"], - &["collections", "abc", "Container"], - &["collections", "abc", "Iterable"], - &["collections", "abc", "Mapping"], - &["collections", "abc", "Reversible"], - &["collections", "abc", "Sequence"], - &["collections", "abc", "Set"], - &["typing", "AbstractSet"], - &["typing", "ByteString"], - &["typing", "Callable"], - &["typing", "Collection"], - &["typing", "Container"], - &["typing", "FrozenSet"], - &["typing", "Iterable"], - &["typing", "Literal"], - &["typing", "Mapping"], - &["typing", "Never"], - &["typing", "NoReturn"], - &["typing", "Reversible"], - &["typing", "Sequence"], - &["typing", "Tuple"], -]; +/// Returns the expected return type for a magic method. +/// +/// See: +pub fn simple_magic_return_type(method: &str) -> Option<&'static str> { + match method { + "__str__" | "__repr__" | "__format__" => Some("str"), + "__bytes__" => Some("bytes"), + "__len__" | "__length_hint__" | "__int__" | "__index__" => Some("int"), + "__float__" => Some("float"), + "__complex__" => Some("complex"), + "__bool__" | "__contains__" | "__instancecheck__" | "__subclasscheck__" => Some("bool"), + "__init__" | "__del__" | "__setattr__" | "__delattr__" | "__setitem__" | "__delitem__" + | "__set__" => Some("None"), + _ => None, + } +} diff --git a/crates/ruff_python_whitespace/Cargo.toml b/crates/ruff_python_whitespace/Cargo.toml index 584418405b..cbfc1aea24 100644 --- a/crates/ruff_python_whitespace/Cargo.toml +++ b/crates/ruff_python_whitespace/Cargo.toml @@ -2,8 +2,13 @@ name = "ruff_python_whitespace" version = "0.0.0" publish = false +authors = { workspace = true } edition = { workspace = true } rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } [lib] diff --git a/crates/ruff_python_whitespace/src/cursor.rs b/crates/ruff_python_whitespace/src/cursor.rs new file mode 100644 index 0000000000..43a750cb4f --- /dev/null +++ b/crates/ruff_python_whitespace/src/cursor.rs @@ -0,0 +1,103 @@ +use std::str::Chars; + +use ruff_text_size::{TextLen, TextSize}; + +pub const EOF_CHAR: char = '\0'; + +/// A [`Cursor`] over a string. +#[derive(Debug, Clone)] +pub struct Cursor<'a> { + chars: Chars<'a>, + source_length: TextSize, +} + +impl<'a> Cursor<'a> { + pub fn new(source: &'a str) -> Self { + Self { + source_length: source.text_len(), + chars: source.chars(), + } + } + + /// Return the remaining input as a string slice. + pub fn chars(&self) -> Chars<'a> { + self.chars.clone() + } + + /// Peeks the next character from the input stream without consuming it. + /// Returns [`EOF_CHAR`] if the file is at the end of the file. + pub fn first(&self) -> char { + self.chars.clone().next().unwrap_or(EOF_CHAR) + } + + /// Peeks the next character from the input stream without consuming it. + /// Returns [`EOF_CHAR`] if the file is at the end of the file. + pub fn last(&self) -> char { + self.chars.clone().next_back().unwrap_or(EOF_CHAR) + } + + // SAFETY: THe `source.text_len` call in `new` would panic if the string length is larger than a `u32`. + #[allow(clippy::cast_possible_truncation)] + pub fn text_len(&self) -> TextSize { + TextSize::new(self.chars.as_str().len() as u32) + } + + pub fn token_len(&self) -> TextSize { + self.source_length - self.text_len() + } + + pub fn start_token(&mut self) { + self.source_length = self.text_len(); + } + + /// Returns `true` if the file is at the end of the file. + pub fn is_eof(&self) -> bool { + self.chars.as_str().is_empty() + } + + /// Consumes the next character + pub fn bump(&mut self) -> Option { + self.chars.next() + } + + /// Consumes the next character from the back + pub fn bump_back(&mut self) -> Option { + self.chars.next_back() + } + + pub fn eat_char(&mut self, c: char) -> bool { + if self.first() == c { + self.bump(); + true + } else { + false + } + } + + pub fn eat_char_back(&mut self, c: char) -> bool { + if self.last() == c { + self.bump_back(); + true + } else { + false + } + } + + /// Eats symbols while predicate returns true or until the end of file is reached. + pub fn eat_while(&mut self, mut predicate: impl FnMut(char) -> bool) { + // It was tried making optimized version of this for eg. line comments, but + // LLVM can inline all of this and compile it down to fast iteration over bytes. + while predicate(self.first()) && !self.is_eof() { + self.bump(); + } + } + + /// Eats symbols from the back while predicate returns true or until the beginning of file is reached. + pub fn eat_back_while(&mut self, mut predicate: impl FnMut(char) -> bool) { + // It was tried making optimized version of this for eg. line comments, but + // LLVM can inline all of this and compile it down to fast iteration over bytes. + while predicate(self.last()) && !self.is_eof() { + self.bump_back(); + } + } +} diff --git a/crates/ruff_python_whitespace/src/lib.rs b/crates/ruff_python_whitespace/src/lib.rs index 36d3ddee97..b8c95e351c 100644 --- a/crates/ruff_python_whitespace/src/lib.rs +++ b/crates/ruff_python_whitespace/src/lib.rs @@ -1,5 +1,7 @@ +mod cursor; mod newlines; mod whitespace; +pub use cursor::*; pub use newlines::*; pub use whitespace::*; diff --git a/crates/ruff_rustpython/Cargo.toml b/crates/ruff_rustpython/Cargo.toml index e66ba4caa6..e6c0bc0005 100644 --- a/crates/ruff_rustpython/Cargo.toml +++ b/crates/ruff_rustpython/Cargo.toml @@ -2,8 +2,13 @@ name = "ruff_rustpython" version = "0.0.0" publish = false +authors = { workspace = true } edition = { workspace = true } rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } [lib] diff --git a/crates/ruff_testing_macros/Cargo.toml b/crates/ruff_testing_macros/Cargo.toml deleted file mode 100644 index 215168923c..0000000000 --- a/crates/ruff_testing_macros/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "ruff_testing_macros" -edition = "2021" -version = "0.0.0" -publish = false - -[lib] -proc-macro = true - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -glob = { workspace = true } -proc-macro2 = { workspace = true } -quote = { workspace = true } -syn = { workspace = true, features = ["extra-traits", "full"] } diff --git a/crates/ruff_testing_macros/src/lib.rs b/crates/ruff_testing_macros/src/lib.rs deleted file mode 100644 index 23815540a1..0000000000 --- a/crates/ruff_testing_macros/src/lib.rs +++ /dev/null @@ -1,403 +0,0 @@ -use proc_macro::TokenStream; -use std::borrow::Cow; -use std::collections::BTreeMap; -use std::env; -use std::path::{Component, PathBuf}; - -use glob::{glob, Pattern}; -use proc_macro2::Span; -use quote::{format_ident, quote}; -use syn::parse::{Parse, ParseStream}; -use syn::punctuated::Punctuated; -use syn::spanned::Spanned; -use syn::{bracketed, parse_macro_input, parse_quote, Error, FnArg, ItemFn, LitStr, Pat, Token}; - -#[derive(Debug)] -struct FixtureConfiguration { - pattern: Pattern, - pattern_span: Span, - exclude: Vec, -} - -struct Arg { - name: syn::Ident, - value: ArgValue, -} - -impl Parse for Arg { - fn parse(input: ParseStream) -> syn::Result { - let name = input.parse()?; - let _equal_token: Token![=] = input.parse()?; - let value = input.parse()?; - - Ok(Self { name, value }) - } -} - -enum ArgValue { - LitStr(LitStr), - List(Punctuated), -} - -impl Parse for ArgValue { - fn parse(input: ParseStream) -> syn::Result { - let value = if input.peek(syn::token::Bracket) { - let inner; - _ = bracketed!(inner in input); - - let values = inner.parse_terminated( - |parser| { - let value: LitStr = parser.parse()?; - Ok(value) - }, - Token![,], - )?; - ArgValue::List(values) - } else { - ArgValue::LitStr(input.parse()?) - }; - - Ok(value) - } -} - -impl Parse for FixtureConfiguration { - fn parse(input: ParseStream) -> syn::Result { - let args: Punctuated<_, Token![,]> = input.parse_terminated(Arg::parse, Token![,])?; - - let mut pattern = None; - let mut exclude = None; - - for arg in args { - match arg.name.to_string().as_str() { - "pattern" => match arg.value { - ArgValue::LitStr(value) => { - pattern = Some(try_parse_pattern(&value)?); - } - ArgValue::List(list) => { - return Err(Error::new( - list.span(), - "The pattern must be a string literal", - )) - } - }, - - "exclude" => { - match arg.value { - ArgValue::LitStr(lit) => return Err(Error::new( - lit.span(), - "The exclude argument must be an array of globs: 'exclude=[\"a.py\"]", - )), - ArgValue::List(list) => { - let mut exclude_patterns = Vec::with_capacity(list.len()); - - for pattern in list { - let (pattern, _) = try_parse_pattern(&pattern)?; - exclude_patterns.push(pattern); - } - - exclude = Some(exclude_patterns); - } - } - } - - _ => { - return Err(Error::new( - arg.name.span(), - format!("Unknown argument {}.", arg.name), - )); - } - } - } - - let exclude = exclude.unwrap_or_default(); - - match pattern { - None => Err(Error::new( - input.span(), - "'fixture' macro must have a pattern attribute", - )), - Some((pattern, pattern_span)) => Ok(Self { - pattern, - pattern_span, - exclude, - }), - } - } -} - -fn try_parse_pattern(pattern_lit: &LitStr) -> syn::Result<(Pattern, Span)> { - let raw_pattern = pattern_lit.value(); - match Pattern::new(&raw_pattern) { - Ok(pattern) => Ok((pattern, pattern_lit.span())), - Err(err) => Err(Error::new( - pattern_lit.span(), - format!("'{raw_pattern}' is not a valid glob pattern: '{}'", err.msg), - )), - } -} - -/// Generates a test for each file that matches the specified pattern. -/// -/// The attributed function must have exactly one argument of the type `&Path`. -/// The `#[test]` attribute must come after the `#[fixture]` argument or `test` will complain -/// that your function can not have any arguments. -/// -/// ## Examples -/// -/// Creates a test for every python file file in the `fixtures` directory. -/// -/// ```ignore -/// #[fixture(pattern="fixtures/*.py")] -/// #[test] -/// fn my_test(path: &Path) -> std::io::Result<()> { -/// // ... implement the test -/// Ok(()) -/// } -/// ``` -/// -/// ### Excluding Files -/// -/// You can exclude files by specifying optional `exclude` patterns. -/// -/// ```ignore -/// #[fixture(pattern="fixtures/*.py", exclude=["a_*.py"])] -/// #[test] -/// fn my_test(path: &Path) -> std::io::Result<()> { -/// // ... implement the test -/// Ok(()) -/// } -/// ``` -/// -/// Creates tests for each python file in the `fixtures` directory except for files matching the `a_*.py` pattern. -#[proc_macro_attribute] -pub fn fixture(attribute: TokenStream, item: TokenStream) -> TokenStream { - let test_fn = parse_macro_input!(item as ItemFn); - let configuration = parse_macro_input!(attribute as FixtureConfiguration); - - let result = generate_fixtures(test_fn, &configuration); - - let stream = match result { - Ok(output) => output, - Err(err) => err.to_compile_error(), - }; - - TokenStream::from(stream) -} - -fn generate_fixtures( - mut test_fn: ItemFn, - configuration: &FixtureConfiguration, -) -> syn::Result { - // Remove the fixtures attribute - test_fn - .attrs - .retain(|attr| !attr.path().is_ident("fixtures")); - - // Extract the name of the only argument of the test function. - let last_arg = test_fn.sig.inputs.last(); - let path_ident = match (test_fn.sig.inputs.len(), last_arg) { - (1, Some(last_arg)) => match last_arg { - FnArg::Typed(typed) => match typed.pat.as_ref() { - Pat::Ident(ident) => ident.ident.clone(), - pat => { - return Err(Error::new( - pat.span(), - "#[fixture] function argument name must be an identifier", - )); - } - }, - FnArg::Receiver(receiver) => { - return Err(Error::new( - receiver.span(), - "#[fixture] function argument name must be an identifier", - )); - } - }, - _ => { - return Err(Error::new( - test_fn.sig.inputs.span(), - "#[fixture] function must have exactly one argument with the type '&Path'", - )); - } - }; - - // Remove all arguments - test_fn.sig.inputs.clear(); - - let crate_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect( - "#[fixture] requires CARGO_MANIFEST_DIR to be set during the build to resolve the relative paths to the test files.", - )); - - let pattern = if configuration.pattern.as_str().starts_with('/') { - Cow::from(configuration.pattern.as_str()) - } else { - Cow::from(format!( - "{}/{}", - crate_dir - .to_str() - .expect("CARGO_MANIFEST_DIR must point to a directory with a UTF8 path"), - configuration.pattern.as_str() - )) - }; - - let files = glob(&pattern).expect("Pattern to be valid").flatten(); - let mut modules = Modules::default(); - - for file in files { - if configuration - .exclude - .iter() - .any(|exclude| exclude.matches_path(&file)) - { - continue; - } - - let mut test_fn = test_fn.clone(); - - let test_name = file - .file_name() - // SAFETY: Glob only matches on file names. - .unwrap() - .to_str() - .expect("Expected path to be valid UTF8") - .replace('.', "_"); - - test_fn.sig.ident = format_ident!("{test_name}"); - - let path = file.as_os_str().to_str().unwrap(); - - test_fn.block.stmts.insert( - 0, - parse_quote!(let #path_ident = std::path::Path::new(#path);), - ); - - modules.push_test(Test { - path: file, - test_fn, - }); - } - - if modules.is_empty() { - return Err(Error::new( - configuration.pattern_span, - "No file matches the specified glob pattern", - )); - } - - let root = find_highest_common_ancestor_module(&modules.root); - - root.generate(&test_fn.sig.ident.to_string()) -} - -fn find_highest_common_ancestor_module(module: &Module) -> &Module { - let children = &module.children; - - if children.len() == 1 { - let (_, child) = children.iter().next().unwrap(); - - match child { - Child::Module(common_child) => find_highest_common_ancestor_module(common_child), - Child::Test(_) => module, - } - } else { - module - } -} - -#[derive(Debug)] -struct Test { - path: PathBuf, - test_fn: ItemFn, -} - -impl Test { - fn generate(&self, _: &str) -> proc_macro2::TokenStream { - let test_fn = &self.test_fn; - quote!(#test_fn) - } -} - -#[derive(Debug, Default)] -struct Module { - children: BTreeMap, -} - -impl Module { - fn generate(&self, name: &str) -> syn::Result { - let mut inner = Vec::with_capacity(self.children.len()); - - for (name, child) in &self.children { - inner.push(child.generate(name)?); - } - - let module_ident = format_ident!("{name}"); - - Ok(quote!( - mod #module_ident { - use super::*; - - #(#inner)* - } - )) - } -} - -#[derive(Debug)] -enum Child { - Module(Module), - Test(Test), -} - -impl Child { - fn generate(&self, name: &str) -> syn::Result { - match self { - Child::Module(module) => module.generate(name), - Child::Test(test) => Ok(test.generate(name)), - } - } -} - -#[derive(Debug, Default)] -struct Modules { - root: Module, -} - -impl Modules { - fn push_test(&mut self, test: Test) { - let mut components = test - .path - .as_path() - .components() - .skip_while(|c| matches!(c, Component::RootDir)) - .peekable(); - - let mut current = &mut self.root; - while let Some(component) = components.next() { - let name = component.as_os_str().to_str().unwrap(); - // A directory - if components.peek().is_some() { - let name = component.as_os_str().to_str().unwrap(); - let entry = current.children.entry(name.to_owned()); - - match entry.or_insert_with(|| Child::Module(Module::default())) { - Child::Module(module) => { - current = module; - } - Child::Test(_) => { - unreachable!() - } - } - } else { - // We reached the final component, insert the test - drop(components); - current.children.insert(name.to_owned(), Child::Test(test)); - break; - } - } - } - - fn is_empty(&self) -> bool { - self.root.children.is_empty() - } -} diff --git a/crates/ruff_textwrap/Cargo.toml b/crates/ruff_textwrap/Cargo.toml index 864a259d33..9b721596c6 100644 --- a/crates/ruff_textwrap/Cargo.toml +++ b/crates/ruff_textwrap/Cargo.toml @@ -2,8 +2,13 @@ name = "ruff_textwrap" version = "0.0.0" publish = false +authors = { workspace = true } edition = { workspace = true } rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } [dependencies] ruff_python_whitespace = { path = "../ruff_python_whitespace" } diff --git a/crates/ruff_wasm/Cargo.toml b/crates/ruff_wasm/Cargo.toml index 7ad96d646b..5383edfa78 100644 --- a/crates/ruff_wasm/Cargo.toml +++ b/crates/ruff_wasm/Cargo.toml @@ -2,8 +2,13 @@ name = "ruff_wasm" version = "0.0.0" publish = false +authors = { workspace = true } edition = { workspace = true } rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } description = "WebAssembly bindings for Ruff" [lib] diff --git a/crates/ruff_wasm/src/lib.rs b/crates/ruff_wasm/src/lib.rs index 84566771dd..27e3de73c0 100644 --- a/crates/ruff_wasm/src/lib.rs +++ b/crates/ruff_wasm/src/lib.rs @@ -10,18 +10,16 @@ use ruff::linter::{check_path, LinterResult}; use ruff::registry::AsRule; use ruff::rules::{ flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, flake8_comprehensions, - flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, flake8_import_conventions, - flake8_pytest_style, flake8_quotes, flake8_self, flake8_tidy_imports, flake8_type_checking, - flake8_unused_arguments, isort, mccabe, pep8_naming, pycodestyle, pydocstyle, pyflakes, pylint, + flake8_copyright, flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, + flake8_import_conventions, flake8_pytest_style, flake8_quotes, flake8_self, + flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, isort, mccabe, pep8_naming, + pycodestyle, pydocstyle, pyflakes, pylint, pyupgrade, }; use ruff::settings::configuration::Configuration; use ruff::settings::options::Options; use ruff::settings::{defaults, flags, Settings}; -use ruff_diagnostics::Edit; use ruff_python_ast::source_code::{Indexer, Locator, SourceLocation, Stylist}; -const VERSION: &str = env!("CARGO_PKG_VERSION"); - #[wasm_bindgen(typescript_custom_section)] const TYPES: &'static str = r#" export interface Diagnostic { @@ -38,7 +36,7 @@ export interface Diagnostic { fix: { message: string | null; edits: { - content: string; + content: string | null; location: { row: number; column: number; @@ -52,12 +50,6 @@ export interface Diagnostic { }; "#; -#[derive(Serialize, Deserialize, Eq, PartialEq, Debug)] -pub struct ExpandedFix { - message: Option, - edits: Vec, -} - #[derive(Serialize, Deserialize, Eq, PartialEq, Debug)] pub struct ExpandedMessage { pub code: String, @@ -67,6 +59,19 @@ pub struct ExpandedMessage { pub fix: Option, } +#[derive(Serialize, Deserialize, Eq, PartialEq, Debug)] +pub struct ExpandedFix { + message: Option, + edits: Vec, +} + +#[derive(Serialize, Deserialize, Eq, PartialEq, Debug)] +struct ExpandedEdit { + location: SourceLocation, + end_location: SourceLocation, + content: Option, +} + #[wasm_bindgen(start)] pub fn run() { use log::Level; @@ -86,7 +91,7 @@ pub fn run() { #[wasm_bindgen] #[allow(non_snake_case)] pub fn currentVersion() -> JsValue { - JsValue::from(VERSION) + JsValue::from(ruff::VERSION) } #[wasm_bindgen] @@ -137,10 +142,8 @@ pub fn defaultSettings() -> Result { flake8_bugbear: Some(flake8_bugbear::settings::Settings::default().into()), flake8_builtins: Some(flake8_builtins::settings::Settings::default().into()), flake8_comprehensions: Some(flake8_comprehensions::settings::Settings::default().into()), + flake8_copyright: Some(flake8_copyright::settings::Settings::default().into()), flake8_errmsg: Some(flake8_errmsg::settings::Settings::default().into()), - flake8_pytest_style: Some(flake8_pytest_style::settings::Settings::default().into()), - flake8_quotes: Some(flake8_quotes::settings::Settings::default().into()), - flake8_self: Some(flake8_self::settings::Settings::default().into()), flake8_gettext: Some(flake8_gettext::settings::Settings::default().into()), flake8_implicit_str_concat: Some( flake8_implicit_str_concat::settings::Settings::default().into(), @@ -148,6 +151,9 @@ pub fn defaultSettings() -> Result { flake8_import_conventions: Some( flake8_import_conventions::settings::Settings::default().into(), ), + flake8_pytest_style: Some(flake8_pytest_style::settings::Settings::default().into()), + flake8_quotes: Some(flake8_quotes::settings::Settings::default().into()), + flake8_self: Some(flake8_self::settings::Settings::default().into()), flake8_tidy_imports: Some(flake8_tidy_imports::settings::Settings::default().into()), flake8_type_checking: Some(flake8_type_checking::settings::Settings::default().into()), flake8_unused_arguments: Some( @@ -160,6 +166,7 @@ pub fn defaultSettings() -> Result { pydocstyle: Some(pydocstyle::settings::Settings::default().into()), pyflakes: Some(pyflakes::settings::Settings::default().into()), pylint: Some(pylint::settings::Settings::default().into()), + pyupgrade: Some(pyupgrade::settings::Settings::default().into()), })?) } @@ -202,6 +209,7 @@ pub fn check(contents: &str, options: JsValue) -> Result { &directives, &settings, flags::Noqa::Enabled, + None, ); let source_code = locator.to_source_code(); @@ -219,7 +227,15 @@ pub fn check(contents: &str, options: JsValue) -> Result { end_location, fix: message.fix.map(|fix| ExpandedFix { message: message.kind.suggestion, - edits: fix.into_edits(), + edits: fix + .into_edits() + .into_iter() + .map(|edit| ExpandedEdit { + location: source_code.source_location(edit.start()), + end_location: source_code.source_location(edit.end()), + content: edit.content().map(ToString::to_string), + }) + .collect(), }), } }) diff --git a/docs/.overrides/partials/integrations/analytics/fathom.html b/docs/.overrides/partials/integrations/analytics/fathom.html new file mode 100644 index 0000000000..340d60d816 --- /dev/null +++ b/docs/.overrides/partials/integrations/analytics/fathom.html @@ -0,0 +1 @@ + diff --git a/docs/assets/bolt.svg b/docs/assets/bolt.svg new file mode 100644 index 0000000000..1d2a503d51 --- /dev/null +++ b/docs/assets/bolt.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/assets/ruff-favicon.png b/docs/assets/ruff-favicon.png index 7395c3fbce..0a6d0871c7 100644 Binary files a/docs/assets/ruff-favicon.png and b/docs/assets/ruff-favicon.png differ diff --git a/docs/assets/ruff.svg b/docs/assets/ruff.svg deleted file mode 100644 index 4494fe78ff..0000000000 --- a/docs/assets/ruff.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - \ No newline at end of file diff --git a/docs/configuration.md b/docs/configuration.md index f2805e731a..3575253ed7 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -11,12 +11,14 @@ If left unspecified, Ruff's default configuration is equivalent to: ```toml [tool.ruff] -# Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default. +# Enable the pycodestyle (`E`) and Pyflakes (`F`) rules by default. +# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or +# McCabe complexity (`C901`) by default. select = ["E", "F"] ignore = [] # Allow autofix for all enabled rules (when `--fix`) is provided. -fixable = ["A", "B", "C", "D", "E", "F", "G", "I", "N", "Q", "S", "T", "W", "ANN", "ARG", "BLE", "COM", "DJ", "DTZ", "EM", "ERA", "EXE", "FBT", "ICN", "INP", "ISC", "NPY", "PD", "PGH", "PIE", "PL", "PT", "PTH", "PYI", "RET", "RSE", "RUF", "SIM", "SLF", "TCH", "TID", "TRY", "UP", "YTT"] +fixable = ["ALL"] unfixable = [] # Exclude a variety of commonly ignored directories. @@ -53,10 +55,6 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" # Assume Python 3.10. target-version = "py310" - -[tool.ruff.mccabe] -# Unlike Flake8, default to a complexity level of 10. -max-complexity = 10 ``` As an example, the following would configure Ruff to: (1) enforce flake8-bugbear rules, in addition @@ -163,7 +161,7 @@ Usage: ruff [OPTIONS] Commands: check Run Ruff on the given files or directories (default) - rule Explain a rule + rule Explain a rule (or all rules) config List or describe the available configuration options linter List all supported upstream linters clean Clear any caches in the current directory and any subdirectories @@ -211,9 +209,11 @@ Options: --ignore-noqa Ignore any `# noqa` comments --format - Output serialization format for violations [env: RUFF_FORMAT=] [possible values: text, json, junit, grouped, github, gitlab, pylint, azure] + Output serialization format for violations [env: RUFF_FORMAT=] [possible values: text, json, json-lines, junit, grouped, github, gitlab, pylint, azure] + -o, --output-file + Specify file to write the linter output to (default: stdout) --target-version - The minimum Python version that should be supported [possible values: py37, py38, py39, py310, py311] + The minimum Python version that should be supported [possible values: py37, py38, py39, py310, py311, py312] --config Path to the `pyproject.toml` or `ruff.toml` file to use for configuration --statistics @@ -326,6 +326,24 @@ By default, Ruff will also skip any files that are omitted via `.ignore`, `.giti Files that are passed to `ruff` directly are always linted, regardless of the above criteria. For example, `ruff check /path/to/excluded/file.py` will always lint `file.py`. +## Jupyter Notebook discovery + +Ruff has built-in experimental support for linting [Jupyter Notebooks](https://jupyter.org/). + +To opt in to linting Jupyter Notebook (`.ipynb`) files, add the `*.ipynb` pattern to your +[`include`](settings.md#include) setting, like so: + +```toml +[tool.ruff] +include = ["*.py", "*.pyi", "**/pyproject.toml", "*.ipynb"] +``` + +This will prompt Ruff to discover Jupyter Notebook (`.ipynb`) files in any specified +directories, and lint them accordingly. + +Alternatively, pass the notebook file(s) to `ruff` on the command-line directly. For example, +`ruff check /path/to/notebook.ipynb` will always lint `notebook.ipynb`. + ## Rule selection The set of enabled rules is controlled via the [`select`](settings.md#select) and diff --git a/docs/faq.md b/docs/faq.md index 3ee9baca50..3ce02b7f98 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -41,6 +41,7 @@ natively, including: - [flake8-builtins](https://pypi.org/project/flake8-builtins/) - [flake8-commas](https://pypi.org/project/flake8-commas/) - [flake8-comprehensions](https://pypi.org/project/flake8-comprehensions/) +- [flake8-copyright](https://pypi.org/project/flake8-copyright/) - [flake8-datetimez](https://pypi.org/project/flake8-datetimez/) - [flake8-debugger](https://pypi.org/project/flake8-debugger/) - [flake8-django](https://pypi.org/project/flake8-django/) @@ -73,6 +74,7 @@ natively, including: - [mccabe](https://pypi.org/project/mccabe/) - [pandas-vet](https://pypi.org/project/pandas-vet/) - [pep8-naming](https://pypi.org/project/pep8-naming/) +- [perflint](https://pypi.org/project/perflint/) ([#4789](https://github.com/astral-sh/ruff/issues/4789)) - [pydocstyle](https://pypi.org/project/pydocstyle/) - [pygrep-hooks](https://github.com/pre-commit/pygrep-hooks) - [pyupgrade](https://pypi.org/project/pyupgrade/) @@ -143,6 +145,7 @@ Today, Ruff can be used to replace Flake8 when used with any of the following pl - [flake8-builtins](https://pypi.org/project/flake8-builtins/) - [flake8-commas](https://pypi.org/project/flake8-commas/) - [flake8-comprehensions](https://pypi.org/project/flake8-comprehensions/) +- [flake8-copyright](https://pypi.org/project/flake8-comprehensions/) - [flake8-datetimez](https://pypi.org/project/flake8-datetimez/) - [flake8-debugger](https://pypi.org/project/flake8-debugger/) - [flake8-django](https://pypi.org/project/flake8-django/) @@ -173,6 +176,7 @@ Today, Ruff can be used to replace Flake8 when used with any of the following pl - [mccabe](https://pypi.org/project/mccabe/) - [pandas-vet](https://pypi.org/project/pandas-vet/) - [pep8-naming](https://pypi.org/project/pep8-naming/) +- [perflint](https://pypi.org/project/perflint/) ([#4789](https://github.com/astral-sh/ruff/issues/4789)) - [pydocstyle](https://pypi.org/project/pydocstyle/) - [tryceratops](https://pypi.org/project/tryceratops/) @@ -305,7 +309,23 @@ src = ["../src", "../test"] ## Does Ruff support Jupyter Notebooks? -Ruff is integrated into [nbQA](https://github.com/nbQA-dev/nbQA), a tool for running linters and +Ruff has built-in experimental support for linting [Jupyter Notebooks](https://jupyter.org/). + +To opt in to linting Jupyter Notebook (`.ipynb`) files, add the `*.ipynb` pattern to your +[`include`](settings.md#include) setting, like so: + +```toml +[tool.ruff] +include = ["*.py", "*.pyi", "**/pyproject.toml", "*.ipynb"] +``` + +This will prompt Ruff to discover Jupyter Notebook (`.ipynb`) files in any specified +directories, and lint them accordingly. + +Alternatively, pass the notebook file(s) to `ruff` on the command-line directly. For example, +`ruff check /path/to/notebook.ipynb` will always lint `notebook.ipynb`. + +Ruff also integrates with [nbQA](https://github.com/nbQA-dev/nbQA), a tool for running linters and code formatters over Jupyter Notebooks. After installing `ruff` and `nbqa`, you can run Ruff over a notebook like so: @@ -350,6 +370,38 @@ matter how they're provided, which avoids accidental incompatibilities and simpl By default, no `convention` is set, and so the enabled rules are determined by the `select` setting alone. +## What is the "nursery"? + +The "nursery" is a collection of newer rules that are considered experimental or unstable. + +If a rule is marked as part of the "nursery", it can only be enabled via direct selection. For +example, consider a hypothetical rule, `HYP001`. If `HYP001` were included in the "nursery", it +could be enabled by adding the following to your `pyproject.toml`: + +```toml +[tool.ruff] +extend-select = ["HYP001"] +``` + +However, it would _not_ be enabled by selecting the `HYP` category, like so: + +```toml +[tool.ruff] +extend-select = ["HYP"] +``` + +Similarly, it would _not_ be enabled via the `ALL` selector: + +```toml +[tool.ruff] +select = ["ALL"] +``` + +(The "nursery" terminology comes from [Clippy](https://doc.rust-lang.org/nightly/clippy/), a similar +tool for linting Rust code.) + +To see which rules are currently in the "nursery", visit the [rules reference](https://beta.ruff.rs/docs/rules/). + ## How can I tell what settings Ruff is using to check my code? Run `ruff check /path/to/code.py --show-settings` to view the resolved settings for a given file. diff --git a/docs/requirements-insiders.txt b/docs/requirements-insiders.txt new file mode 100644 index 0000000000..20e4c11ad7 --- /dev/null +++ b/docs/requirements-insiders.txt @@ -0,0 +1,4 @@ +PyYAML==6.0 +black==23.3.0 +mkdocs==1.4.3 +git+ssh://git@github.com/astral-sh/mkdocs-material-insiders.git diff --git a/docs/requirements.txt b/docs/requirements.txt index 647559a59e..db22a4e7cb 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,4 @@ -mkdocs~=1.4.2 -mkdocs-material~=9.0.6 -PyYAML~=6.0 +PyYAML==6.0 black==23.3.0 +mkdocs==1.4.3 +mkdocs-material==9.1.18 diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css new file mode 100644 index 0000000000..2fca5ce9e6 --- /dev/null +++ b/docs/stylesheets/extra.css @@ -0,0 +1,94 @@ +:root { + --black: #261230; + --white: #ffffff; + --radiate: #d7ff64; + --flare: #6340ac; + --rock: #78876e; + --galaxy: #261230; + --space: #30173d; + --comet: #6f5d6f; + --cosmic: #de5fe9; + --sun: #ffac2f; + --electron: #46ebe1; + --aurora: #46eb74; + --constellation: #5f6de9; + --neutron: #cff3cf; + --proton: #f6afbc; + --nebula: #cdcbfb; + --supernova: #f1aff6; + --starlight: #f4f4f1; + --lunar: #fbf2fc; + --asteroid: #e3cee3; + --crater: #f0dfdf; +} + +[data-md-color-scheme="astral-light"] { + --md-default-bg-color--dark: var(--black); + --md-primary-fg-color: var(--galaxy); + --md-typeset-a-color: var(--flare); + --md-accent-fg-color: var(--cosmic); +} + +[data-md-color-scheme="astral-dark"] { + --md-default-bg-color: var(--galaxy); + --md-default-fg-color: var(--white); + --md-default-fg-color--light: var(--white); + --md-default-fg-color--lighter: var(--white); + --md-primary-fg-color: var(--space); + --md-primary-bg-color: var(--white); + --md-accent-fg-color: var(--radiate); + + --md-typeset-color: var(--white); + --md-typeset-a-color: var(--radiate); + --md-typeset-mark-color: var(--sun); + + --md-code-fg-color: var(--white); + --md-code-bg-color: var(--space); + + --md-code-hl-comment-color: var(--asteroid); + --md-code-hl-punctuation-color: var(--asteroid); + --md-code-hl-generic-color: var(--supernova); + --md-code-hl-variable-color: var(--starlight); + --md-code-hl-string-color: var(--radiate); + --md-code-hl-keyword-color: var(--supernova); + --md-code-hl-operator-color: var(--supernova); + --md-code-hl-number-color: var(--electron); + --md-code-hl-special-color: var(--electron); + --md-code-hl-function-color: var(--neutron); + --md-code-hl-constant-color: var(--radiate); + --md-code-hl-name-color: var(--md-code-fg-color); + + --md-typeset-del-color: hsla(6, 90%, 60%, 0.15); + --md-typeset-ins-color: hsla(150, 90%, 44%, 0.15); + + --md-typeset-table-color: hsla(0, 0%, 100%, 0.12); + --md-typeset-table-color--light: hsla(0, 0%, 100%, 0.035); +} + +[data-md-color-scheme="astral-light"] img[src$="#only-dark"], +[data-md-color-scheme="astral-light"] img[src$="#gh-dark-mode-only"] { + display: none; /* Hide dark images in light mode */ +} + +[data-md-color-scheme="astral-light"] img[src$="#only-light"], +[data-md-color-scheme="astral-light"] img[src$="#gh-light-mode-only"] { + display: inline; /* Show light images in light mode */ +} + +[data-md-color-scheme="astral-dark"] img[src$="#only-light"], +[data-md-color-scheme="astral-dark"] img[src$="#gh-light-mode-only"] { + display: none; /* Hide light images in dark mode */ +} + +[data-md-color-scheme="astral-dark"] img[src$="#only-dark"], +[data-md-color-scheme="astral-dark"] img[src$="#gh-dark-mode-only"] { + display: inline; /* Show dark images in dark mode */ +} + +/* See: https://github.com/squidfunk/mkdocs-material/issues/175#issuecomment-616694465 */ +.md-typeset__table { + min-width: 100%; +} +.md-typeset table:not([class]) { + display: table; +} diff --git a/docs/tutorial.md b/docs/tutorial.md index 45488161f9..d8e87368a1 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -26,7 +26,7 @@ def sum_even_numbers(numbers: List[int]) -> int: return sum(num for num in numbers if num % 2 == 0) ``` -To start, we'll install Ruff through PyPI (or with our [preferred package manager](installation.md)): +To start, we'll install Ruff through PyPI (or with your [preferred package manager](installation.md)): ```shell > pip install ruff @@ -46,7 +46,7 @@ Ruff identified an unused import, which is a common error in Python code. Ruff c ```shell ❯ ruff check --fix . -Found 1 error (1 fixed, 0 renumbersing). +Found 1 error (1 fixed, 0 remaining). ``` Running `git diff` shows the following: @@ -242,7 +242,7 @@ This tutorial has focused on Ruff's command-line interface, but Ruff can also be ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.0.272 + rev: v0.0.278 hooks: - id: ruff ``` diff --git a/docs/usage.md b/docs/usage.md index 6e41618375..6dfcbedde1 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -22,7 +22,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com) hook: ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.0.272 + rev: v0.0.278 hooks: - id: ruff ``` @@ -32,12 +32,23 @@ Or, to enable autofix: ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.0.272 + rev: v0.0.278 hooks: - id: ruff args: [ --fix, --exit-non-zero-on-fix ] ``` +Or, to run the hook on Jupyter Notebooks too: + +```yaml +- repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.0.278 + hooks: + - id: ruff + types_or: [python, pyi, jupyter] +``` + Ruff's pre-commit hook should be placed after other formatting tools, such as Black and isort, _unless_ you enable autofix, in which case, Ruff's pre-commit hook should run _before_ Black, isort, and other formatting tools, as Ruff's autofix behavior can output code changes that require diff --git a/fuzz/.gitignore b/fuzz/.gitignore index 0ee1bebe7f..0aae9a3e34 100644 --- a/fuzz/.gitignore +++ b/fuzz/.gitignore @@ -1,2 +1,3 @@ artifacts/ corpus/ruff_fix_validity +Cargo.lock diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock deleted file mode 100644 index 677868be0c..0000000000 --- a/fuzz/Cargo.lock +++ /dev/null @@ -1,1965 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "Inflector" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" - -[[package]] -name = "aho-corasick" -version = "0.7.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" -dependencies = [ - "memchr", -] - -[[package]] -name = "aho-corasick" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" -dependencies = [ - "memchr", -] - -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "annotate-snippets" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7021ce4924a3f25f802b2cccd1af585e39ea1a363a1aa2e72afe54b67a3a7a7" - -[[package]] -name = "annotate-snippets" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3b9d411ecbaf79885c6df4d75fff75858d5995ff25385657a28af47e82f9c36" -dependencies = [ - "unicode-width", - "yansi-term", -] - -[[package]] -name = "anstream" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is-terminal", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" - -[[package]] -name = "anstyle-parse" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "anstyle-wincon" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" -dependencies = [ - "anstyle", - "windows-sys", -] - -[[package]] -name = "anyhow" -version = "1.0.71" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" - -[[package]] -name = "arbitrary" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d098ff73c1ca148721f37baad5ea6a465a13f9573aba8641fbbbae8164a54e" -dependencies = [ - "derive_arbitrary", -] - -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi 0.1.19", - "libc", - "winapi", -] - -[[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6776fc96284a0bb647b615056fc496d1fe1644a7ab01829818a6d91cae888b84" - -[[package]] -name = "bstr" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a246e68bb43f6cd9db24bea052a53e40405417c5fb372e3d1a8a7f770a564ef5" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "bumpalo" -version = "3.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" - -[[package]] -name = "cc" -version = "1.0.79" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" -dependencies = [ - "jobserver", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "chic" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5b5db619f3556839cb2223ae86ff3f9a09da2c5013be42bc9af08c9589bf70c" -dependencies = [ - "annotate-snippets 0.6.1", -] - -[[package]] -name = "chrono" -version = "0.4.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" -dependencies = [ - "android-tzdata", - "iana-time-zone", - "js-sys", - "num-traits", - "time", - "wasm-bindgen", - "winapi", -] - -[[package]] -name = "clap" -version = "4.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "401a4694d2bf92537b6867d94de48c4842089645fdcdf6c71865b175d836e9c2" -dependencies = [ - "clap_builder", - "clap_derive", - "once_cell", -] - -[[package]] -name = "clap_builder" -version = "4.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72394f3339a76daf211e57d4bcb374410f3965dcc606dd0e03738c7888766980" -dependencies = [ - "anstream", - "anstyle", - "bitflags 1.3.2", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8cd2b2a819ad6eec39e8f1d6b53001af1e5469f8c177579cdaeb313115b825f" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.18", -] - -[[package]] -name = "clap_lex" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" - -[[package]] -name = "colorchoice" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" - -[[package]] -name = "colored" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" -dependencies = [ - "atty", - "lazy_static", - "winapi", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" - -[[package]] -name = "countme" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" - -[[package]] -name = "crunchy" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" - -[[package]] -name = "derive_arbitrary" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e0efad4403bfc52dc201159c4b842a246a14b98c64b55dfd0f2d89729dfeb8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.18", -] - -[[package]] -name = "dirs" -version = "5.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" -dependencies = [ - "libc", - "option-ext", - "redox_users", - "windows-sys", -] - -[[package]] -name = "drop_bomb" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bda8e21c04aca2ae33ffc2fd8c23134f3cac46db123ba97bd9d3f3b8a4a85e1" - -[[package]] -name = "either" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" - -[[package]] -name = "errno" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" -dependencies = [ - "errno-dragonfly", - "libc", - "windows-sys", -] - -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "fern" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9f0c14694cbd524c8720dd69b0e3179344f04ebb5f90f2e4a440c6ea3b2f1ee" -dependencies = [ - "log", -] - -[[package]] -name = "filetime" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "windows-sys", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "form_urlencoded" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "getrandom" -version = "0.2.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.11.0+wasi-snapshot-preview1", -] - -[[package]] -name = "glob" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" - -[[package]] -name = "globset" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc" -dependencies = [ - "aho-corasick 0.7.20", - "bstr", - "fnv", - "log", - "regex", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "hermit-abi" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - -[[package]] -name = "hermit-abi" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" - -[[package]] -name = "hexf-parse" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" - -[[package]] -name = "iana-time-zone" -version = "0.1.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "idna" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" -dependencies = [ - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "ignore" -version = "0.4.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbe7873dab538a9a44ad79ede1faf5f30d49f9a5c883ddbab48bce81b64b7492" -dependencies = [ - "globset", - "lazy_static", - "log", - "memchr", - "regex", - "same-file", - "thread_local", - "walkdir", - "winapi-util", -] - -[[package]] -name = "imperative" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f92123bf2fe0d9f1b5df1964727b970ca3b2d0203d47cf97fb1f36d856b6398" -dependencies = [ - "phf", - "rust-stemmers", -] - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown", - "serde", -] - -[[package]] -name = "io-lifetimes" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" -dependencies = [ - "hermit-abi 0.3.1", - "libc", - "windows-sys", -] - -[[package]] -name = "is-macro" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7d079e129b77477a49c5c4f1cfe9ce6c2c909ef52520693e8e811a714c7b20" -dependencies = [ - "Inflector", - "pmutil", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "is-terminal" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" -dependencies = [ - "hermit-abi 0.3.1", - "io-lifetimes", - "rustix", - "windows-sys", -] - -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" - -[[package]] -name = "jobserver" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" -dependencies = [ - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f37a4a5928311ac501dee68b3c7613a1037d0edb30c8e5427bd832d55d1b790" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "lalrpop-util" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f35c735096c0293d313e8f2a641627472b83d01b937177fe76e5e2708d31e0d" - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - -[[package]] -name = "lexical-parse-float" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683b3a5ebd0130b8fb52ba0bdc718cc56815b6a097e28ae5a6997d0ad17dc05f" -dependencies = [ - "lexical-parse-integer", - "lexical-util", - "static_assertions", -] - -[[package]] -name = "lexical-parse-integer" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d0994485ed0c312f6d965766754ea177d07f9c00c9b82a5ee62ed5b47945ee9" -dependencies = [ - "lexical-util", - "static_assertions", -] - -[[package]] -name = "lexical-util" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5255b9ff16ff898710eb9eb63cb39248ea8a5bb036bea8085b1a767ff6c4e3fc" -dependencies = [ - "static_assertions", -] - -[[package]] -name = "libc" -version = "0.2.146" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b" - -[[package]] -name = "libcst" -version = "0.1.0" -source = "git+https://github.com/charliermarsh/LibCST?rev=80e4c1399f95e5beb532fdd1e209ad2dbb470438#80e4c1399f95e5beb532fdd1e209ad2dbb470438" -dependencies = [ - "chic", - "itertools", - "libcst_derive", - "once_cell", - "paste", - "peg", - "regex", - "thiserror", -] - -[[package]] -name = "libcst_derive" -version = "0.1.0" -source = "git+https://github.com/charliermarsh/LibCST?rev=80e4c1399f95e5beb532fdd1e209ad2dbb470438#80e4c1399f95e5beb532fdd1e209ad2dbb470438" -dependencies = [ - "quote", - "syn 1.0.109", -] - -[[package]] -name = "libfuzzer-sys" -version = "0.4.6" -source = "git+https://github.com/rust-fuzz/libfuzzer#1221c356e993b9f82d1ccd152f1c7636468758d2" -dependencies = [ - "arbitrary", - "cc", - "once_cell", -] - -[[package]] -name = "linux-raw-sys" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" - -[[package]] -name = "log" -version = "0.4.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de" - -[[package]] -name = "matches" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" - -[[package]] -name = "memchr" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" - -[[package]] -name = "natord" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "308d96db8debc727c3fd9744aac51751243420e46edf401010908da7f8d5e57c" - -[[package]] -name = "nextest-workspace-hack" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d906846a98739ed9d73d66e62c2641eef8321f1734b7a1156ab045a0248fb2b3" - -[[package]] -name = "nohash-hasher" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" - -[[package]] -name = "num-bigint" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-integer" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" -dependencies = [ - "autocfg", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" -dependencies = [ - "autocfg", -] - -[[package]] -name = "once_cell" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" - -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - -[[package]] -name = "paste" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" - -[[package]] -name = "path-absolutize" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43eb3595c63a214e1b37b44f44b0a84900ef7ae0b4c5efce59e123d246d7a0de" -dependencies = [ - "path-dedot", -] - -[[package]] -name = "path-dedot" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d55e486337acb9973cdea3ec5638c1b3bcb22e573b2b7b41969e0c744d5a15e" -dependencies = [ - "once_cell", -] - -[[package]] -name = "pathdiff" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" - -[[package]] -name = "peg" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a07f2cafdc3babeebc087e499118343442b742cc7c31b4d054682cc598508554" -dependencies = [ - "peg-macros", - "peg-runtime", -] - -[[package]] -name = "peg-macros" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a90084dc05cf0428428e3d12399f39faad19b0909f64fb9170c9fdd6d9cd49b" -dependencies = [ - "peg-runtime", - "proc-macro2", - "quote", -] - -[[package]] -name = "peg-runtime" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa00462b37ead6d11a82c9d568b26682d78e0477dc02d1966c013af80969739" - -[[package]] -name = "pep440_rs" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe1d15693a11422cfa7d401b00dc9ae9fb8edbfbcb711a77130663f4ddf67650" -dependencies = [ - "lazy_static", - "regex", - "serde", - "tracing", - "unicode-width", -] - -[[package]] -name = "pep508_rs" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "969679a29dfdc8278a449f75b3dd45edf57e649bd59f7502429c2840751c46d8" -dependencies = [ - "once_cell", - "pep440_rs", - "regex", - "serde", - "thiserror", - "tracing", - "unicode-width", - "url", -] - -[[package]] -name = "percent-encoding" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" - -[[package]] -name = "phf" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928c6535de93548188ef63bb7c4036bd415cd8f36ad25af44b9789b2ee72a48c" -dependencies = [ - "phf_shared", -] - -[[package]] -name = "phf_codegen" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56ac890c5e3ca598bbdeaa99964edb5b0258a583a9eb6ef4e89fc85d9224770" -dependencies = [ - "phf_generator", - "phf_shared", -] - -[[package]] -name = "phf_generator" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1181c94580fa345f50f19d738aaa39c0ed30a600d95cb2d3e23f94266f14fbf" -dependencies = [ - "phf_shared", - "rand", -] - -[[package]] -name = "phf_shared" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1fb5f6f826b772a8d4c0394209441e7d37cbbb967ae9c7e0e8134365c9ee676" -dependencies = [ - "siphasher", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" - -[[package]] -name = "pmutil" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3894e5d549cccbe44afecf72922f277f603cd4bb0219c8342631ef18fffbe004" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "proc-macro2" -version = "1.0.59" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "pyproject-toml" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04dbbb336bd88583943c7cd973a32fed323578243a7569f40cb0c7da673321b" -dependencies = [ - "indexmap", - "pep440_rs", - "pep508_rs", - "serde", - "toml", -] - -[[package]] -name = "quick-junit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b909fe9bf2abb1e3d6a97c9189a37c8105c61d03dca9ce6aace023e7d682bd" -dependencies = [ - "chrono", - "indexmap", - "nextest-workspace-hack", - "quick-xml", - "thiserror", - "uuid", -] - -[[package]] -name = "quick-xml" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f50b1c63b38611e7d4d7f68b82d3ad0cc71a2ad2e7f61fc10f1328d917c93cd" -dependencies = [ - "memchr", -] - -[[package]] -name = "quote" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" - -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_users" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" -dependencies = [ - "getrandom", - "redox_syscall", - "thiserror", -] - -[[package]] -name = "regex" -version = "1.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" -dependencies = [ - "aho-corasick 1.0.2", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" - -[[package]] -name = "result-like" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccc7ce6435c33898517a30e85578cd204cbb696875efb93dec19a2d31294f810" -dependencies = [ - "result-like-derive", -] - -[[package]] -name = "result-like-derive" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fabf0a2e54f711c68c50d49f648a1a8a37adcb57353f518ac4df374f0788f42" -dependencies = [ - "pmutil", - "proc-macro2", - "quote", - "syn 1.0.109", - "syn-ext", -] - -[[package]] -name = "ruff" -version = "0.0.272" -dependencies = [ - "annotate-snippets 0.9.1", - "anyhow", - "bitflags 2.3.1", - "chrono", - "colored", - "dirs", - "fern", - "glob", - "globset", - "ignore", - "imperative", - "is-macro", - "itertools", - "libcst", - "log", - "natord", - "nohash-hasher", - "num-bigint", - "num-traits", - "once_cell", - "path-absolutize", - "pathdiff", - "pep440_rs", - "pyproject-toml", - "quick-junit", - "regex", - "result-like", - "ruff_cache", - "ruff_diagnostics", - "ruff_macros", - "ruff_python_whitespace", - "ruff_python_ast", - "ruff_python_semantic", - "ruff_python_stdlib", - "ruff_rustpython", - "ruff_text_size", - "ruff_textwrap", - "rustc-hash", - "rustpython-format", - "rustpython-parser", - "semver", - "serde", - "serde_json", - "shellexpand", - "similar", - "smallvec", - "strum", - "strum_macros", - "thiserror", - "toml", - "typed-arena", - "unicode-width", - "unicode_names2", -] - -[[package]] -name = "ruff-fuzz" -version = "0.0.0" -dependencies = [ - "arbitrary", - "libfuzzer-sys", - "ruff", - "ruff_python_ast", - "ruff_python_formatter", - "similar", -] - -[[package]] -name = "ruff_cache" -version = "0.0.0" -dependencies = [ - "filetime", - "globset", - "itertools", - "regex", -] - -[[package]] -name = "ruff_diagnostics" -version = "0.0.0" -dependencies = [ - "anyhow", - "log", - "ruff_text_size", - "serde", -] - -[[package]] -name = "ruff_formatter" -version = "0.0.0" -dependencies = [ - "drop_bomb", - "ruff_text_size", - "rustc-hash", - "static_assertions", - "tracing", - "unicode-width", -] - -[[package]] -name = "ruff_index" -version = "0.0.0" -dependencies = [ - "ruff_macros", -] - -[[package]] -name = "ruff_macros" -version = "0.0.0" -dependencies = [ - "itertools", - "proc-macro2", - "quote", - "ruff_textwrap", - "syn 2.0.18", -] - -[[package]] -name = "ruff_python_whitespace" -version = "0.0.0" -dependencies = [ - "memchr", - "ruff_text_size", -] - -[[package]] -name = "ruff_python_ast" -version = "0.0.0" -dependencies = [ - "anyhow", - "bitflags 2.3.1", - "is-macro", - "itertools", - "log", - "memchr", - "num-bigint", - "num-traits", - "once_cell", - "ruff_python_whitespace", - "ruff_text_size", - "rustc-hash", - "rustpython-ast", - "rustpython-literal", - "rustpython-parser", - "serde", - "smallvec", -] - -[[package]] -name = "ruff_python_formatter" -version = "0.0.0" -dependencies = [ - "anyhow", - "clap", - "countme", - "is-macro", - "itertools", - "once_cell", - "ruff_formatter", - "ruff_python_whitespace", - "ruff_python_ast", - "ruff_text_size", - "rustc-hash", - "rustpython-parser", -] - -[[package]] -name = "ruff_python_semantic" -version = "0.0.0" -dependencies = [ - "bitflags 2.3.1", - "is-macro", - "nohash-hasher", - "num-traits", - "ruff_index", - "ruff_python_ast", - "ruff_python_stdlib", - "ruff_text_size", - "rustc-hash", - "rustpython-parser", - "smallvec", -] - -[[package]] -name = "ruff_python_stdlib" -version = "0.0.0" -dependencies = [ - "once_cell", - "rustc-hash", -] - -[[package]] -name = "ruff_rustpython" -version = "0.0.0" -dependencies = [ - "anyhow", - "rustpython-parser", -] - -[[package]] -name = "ruff_text_size" -version = "0.0.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd#7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd" -dependencies = [ - "serde", -] - -[[package]] -name = "ruff_textwrap" -version = "0.0.0" -dependencies = [ - "ruff_python_whitespace", - "ruff_text_size", -] - -[[package]] -name = "rust-stemmers" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e46a2036019fdb888131db7a4c847a1063a7493f971ed94ea82c67eada63ca54" -dependencies = [ - "serde", - "serde_derive", -] - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - -[[package]] -name = "rustix" -version = "0.37.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" -dependencies = [ - "bitflags 1.3.2", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys", - "windows-sys", -] - -[[package]] -name = "rustpython-ast" -version = "0.2.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd#7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd" -dependencies = [ - "is-macro", - "num-bigint", - "rustpython-parser-core", - "static_assertions", -] - -[[package]] -name = "rustpython-format" -version = "0.2.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd#7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd" -dependencies = [ - "bitflags 2.3.1", - "itertools", - "num-bigint", - "num-traits", - "rustpython-literal", -] - -[[package]] -name = "rustpython-literal" -version = "0.2.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd#7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd" -dependencies = [ - "hexf-parse", - "is-macro", - "lexical-parse-float", - "num-traits", - "unic-ucd-category", -] - -[[package]] -name = "rustpython-parser" -version = "0.2.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd#7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd" -dependencies = [ - "anyhow", - "is-macro", - "itertools", - "lalrpop-util", - "log", - "num-bigint", - "num-traits", - "phf", - "phf_codegen", - "rustc-hash", - "rustpython-ast", - "rustpython-parser-core", - "tiny-keccak", - "unic-emoji-char", - "unic-ucd-ident", - "unicode_names2", -] - -[[package]] -name = "rustpython-parser-core" -version = "0.2.0" -source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd#7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd" -dependencies = [ - "is-macro", - "ruff_text_size", -] - -[[package]] -name = "rustversion" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06" - -[[package]] -name = "ryu" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "semver" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" - -[[package]] -name = "serde" -version = "1.0.163" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.163" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.18", -] - -[[package]] -name = "serde_json" -version = "1.0.96" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" -dependencies = [ - "indexmap", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_spanned" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93107647184f6027e3b7dcb2e11034cf95ffa1e3a682c67951963ac69c1c007d" -dependencies = [ - "serde", -] - -[[package]] -name = "shellexpand" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da03fa3b94cc19e3ebfc88c4229c49d8f08cdbd1228870a45f0ffdf84988e14b" -dependencies = [ - "dirs", -] - -[[package]] -name = "similar" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420acb44afdae038210c99e69aae24109f32f15500aa708e81d46c9f29d55fcf" - -[[package]] -name = "siphasher" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" - -[[package]] -name = "smallvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - -[[package]] -name = "strum" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.24.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "rustversion", - "syn 1.0.109", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn-ext" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b86cb2b68c5b3c078cac02588bc23f3c04bb828c5d3aedd17980876ec6a7be6" -dependencies = [ - "syn 1.0.109", -] - -[[package]] -name = "thiserror" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.18", -] - -[[package]] -name = "thread_local" -version = "1.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" -dependencies = [ - "cfg-if", - "once_cell", -] - -[[package]] -name = "time" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" -dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", -] - -[[package]] -name = "tiny-keccak" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" -dependencies = [ - "crunchy", -] - -[[package]] -name = "tinyvec" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "toml" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6135d499e69981f9ff0ef2167955a5333c35e36f6937d382974566b3d5b94ec" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a76a9312f5ba4c2dec6b9161fdf25d87ad8a09256ccea5a556fef03c706a10f" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.19.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380d56e8670370eee6566b0bfd4265f65b3f432e8c6d85623f728d4fa31f739" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "winnow", -] - -[[package]] -name = "tracing" -version = "0.1.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" -dependencies = [ - "cfg-if", - "log", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.18", -] - -[[package]] -name = "tracing-core" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" -dependencies = [ - "once_cell", -] - -[[package]] -name = "typed-arena" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" - -[[package]] -name = "unic-char-property" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" -dependencies = [ - "unic-char-range", -] - -[[package]] -name = "unic-char-range" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" - -[[package]] -name = "unic-common" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" - -[[package]] -name = "unic-emoji-char" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b07221e68897210270a38bde4babb655869637af0f69407f96053a34f76494d" -dependencies = [ - "unic-char-property", - "unic-char-range", - "unic-ucd-version", -] - -[[package]] -name = "unic-ucd-category" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8d4591f5fcfe1bd4453baaf803c40e1b1e69ff8455c47620440b46efef91c0" -dependencies = [ - "matches", - "unic-char-property", - "unic-char-range", - "unic-ucd-version", -] - -[[package]] -name = "unic-ucd-ident" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" -dependencies = [ - "unic-char-property", - "unic-char-range", - "unic-ucd-version", -] - -[[package]] -name = "unic-ucd-version" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" -dependencies = [ - "unic-common", -] - -[[package]] -name = "unicode-bidi" -version = "0.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" - -[[package]] -name = "unicode-ident" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" - -[[package]] -name = "unicode-normalization" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "unicode-width" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" - -[[package]] -name = "unicode_names2" -version = "0.6.0" -source = "git+https://github.com/youknowone/unicode_names2.git?rev=4ce16aa85cbcdd9cc830410f1a72ef9a235f2fde#4ce16aa85cbcdd9cc830410f1a72ef9a235f2fde" -dependencies = [ - "phf", -] - -[[package]] -name = "url" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "utf8parse" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" - -[[package]] -name = "uuid" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2" - -[[package]] -name = "walkdir" -version = "2.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasm-bindgen" -version = "0.2.86" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bba0e8cb82ba49ff4e229459ff22a191bbe9a1cb3a341610c9c33efc27ddf73" -dependencies = [ - "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.86" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b04bc93f9d6bdee709f6bd2118f57dd6679cf1176a1af464fca3ab0d66d8fb" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn 2.0.18", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.86" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14d6b024f1a526bb0234f52840389927257beb670610081360e5a03c5df9c258" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.86" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.18", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.86" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" - -[[package]] -name = "winnow" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61de7bac303dc551fe038e2b3cef0f571087a47571ea6e79a87692ac99b99699" -dependencies = [ - "memchr", -] - -[[package]] -name = "yansi-term" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe5c30ade05e61656247b2e334a031dfd0cc466fadef865bdcdea8d537951bf1" -dependencies = [ - "winapi", -] diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 2e00df5938..6b24114ad4 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -24,6 +24,9 @@ ruff_python_ast = { path = "../crates/ruff_python_ast" } ruff_python_formatter = { path = "../crates/ruff_python_formatter" } similar = { version = "2.2.1" } +# Current tag: v0.0.7 +rustpython-parser = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "c174bbf1f29527edd43d432326327f16f47ab9e0" , default-features = false, features = ["full-lexer", "num-bigint"] } + # Prevent this from interfering with workspaces [workspace] members = ["."] diff --git a/fuzz/README.md b/fuzz/README.md index 42907fd9f1..406fc4c913 100644 --- a/fuzz/README.md +++ b/fuzz/README.md @@ -12,6 +12,8 @@ To use the fuzzers provided in this directory, start by invoking: This will install [`cargo-fuzz`](https://github.com/rust-fuzz/cargo-fuzz) and optionally download a [dataset](https://zenodo.org/record/3628784) which improves the efficacy of the testing. +**This step is necessary for initialising the corpus directory, as all fuzzers share a common +corpus.** The dataset may take several hours to download and clean, so if you're just looking to try out the fuzzers, skip the dataset download, though be warned that some features simply cannot be tested without it (very unlikely for the fuzzer to generate valid python code from "thin air"). @@ -22,6 +24,8 @@ Once you have initialised the fuzzers, you can then execute any fuzzer with: cargo fuzz run -s none name_of_fuzzer -- -timeout=1 ``` +**Users using Apple M1 devices must use a nightly compiler and omit the `-s none` portion of this +command, as this architecture does not support fuzzing without a sanitizer.** You can view the names of the available fuzzers with `cargo fuzz list`. For specific details about how each fuzzer works, please read this document in its entirety. @@ -74,6 +78,8 @@ itself, each harness is briefly described below. This fuzz harness does not perform any "smart" testing of Ruff; it merely checks that the parsing and unparsing of a particular input (what would normally be a source code file) does not crash. +It also attempts to verify that the locations of tokens and errors identified do not fall in the +middle of a UTF-8 code point, which may cause downstream panics. While this is unlikely to find any issues on its own, it executes very quickly and covers a large and diverse code region that may speed up the generation of inputs and therefore make a more valuable corpus quickly. @@ -95,11 +101,3 @@ This fuzz harness checks that fixes applied by Ruff do not introduce new errors [`ruff::test::test_snippet`](../crates/ruff/src/test.rs) testing utility. It currently is only configured to use default settings, but may be extended in future versions to test non-default linter settings. - -## Experimental settings - -You can optionally use `--no-default-features --features libafl` to use the libafl fuzzer instead of -libfuzzer. -This fuzzer has experimental support, but can vastly improve fuzzer performance. -If you are not already familiar with [LibAFL](https://github.com/AFLplusplus/LibAFL), this mode is -not currently recommended. diff --git a/fuzz/fuzz_targets/ruff_parse_simple.rs b/fuzz/fuzz_targets/ruff_parse_simple.rs index e685f73857..9eda53e05f 100644 --- a/fuzz/fuzz_targets/ruff_parse_simple.rs +++ b/fuzz/fuzz_targets/ruff_parse_simple.rs @@ -4,13 +4,60 @@ #![no_main] use libfuzzer_sys::{fuzz_target, Corpus}; -use ruff_python_ast::source_code::round_trip; +use ruff_python_ast::source_code::{Generator, Locator, Stylist}; +use rustpython_parser::ast::Suite; +use rustpython_parser::{lexer, Mode, Parse, ParseError}; fn do_fuzz(case: &[u8]) -> Corpus { let Ok(code) = std::str::from_utf8(case) else { return Corpus::Reject; }; // just round-trip it once to trigger both parse and unparse - let _ = round_trip(code, "fuzzed-source.py"); + let locator = Locator::new(code); + let python_ast = match Suite::parse(code, "fuzzed-source.py") { + Ok(stmts) => stmts, + Err(ParseError { offset, .. }) => { + let offset = offset.to_usize(); + assert!( + code.is_char_boundary(offset), + "Invalid error location {} (not at char boundary)", + offset + ); + return Corpus::Keep; + } + }; + + let tokens: Vec<_> = lexer::lex(code, Mode::Module).collect(); + + for maybe_token in tokens.iter() { + match maybe_token.as_ref() { + Ok((_, range)) => { + let start = range.start().to_usize(); + let end = range.end().to_usize(); + assert!( + code.is_char_boundary(start), + "Invalid start position {} (not at char boundary)", + start + ); + assert!( + code.is_char_boundary(end), + "Invalid end position {} (not at char boundary)", + end + ); + } + Err(err) => { + let offset = err.location.to_usize(); + assert!( + code.is_char_boundary(offset), + "Invalid error location {} (not at char boundary)", + offset + ); + } + } + } + + let stylist = Stylist::from_tokens(&tokens, &locator); + let mut generator: Generator = (&stylist).into(); + generator.unparse_suite(&python_ast); Corpus::Keep } diff --git a/fuzz/init-fuzzer.sh b/fuzz/init-fuzzer.sh index eb7e026505..cc99cdee27 100644 --- a/fuzz/init-fuzzer.sh +++ b/fuzz/init-fuzzer.sh @@ -17,6 +17,7 @@ if [ ! -d corpus/ruff_fix_validity ]; then if [[ $REPLY =~ ^[Yy]$ ]]; then curl -L 'https://zenodo.org/record/3628784/files/python-corpus.tar.gz?download=1' | tar xz fi + curl -L 'https://github.com/python/cpython/archive/refs/tags/v3.12.0b2.tar.gz' | tar xz cp -r "../../../crates/ruff/resources/test" . cd - cargo fuzz cmin -s none ruff_fix_validity diff --git a/fuzz/reinit-fuzzer.sh b/fuzz/reinit-fuzzer.sh index a1acb8328f..9ff9fd1ad1 100644 --- a/fuzz/reinit-fuzzer.sh +++ b/fuzz/reinit-fuzzer.sh @@ -6,9 +6,7 @@ SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) cd "$SCRIPT_DIR" cd corpus/ruff_fix_validity -if [[ $REPLY =~ ^[Yy]$ ]]; then - curl -L 'https://zenodo.org/record/3628784/files/python-corpus.tar.gz?download=1' | tar xz -fi +curl -L 'https://github.com/python/cpython/archive/refs/tags/v3.12.0b2.tar.gz' | tar xz cp -r "../../../crates/ruff/resources/test" . cd - cargo fuzz cmin -s none ruff_fix_validity diff --git a/mkdocs.insiders.yml b/mkdocs.insiders.yml new file mode 100644 index 0000000000..7435eb6a01 --- /dev/null +++ b/mkdocs.insiders.yml @@ -0,0 +1,4 @@ +INHERIT: mkdocs.generated.yml +plugins: + - search + - typeset diff --git a/mkdocs.template.yml b/mkdocs.template.yml index c789f2482f..8f4ec08a09 100644 --- a/mkdocs.template.yml +++ b/mkdocs.template.yml @@ -1,10 +1,11 @@ site_name: Ruff theme: name: material - logo: assets/ruff.svg + logo: assets/bolt.svg favicon: assets/ruff-favicon.png features: - navigation.instant + - navigation.instant.prefetch - navigation.tracking - content.code.annotate - toc.integrate @@ -14,18 +15,16 @@ theme: - content.code.copy palette: - media: "(prefers-color-scheme: light)" - scheme: default - primary: red + scheme: astral-light toggle: icon: material/weather-sunny name: Switch to dark mode - media: "(prefers-color-scheme: dark)" - scheme: slate - primary: red + scheme: astral-dark toggle: icon: material/weather-night name: Switch to light mode - custom_dir: .overrides + custom_dir: docs/.overrides repo_url: https://github.com/astral-sh/ruff repo_name: ruff site_author: charliermarsh @@ -50,3 +49,8 @@ markdown_extensions: anchor_linenums: true plugins: - search +extra_css: + - stylesheets/extra.css +extra: + analytics: + provider: fathom diff --git a/playground/src/Editor/SourceEditor.tsx b/playground/src/Editor/SourceEditor.tsx index c039cbf302..492b0836cc 100644 --- a/playground/src/Editor/SourceEditor.tsx +++ b/playground/src/Editor/SourceEditor.tsx @@ -54,7 +54,7 @@ export default function SourceEditor({ provideCodeActions: function (model, position) { const actions = diagnostics .filter((check) => position.startLineNumber === check.location.row) - .filter((check) => check.fix) + .filter(({ fix }) => fix) .map((check) => ({ title: check.fix ? check.fix.message @@ -71,11 +71,11 @@ export default function SourceEditor({ edit: { range: { startLineNumber: edit.location.row, - startColumn: edit.location.column + 1, + startColumn: edit.location.column, endLineNumber: edit.end_location.row, - endColumn: edit.end_location.column + 1, + endColumn: edit.end_location.column, }, - text: edit.content, + text: edit.content || "", }, })), } diff --git a/pyproject.toml b/pyproject.toml index e5f1a2c6f3..498c120a36 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "maturin" [project] name = "ruff" -version = "0.0.272" +version = "0.0.278" description = "An extremely fast Python linter, written in Rust." authors = [{ name = "Charlie Marsh", email = "charlie.r.marsh@gmail.com" }] maintainers = [{ name = "Charlie Marsh", email = "charlie.r.marsh@gmail.com" }] diff --git a/python/ruff/__main__.py b/python/ruff/__main__.py index 9275fcef3d..44384bc14b 100644 --- a/python/ruff/__main__.py +++ b/python/ruff/__main__.py @@ -1,4 +1,5 @@ import os +import subprocess import sys import sysconfig from pathlib import Path @@ -31,4 +32,7 @@ def find_ruff_bin() -> Path: if __name__ == "__main__": ruff = find_ruff_bin() - sys.exit(os.spawnv(os.P_WAIT, ruff, ["ruff", *sys.argv[1:]])) + # Passing a path-like to `subprocess.run()` on windows is only supported in 3.8+, + # but we also support 3.7 + completed_process = subprocess.run([os.fsdecode(ruff), *sys.argv[1:]]) + sys.exit(completed_process.returncode) diff --git a/ruff.schema.json b/ruff.schema.json index 69702b9f17..ac8dfa68b5 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -198,6 +198,17 @@ } ] }, + "flake8-copyright": { + "description": "Options for the `flake8-copyright` plugin.", + "anyOf": [ + { + "$ref": "#/definitions/Flake8CopyrightOptions" + }, + { + "type": "null" + } + ] + }, "flake8-errmsg": { "description": "Options for the `flake8-errmsg` plugin.", "anyOf": [ @@ -344,7 +355,7 @@ ] }, "include": { - "description": "A list of file patterns to include when linting.\n\nInclusion are based on globs, and should be single-path patterns, like `*.pyw`, to include any file with the `.pyw` extension.\n\nFor more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).", + "description": "A list of file patterns to include when linting.\n\nInclusion are based on globs, and should be single-path patterns, like `*.pyw`, to include any file with the `.pyw` extension. `pyproject.toml` is included here not for configuration but because we lint whether e.g. the `[project]` matches the schema.\n\nFor more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).", "type": [ "array", "null" @@ -464,6 +475,17 @@ } ] }, + "pyupgrade": { + "description": "Options for the `pyupgrade` plugin.", + "anyOf": [ + { + "$ref": "#/definitions/PyUpgradeOptions" + }, + { + "type": "null" + } + ] + }, "required-version": { "description": "Require a specific version of Ruff to be running (useful for unifying results across many environments, e.g., with a `pyproject.toml` file).", "anyOf": [ @@ -739,6 +761,35 @@ }, "additionalProperties": false }, + "Flake8CopyrightOptions": { + "type": "object", + "properties": { + "author": { + "description": "Author to enforce within the copyright notice. If provided, the author must be present immediately following the copyright notice.", + "type": [ + "string", + "null" + ] + }, + "min-file-size": { + "description": "A minimum file size (in bytes) required for a copyright notice to be enforced. By default, all files are validated.", + "type": [ + "integer", + "null" + ], + "format": "uint", + "minimum": 0.0 + }, + "notice-rgx": { + "description": "The regular expression used to match the copyright notice, compiled with the [`regex`](https://docs.rs/regex/latest/regex/) crate.\n\nDefaults to `(?i)Copyright\\s+(\\(C\\)\\s+)?\\d{4}(-\\d{4})*`, which matches the following: - `Copyright 2023` - `Copyright (C) 2023` - `Copyright 2021-2023` - `Copyright (C) 2021-2023`", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, "Flake8ErrMsgOptions": { "type": "object", "properties": { @@ -1084,6 +1135,13 @@ "IsortOptions": { "type": "object", "properties": { + "case-sensitive": { + "description": "Sort imports taking into account case sensitivity.", + "type": [ + "boolean", + "null" + ] + }, "classes": { "description": "An override list of tokens to always recognize as a Class for `order-by-type` regardless of casing.", "type": [ @@ -1112,7 +1170,7 @@ } }, "extra-standard-library": { - "description": "A list of modules to consider standard-library, in addition to those known to Ruff in advance.", + "description": "A list of modules to consider standard-library, in addition to those known to Ruff in advance.\n\nSupports glob patterns. For more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).", "type": [ "array", "null" @@ -1163,7 +1221,7 @@ } }, "known-first-party": { - "description": "A list of modules to consider first-party, regardless of whether they can be identified as such via introspection of the local filesystem.", + "description": "A list of modules to consider first-party, regardless of whether they can be identified as such via introspection of the local filesystem.\n\nSupports glob patterns. For more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).", "type": [ "array", "null" @@ -1173,7 +1231,7 @@ } }, "known-local-folder": { - "description": "A list of modules to consider being a local folder. Generally, this is reserved for relative imports (`from . import module`).", + "description": "A list of modules to consider being a local folder. Generally, this is reserved for relative imports (`from . import module`).\n\nSupports glob patterns. For more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).", "type": [ "array", "null" @@ -1183,7 +1241,7 @@ } }, "known-third-party": { - "description": "A list of modules to consider third-party, regardless of whether they can be identified as such via introspection of the local filesystem.", + "description": "A list of modules to consider third-party, regardless of whether they can be identified as such via introspection of the local filesystem.\n\nSupports glob patterns. For more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).", "type": [ "array", "null" @@ -1357,7 +1415,7 @@ } }, "ignore-names": { - "description": "A list of names to ignore when considering `pep8-naming` violations.", + "description": "A list of names (or patterns) to ignore when considering `pep8-naming` violations.", "type": [ "array", "null" @@ -1379,6 +1437,19 @@ }, "additionalProperties": false }, + "PyUpgradeOptions": { + "type": "object", + "properties": { + "keep-runtime-typing": { + "description": "Whether to avoid PEP 585 (`List[int]` -> `list[int]`) and PEP 604 (`Union[str, int]` -> `str | int`) rewrites even if a file imports `from __future__ import annotations`.\n\nThis setting is only applicable when the target Python version is below 3.9 and 3.10 respectively, and is most commonly used when working with libraries like Pydantic and FastAPI, which rely on the ability to parse type annotations at runtime. The use of `from __future__ import annotations` causes Python to treat the type annotations as strings, which typically allows for the use of language features that appear in later Python versions but are not yet supported by the current version (e.g., `str | int`). However, libraries that rely on runtime type annotations will break if the annotations are incompatible with the current Python version.\n\nFor example, while the following is valid Python 3.8 code due to the presence of `from __future__ import annotations`, the use of `str| int` prior to Python 3.10 will cause Pydantic to raise a `TypeError` at runtime:\n\n```python from __future__ import annotations\n\nimport pydantic\n\nclass Foo(pydantic.BaseModel): bar: str | int ```", + "type": [ + "boolean", + "null" + ] + } + }, + "additionalProperties": false + }, "Pycodestyle": { "type": "object", "properties": { @@ -1515,24 +1586,25 @@ "py38", "py39", "py310", - "py311" + "py311", + "py312" ] }, "Quote": { "oneOf": [ - { - "description": "Use single quotes.", - "type": "string", - "enum": [ - "single" - ] - }, { "description": "Use double quotes.", "type": "string", "enum": [ "double" ] + }, + { + "description": "Use single quotes.", + "type": "string", + "enum": [ + "single" + ] } ] }, @@ -1640,6 +1712,7 @@ "B031", "B032", "B033", + "B034", "B9", "B90", "B904", @@ -1679,6 +1752,8 @@ "COM812", "COM818", "COM819", + "CPY", + "CPY001", "D", "D1", "D10", @@ -1999,6 +2074,7 @@ "NPY00", "NPY001", "NPY002", + "NPY003", "PD", "PD0", "PD00", @@ -2017,6 +2093,18 @@ "PD9", "PD90", "PD901", + "PERF", + "PERF1", + "PERF10", + "PERF101", + "PERF102", + "PERF2", + "PERF20", + "PERF203", + "PERF4", + "PERF40", + "PERF401", + "PERF402", "PGH", "PGH0", "PGH00", @@ -2041,15 +2129,19 @@ "PL", "PLC", "PLC0", + "PLC01", + "PLC010", + "PLC0105", + "PLC013", + "PLC0131", + "PLC0132", "PLC02", "PLC020", + "PLC0205", "PLC0208", "PLC04", "PLC041", "PLC0414", - "PLC1", - "PLC19", - "PLC190", "PLC1901", "PLC3", "PLC30", @@ -2130,6 +2222,7 @@ "PLR1701", "PLR171", "PLR1711", + "PLR1714", "PLR172", "PLR1722", "PLR2", @@ -2234,6 +2327,10 @@ "PYI0", "PYI00", "PYI001", + "PYI002", + "PYI003", + "PYI004", + "PYI005", "PYI006", "PYI007", "PYI008", @@ -2253,13 +2350,16 @@ "PYI025", "PYI029", "PYI03", + "PYI030", "PYI032", "PYI033", "PYI034", "PYI035", + "PYI036", "PYI04", "PYI042", "PYI043", + "PYI044", "PYI045", "PYI048", "PYI05", @@ -2303,6 +2403,11 @@ "RUF01", "RUF010", "RUF011", + "RUF012", + "RUF013", + "RUF014", + "RUF015", + "RUF016", "RUF1", "RUF10", "RUF100", @@ -2515,6 +2620,7 @@ "UP036", "UP037", "UP038", + "UP039", "W", "W1", "W19", @@ -2554,6 +2660,7 @@ "enum": [ "text", "json", + "json-lines", "junit", "grouped", "github", diff --git a/scripts/_utils.py b/scripts/_utils.py index 23ac16d57d..7871be6cd9 100644 --- a/scripts/_utils.py +++ b/scripts/_utils.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import re from pathlib import Path diff --git a/scripts/add_plugin.py b/scripts/add_plugin.py index 7cc83974b7..ce9e143a21 100755 --- a/scripts/add_plugin.py +++ b/scripts/add_plugin.py @@ -8,6 +8,7 @@ Example usage: --url https://pypi.org/project/flake8-pie/ --prefix PIE """ +from __future__ import annotations import argparse @@ -44,7 +45,7 @@ mod tests { fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy()); - let messages = test_path( + let diagnostics = test_path( Path::new("%s").join(path).as_path(), &settings::Settings::for_rule(rule_code), )?; diff --git a/scripts/add_rule.py b/scripts/add_rule.py index 3d1eb14c33..3e4c105822 100755 --- a/scripts/add_rule.py +++ b/scripts/add_rule.py @@ -9,6 +9,7 @@ Example usage: --code 807 \ --linter flake8-pie """ +from __future__ import annotations import argparse import subprocess @@ -70,7 +71,7 @@ def main(*, name: str, prefix: str, code: str, linter: str) -> None: contents = rules_mod.read_text() parts = contents.split("\n\n") - new_pub_use = f"pub(crate) use {rule_name_snake}::{{{rule_name_snake}, {name}}}" + new_pub_use = f"pub(crate) use {rule_name_snake}::*" new_mod = f"mod {rule_name_snake};" if len(parts) == 2: @@ -97,6 +98,17 @@ use ruff_macros::{{derive_message_formats, violation}}; use crate::checkers::ast::Checker; +/// ## What it does +/// +/// ## Why is this bad? +/// +/// ## Example +/// ```python +/// ``` +/// +/// Use instead: +/// ```python +/// ``` #[violation] pub struct {name}; diff --git a/scripts/check_docs_formatted.py b/scripts/check_docs_formatted.py index 34068a2e80..f10e38cb0e 100755 --- a/scripts/check_docs_formatted.py +++ b/scripts/check_docs_formatted.py @@ -1,17 +1,22 @@ #!/usr/bin/env python3 """Check code snippets in docs are formatted by black.""" +from __future__ import annotations + import argparse import os import re import textwrap -from collections.abc import Sequence from pathlib import Path from re import Match +from typing import TYPE_CHECKING import black from black.mode import Mode, TargetVersion from black.parsing import InvalidInput +if TYPE_CHECKING: + from collections.abc import Sequence + TARGET_VERSIONS = ["py37", "py38", "py39", "py310", "py311"] SNIPPED_RE = re.compile( r"(?P^(?P *)```\s*python\n)" @@ -28,6 +33,7 @@ KNOWN_FORMATTING_VIOLATIONS = [ "bad-quotes-inline-string", "bad-quotes-multiline-string", "explicit-string-concatenation", + "indent-with-spaces", "indentation-with-invalid-multiple", "line-too-long", "missing-trailing-comma", @@ -39,14 +45,23 @@ KNOWN_FORMATTING_VIOLATIONS = [ "multiple-spaces-before-operator", "multiple-statements-on-one-line-colon", "multiple-statements-on-one-line-semicolon", + "no-blank-line-before-function", "no-indented-block-comment", "no-space-after-block-comment", "no-space-after-inline-comment", + "one-blank-line-after-class", + "over-indentation", "over-indented", "prohibited-trailing-comma", + "shebang-leading-whitespace", + "surrounding-whitespace", "too-few-spaces-before-inline-comment", "trailing-comma-on-bare-tuple", + "triple-single-quotes", + "under-indentation", "unexpected-indentation-comment", + "unicode-kind-prefix", + "unnecessary-class-parentheses", "useless-semicolon", "whitespace-after-open-bracket", "whitespace-before-close-bracket", @@ -171,10 +186,7 @@ def main(argv: Sequence[str] | None = None) -> int: generate_docs() # Get static docs - static_docs = [] - for file in os.listdir("docs"): - if file.endswith(".md"): - static_docs.append(Path("docs") / file) + static_docs = [Path("docs") / f for f in os.listdir("docs") if f.endswith(".md")] # Check rules generated if not Path("docs/rules").exists(): @@ -182,10 +194,9 @@ def main(argv: Sequence[str] | None = None) -> int: return 1 # Get generated rules - generated_docs = [] - for file in os.listdir("docs/rules"): - if file.endswith(".md"): - generated_docs.append(Path("docs/rules") / file) + generated_docs = [ + Path("docs/rules") / f for f in os.listdir("docs/rules") if f.endswith(".md") + ] if len(generated_docs) == 0: print("Please generate rules first.") diff --git a/scripts/check_ecosystem.py b/scripts/check_ecosystem.py index d7afcb764b..ed7c876811 100755 --- a/scripts/check_ecosystem.py +++ b/scripts/check_ecosystem.py @@ -20,7 +20,7 @@ from asyncio.subprocess import PIPE, create_subprocess_exec from contextlib import asynccontextmanager, nullcontext from pathlib import Path from signal import SIGINT, SIGTERM -from typing import TYPE_CHECKING, NamedTuple, Self +from typing import TYPE_CHECKING, NamedTuple, Self, TypeVar if TYPE_CHECKING: from collections.abc import AsyncIterator, Iterator, Sequence @@ -44,12 +44,12 @@ class Repository(NamedTuple): async def clone(self: Self, checkout_dir: Path) -> AsyncIterator[Path]: """Shallow clone this repository to a temporary directory.""" if checkout_dir.exists(): - logger.debug(f"Reusing {self.org}/{self.repo}") - yield Path(checkout_dir) + logger.debug(f"Reusing {self.org}:{self.repo}") + yield await self._get_commit(checkout_dir) return - logger.debug(f"Cloning {self.org}/{self.repo}") - git_command = [ + logger.debug(f"Cloning {self.org}:{self.repo}") + git_clone_command = [ "git", "clone", "--config", @@ -60,24 +60,49 @@ class Repository(NamedTuple): "--no-tags", ] if self.ref: - git_command.extend(["--branch", self.ref]) + git_clone_command.extend(["--branch", self.ref]) - git_command.extend( + git_clone_command.extend( [ f"https://github.com/{self.org}/{self.repo}", checkout_dir, ], ) - process = await create_subprocess_exec(*git_command) + git_clone_process = await create_subprocess_exec( + *git_clone_command, + env={"GIT_TERMINAL_PROMPT": "0"}, + ) - status_code = await process.wait() + status_code = await git_clone_process.wait() logger.debug( f"Finished cloning {self.org}/{self.repo} with status {status_code}", ) + yield await self._get_commit(checkout_dir) - yield Path(checkout_dir) + def url_for(self: Self, commit_sha: str, path: str, lnum: int | None = None) -> str: + """ + Return the GitHub URL for the given commit, path, and line number, if given. + """ + # Default to main branch + url = f"https://github.com/{self.org}/{self.repo}/blob/{commit_sha}/{path}" + if lnum: + url += f"#L{lnum}" + return url + + async def _get_commit(self: Self, checkout_dir: Path) -> str: + """Return the commit sha for the repository in the checkout directory.""" + git_sha_process = await create_subprocess_exec( + *["git", "rev-parse", "HEAD"], + cwd=checkout_dir, + stdout=PIPE, + ) + git_sha_stdout, _ = await git_sha_process.communicate() + assert ( + await git_sha_process.wait() == 0 + ), f"Failed to retrieve commit sha at {checkout_dir}" + return git_sha_stdout.decode().strip() REPOSITORIES: list[Repository] = [ @@ -85,6 +110,9 @@ REPOSITORIES: list[Repository] = [ Repository("bokeh", "bokeh", "branch-3.2", select="ALL"), Repository("pypa", "build", "main"), Repository("pypa", "cibuildwheel", "main"), + Repository("pypa", "setuptools", "main"), + Repository("pypa", "pip", "main"), + Repository("python", "mypy", "master"), Repository("DisnakeDev", "disnake", "master"), Repository("scikit-build", "scikit-build", "main"), Repository("scikit-build", "scikit-build-core", "main"), @@ -152,6 +180,7 @@ class Diff(NamedTuple): removed: set[str] added: set[str] + source_sha: str def __bool__(self: Self) -> bool: """Return true if this diff is non-empty.""" @@ -175,25 +204,24 @@ async def compare( """Check a specific repository against two versions of ruff.""" removed, added = set(), set() - # Allows to keep the checkouts locations + # By the default, the git clone are transient, but if the user provides a + # directory for permanent storage we keep it there if checkouts: - checkout_parent = checkouts.joinpath(repo.org) - # Don't create the repodir itself, we need that for checking for existing - # clones - checkout_parent.mkdir(exist_ok=True, parents=True) - location_context = nullcontext(checkout_parent) + location_context = nullcontext(checkouts) else: location_context = tempfile.TemporaryDirectory() with location_context as checkout_parent: - checkout_dir = Path(checkout_parent).joinpath(repo.repo) - async with repo.clone(checkout_dir) as path: + assert ":" not in repo.org + assert ":" not in repo.repo + checkout_dir = Path(checkout_parent).joinpath(f"{repo.org}:{repo.repo}") + async with repo.clone(checkout_dir) as checkout_sha: try: async with asyncio.TaskGroup() as tg: check1 = tg.create_task( check( ruff=ruff1, - path=path, + path=checkout_dir, name=f"{repo.org}/{repo.repo}", select=repo.select, ignore=repo.ignore, @@ -204,7 +232,7 @@ async def compare( check2 = tg.create_task( check( ruff=ruff2, - path=path, + path=checkout_dir, name=f"{repo.org}/{repo.repo}", select=repo.select, ignore=repo.ignore, @@ -221,7 +249,7 @@ async def compare( elif line.startswith("+ "): added.add(line[2:]) - return Diff(removed, added) + return Diff(removed, added, checkout_sha) def read_projects_jsonl(projects_jsonl: Path) -> dict[tuple[str, str], Repository]: @@ -267,6 +295,14 @@ def read_projects_jsonl(projects_jsonl: Path) -> dict[tuple[str, str], Repositor return repositories +DIFF_LINE_RE = re.compile( + r"^(?P
[+-]) (?P(?P[^:]+):(?P\d+):\d+:) (?P.*)$",
+)
+
+
+T = TypeVar("T")
+
+
 async def main(
     *,
     ruff1: Path,
@@ -282,8 +318,19 @@ async def main(
 
     logger.debug(f"Checking {len(repositories)} projects")
 
+    # https://stackoverflow.com/a/61478547/3549270
+    # Otherwise doing 3k repositories can take >8GB RAM
+    semaphore = asyncio.Semaphore(50)
+
+    async def limited_parallelism(coroutine: T) -> T:
+        async with semaphore:
+            return await coroutine
+
     results = await asyncio.gather(
-        *[compare(ruff1, ruff2, repo, checkouts) for repo in repositories.values()],
+        *[
+            limited_parallelism(compare(ruff1, ruff2, repo, checkouts))
+            for repo in repositories.values()
+        ],
         return_exceptions=True,
     )
 
@@ -333,21 +380,30 @@ async def main(
                 print("

") print() - diff_str = "\n".join(diff) + repo = repositories[(org, repo)] + diff_lines = list(diff) - print("```diff") - print(diff_str) - print("```") + print("

")
+                for line in diff_lines:
+                    match = DIFF_LINE_RE.match(line)
+                    if match is None:
+                        print(line)
+                        continue
+
+                    pre, inner, path, lnum, post = match.groups()
+                    url = repo.url_for(diff.source_sha, path, int(lnum))
+                    print(f"{pre} {inner} {post}")
+                print("
") print() print("

") print("") # Count rule changes - for line in diff_str.splitlines(): + for line in diff_lines: # Find rule change for current line or construction # + /::: - matches = re.search(r": ([A-Z]{1,3}[0-9]{3,4})", line) + matches = re.search(r": ([A-Z]{1,4}[0-9]{3,4})", line) if matches is None: # Handle case where there are no regex matches e.g. @@ -431,6 +487,8 @@ if __name__ == "__main__": logging.basicConfig(level=logging.INFO) loop = asyncio.get_event_loop() + if args.checkouts: + args.checkouts.mkdir(exist_ok=True, parents=True) main_task = asyncio.ensure_future( main( ruff1=args.ruff1, diff --git a/scripts/ecosystem_all_check.py b/scripts/ecosystem_all_check.py index 9f509c57ac..96107c7365 100644 --- a/scripts/ecosystem_all_check.py +++ b/scripts/ecosystem_all_check.py @@ -3,13 +3,14 @@ panics, autofix errors and similar problems. It's a less elaborate, more hacky version of check_ecosystem.py """ +from __future__ import annotations import json import subprocess import sys from pathlib import Path from subprocess import CalledProcessError -from typing import NamedTuple, Optional +from typing import NamedTuple from tqdm import tqdm @@ -19,7 +20,7 @@ class Repository(NamedTuple): org: str repo: str - ref: Optional[str] + ref: str | None def main() -> None: diff --git a/scripts/generate_known_standard_library.py b/scripts/generate_known_standard_library.py index 7fd7ae7222..f35a714d0e 100644 --- a/scripts/generate_known_standard_library.py +++ b/scripts/generate_known_standard_library.py @@ -5,19 +5,21 @@ Source: Only the generation of the file has been modified for use in this project. """ +from __future__ import annotations from pathlib import Path from sphinx.ext.intersphinx import fetch_inventory URL = "https://docs.python.org/{}/objects.inv" -PATH = Path("crates") / "ruff_python" / "src" / "sys.rs" +PATH = Path("crates") / "ruff_python_stdlib" / "src" / "sys.rs" VERSIONS: list[tuple[int, int]] = [ (3, 7), (3, 8), (3, 9), (3, 10), (3, 11), + (3, 12), ] @@ -36,18 +38,16 @@ with PATH.open("w") as f: f.write( """\ //! This file is generated by `scripts/generate_known_standard_library.py` -use once_cell::sync::Lazy; -use rustc_hash::{FxHashMap, FxHashSet}; -// See: https://pycqa.github.io/isort/docs/configuration/options.html#known-standard-library -pub static KNOWN_STANDARD_LIBRARY: Lazy>> = - Lazy::new(|| { - FxHashMap::from_iter([ -""", # noqa: E501 +pub fn is_known_standard_library(minor_version: u32, module: &str) -> bool { + matches!((minor_version, module), +""", ) - for major, minor in VERSIONS: - version = f"{major}.{minor}" - url = URL.format(version) + + modules_by_version = {} + + for major_version, minor_version in VERSIONS: + url = URL.format(f"{major_version}.{minor_version}") invdata = fetch_inventory(FakeApp(), "", url) modules = { @@ -59,33 +59,44 @@ pub static KNOWN_STANDARD_LIBRARY: Lazy 0: + f.write(" | ") + f.write(f'"{module}"') + f.write(")") + f.write("\n") + + # Next, add any version-specific modules. + for _major_version, minor_version in VERSIONS: + version_modules = set.difference( + modules_by_version[minor_version], + ubiquitous_modules, ) + + f.write(" | ") + f.write(f"({minor_version}, ") + for i, module in enumerate(sorted(version_modules)): + if i > 0: + f.write(" | ") + f.write(f'"{module}"') + f.write(")") + f.write("\n") + f.write( """\ - ]) - }); -""", + ) +} + """, ) diff --git a/scripts/generate_mkdocs.py b/scripts/generate_mkdocs.py index 5793858e91..f6954b153a 100644 --- a/scripts/generate_mkdocs.py +++ b/scripts/generate_mkdocs.py @@ -1,4 +1,6 @@ """Generate an MkDocs-compatible `docs` and `mkdocs.yml` from the README.md.""" +from __future__ import annotations + import argparse import re import shutil @@ -30,10 +32,6 @@ SECTIONS: list[Section] = [ Section("Contributing", "contributing.md", generated=True), ] -FATHOM_SCRIPT: str = ( - '" -) LINK_REWRITES: dict[str, str] = { "https://beta.ruff.rs/docs/": "index.md", @@ -43,7 +41,6 @@ LINK_REWRITES: dict[str, str] = { ), "https://beta.ruff.rs/docs/contributing/": "contributing.md", "https://beta.ruff.rs/docs/editor-integrations/": "editor-integrations.md", - "https://beta.ruff.rs/docs/faq/": "faq.md", "https://beta.ruff.rs/docs/faq/#how-does-ruff-compare-to-flake8": ( "faq.md#how-does-ruff-compare-to-flake8" ), @@ -51,7 +48,6 @@ LINK_REWRITES: dict[str, str] = { "https://beta.ruff.rs/docs/rules/": "rules.md", "https://beta.ruff.rs/docs/rules/#error-e": "rules.md#error-e", "https://beta.ruff.rs/docs/settings/": "settings.md", - "https://beta.ruff.rs/docs/usage/": "usage.md", } @@ -90,7 +86,13 @@ def main() -> None: # Rewrite links to the documentation. for src, dst in LINK_REWRITES.items(): - content = content.replace(f"({src})", f"({dst})") + before = content + after = content.replace(f"({src})", f"({dst})") + if before == after: + msg = f"Unexpected link rewrite in README.md: {src}" + raise ValueError(msg) + content = after + if m := re.search(r"\(https://beta.ruff.rs/docs/.*\)", content): msg = f"Unexpected absolute link to documentation: {m.group(0)}" raise ValueError(msg) @@ -138,18 +140,8 @@ def main() -> None: with Path("mkdocs.template.yml").open(encoding="utf8") as fp: config = yaml.safe_load(fp) config["nav"] = [{section.title: section.filename} for section in SECTIONS] - config["extra"] = {"analytics": {"provider": "fathom"}} - Path(".overrides/partials/integrations/analytics").mkdir( - parents=True, - exist_ok=True, - ) - with Path(".overrides/partials/integrations/analytics/fathom.html").open( - "w+", - ) as fp: - fp.write(FATHOM_SCRIPT) - - with Path("mkdocs.yml").open("w+") as fp: + with Path("mkdocs.generated.yml").open("w+") as fp: yaml.safe_dump(config, fp) diff --git a/scripts/pyproject.toml b/scripts/pyproject.toml index c34ba92a24..1a3a318a1a 100644 --- a/scripts/pyproject.toml +++ b/scripts/pyproject.toml @@ -2,7 +2,7 @@ name = "scripts" version = "0.0.1" dependencies = ["sphinx"] -requires-python = ">=3.9" +requires-python = ">=3.8" [tool.black] line-length = 88 @@ -18,7 +18,9 @@ ignore = [ "G", # flake8-logging "T", # flake8-print "FBT", # flake8-boolean-trap + "PERF", # perflint + "ANN401", ] -[tool.ruff.pydocstyle] -convention = "pep257" +[tool.ruff.isort] +required-imports = ["from __future__ import annotations"] diff --git a/scripts/transform_readme.py b/scripts/transform_readme.py index d2afcdf1bf..7170eef20f 100644 --- a/scripts/transform_readme.py +++ b/scripts/transform_readme.py @@ -4,12 +4,14 @@ By default, we assume that our README.md will be rendered on GitHub. However, di targets have different strategies for rendering light- and dark-mode images. This script adjusts the images in the README.md to support the given target. """ +from __future__ import annotations + import argparse from pathlib import Path URL = "https://user-images.githubusercontent.com/1309177/{}.svg" -URL_LIGHT = URL.format("212613257-5f4bca12-6d6b-4c79-9bac-51a4c6d08928") -URL_DARK = URL.format("212613422-7faaf278-706b-4294-ad92-236ffcab3430") +URL_LIGHT = URL.format("232603516-4fb4892d-585c-4b20-b810-3db9161831e4") +URL_DARK = URL.format("232603514-c95e9b0f-6b31-43de-9a80-9e844173fd6a") # https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#specifying-the-theme-an-image-is-shown-to GITHUB = f""" diff --git a/scripts/update_ambiguous_characters.py b/scripts/update_ambiguous_characters.py index 4243313e6e..cf165af585 100644 --- a/scripts/update_ambiguous_characters.py +++ b/scripts/update_ambiguous_characters.py @@ -1,4 +1,6 @@ """Generate the confusables.rs file from the VS Code ambiguous.json file.""" +from __future__ import annotations + import json import subprocess from pathlib import Path @@ -43,9 +45,7 @@ def format_confusables_rs(raw_data: dict[str, list[int]]) -> str: for i in range(0, len(items), 2): flattened_items.add((items[i], items[i + 1])) - tuples = [] - for left, right in sorted(flattened_items): - tuples.append(f" {left}u32 => {right},\n") + tuples = [f" {left}u32 => {right},\n" for left, right in sorted(flattened_items)] print(f"{len(tuples)} confusable tuples.") diff --git a/scripts/update_schemastore.py b/scripts/update_schemastore.py index 9c79d7dc42..b060db31c0 100644 --- a/scripts/update_schemastore.py +++ b/scripts/update_schemastore.py @@ -4,14 +4,15 @@ This script will clone astral-sh/schemastore, update the schema and push the cha to a new branch tagged with the ruff git hash. You should see a URL to create the PR to schemastore in the CLI. """ +from __future__ import annotations import json from pathlib import Path from subprocess import check_call, check_output from tempfile import TemporaryDirectory -schemastore_fork = "https://github.com/astral-sh/schemastore" -schemastore_upstream = "https://github.com/SchemaStore/schemastore" +schemastore_fork = "git@github.com:astral-sh/schemastore.git" +schemastore_upstream = "git@github.com:SchemaStore/schemastore.git" ruff_repo = "https://github.com/astral-sh/ruff" root = Path( check_output(["git", "rev-parse", "--show-toplevel"], text=True).strip(),