diff --git a/.github/renovate.json5 b/.github/renovate.json5 index fc5adae88f6ca..42e836ad3a8c5 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -4,8 +4,9 @@ suppressNotifications: ["prEditedNotification"], extends: ["config:recommended"], labels: ["internal"], - schedule: ["before 4am on Monday"], + schedule: ["on Monday"], separateMajorMinor: false, + prHourlyLimit: 10, enabledManagers: ["github-actions", "pre-commit", "cargo", "pep621", "npm"], cargo: { // See https://docs.renovatebot.com/configuration-options/#rangestrategy @@ -45,6 +46,12 @@ matchPackagePatterns: ["monaco"], description: "Weekly update of the Monaco editor", }, + { + groupName: "strum", + matchManagers: ["cargo"], + matchPackagePatterns: ["strum"], + description: "Weekly update of strum dependencies", + }, ], vulnerabilityAlerts: { commitMessageSuffix: "", diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b8b3415a8027c..ef1f69dd19d34 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -35,7 +35,7 @@ jobs: with: fetch-depth: 0 - - uses: tj-actions/changed-files@v43 + - uses: tj-actions/changed-files@v44 id: changed with: files_yaml: | @@ -525,8 +525,23 @@ jobs: - uses: Swatinem/rust-cache@v2 + # Codspeed comes with a very ancient cargo version (1.66) that resolves features flags differently than what we use now. + # This can result in build failures; see https://github.com/astral-sh/ruff/pull/10700. + # There's a pending codspeed PR to upgrade to a newer cargo version, but until that's merged, we need to use the workaround below. + # https://github.com/CodSpeedHQ/codspeed-rust/pull/31 + # What we do is to call cargo build manually with the correct feature flags and RUSTC settings. We'll have to + # manually maintain the list of benchmarks to run with codspeed (the benefit is that we could detect which benchmarks to run and build based on the changes). + # This is inspired by https://github.com/oxc-project/oxc/blob/a0532adc654039a0c7ead7b35216dfa0b0cb8e8f/.github/workflows/benchmark.yml - name: "Build benchmarks" - run: cargo codspeed build --features codspeed -p ruff_benchmark + env: + RUSTFLAGS: "-C debuginfo=2 -C strip=none -g --cfg codspeed" + shell: bash + # Build all benchmarks, copy the binary to the codspeed directory, remove any `*.d` files that might have been created. + run: | + cargo build --release -p ruff_benchmark --bench parser --bench linter --bench formatter --bench lexer --features=codspeed + mkdir -p ./target/codspeed/ruff_benchmark + cp ./target/release/deps/{lexer,parser,linter,formatter}* target/codspeed/ruff_benchmark/ + rm -rf ./target/codspeed/ruff_benchmark/*.d - name: "Run benchmarks" uses: CodSpeedHQ/action@v2 diff --git a/CHANGELOG.md b/CHANGELOG.md index a150d771cf83d..e92349fca5bee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,54 @@ # Changelog +## 0.3.5 + +### Preview features + +- \[`pylint`\] Implement `modified-iterating-set` (`E4703`) ([#10473](https://github.com/astral-sh/ruff/pull/10473)) +- \[`refurb`\] Implement `for-loop-set-mutations` (`FURB142`) ([#10583](https://github.com/astral-sh/ruff/pull/10583)) +- \[`refurb`\] Implement `unnecessary-from-float` (`FURB164`) ([#10647](https://github.com/astral-sh/ruff/pull/10647)) +- \[`refurb`\] Implement `verbose-decimal-constructor` (`FURB157`) ([#10533](https://github.com/astral-sh/ruff/pull/10533)) + +### Rule changes + +- \[`flake8-comprehensions`\] Handled special case for `C401` which also matches `C416` ([#10596](https://github.com/astral-sh/ruff/pull/10596)) +- \[`flake8-pyi`\] Mark `unaliased-collections-abc-set-import` fix as "safe" for more cases in stub files (`PYI025`) ([#10547](https://github.com/astral-sh/ruff/pull/10547)) +- \[`numpy`\] Add `row_stack` to NumPy 2.0 migration rule ([#10646](https://github.com/astral-sh/ruff/pull/10646)) +- \[`pycodestyle`\] Allow cell magics before an import (`E402`) ([#10545](https://github.com/astral-sh/ruff/pull/10545)) +- \[`pycodestyle`\] Avoid blank line rules for the first logical line in cell ([#10291](https://github.com/astral-sh/ruff/pull/10291)) + +### Configuration + +- Respected nested namespace packages ([#10541](https://github.com/astral-sh/ruff/pull/10541)) +- \[`flake8-boolean-trap`\] Add setting for user defined allowed boolean trap ([#10531](https://github.com/astral-sh/ruff/pull/10531)) + +### Bug fixes + +- Correctly handle references in `__all__` definitions when renaming symbols in autofixes ([#10527](https://github.com/astral-sh/ruff/pull/10527)) +- Track ranges of names inside `__all__` definitions ([#10525](https://github.com/astral-sh/ruff/pull/10525)) +- \[`flake8-bugbear`\] Avoid false positive for usage after `continue` (`B031`) ([#10539](https://github.com/astral-sh/ruff/pull/10539)) +- \[`flake8-copyright`\] Accept commas in default copyright pattern ([#9498](https://github.com/astral-sh/ruff/pull/9498)) +- \[`flake8-datetimez`\] Allow f-strings with `%z` for `DTZ007` ([#10651](https://github.com/astral-sh/ruff/pull/10651)) +- \[`flake8-pytest-style`\] Fix `PT014` autofix for last item in list ([#10532](https://github.com/astral-sh/ruff/pull/10532)) +- \[`flake8-quotes`\] Ignore `Q000`, `Q001` when string is inside forward ref ([#10585](https://github.com/astral-sh/ruff/pull/10585)) +- \[`isort`\] Always place non-relative imports after relative imports ([#10669](https://github.com/astral-sh/ruff/pull/10669)) +- \[`isort`\] Respect Unicode characters in import sorting ([#10529](https://github.com/astral-sh/ruff/pull/10529)) +- \[`pyflakes`\] Fix F821 false negatives when `from __future__ import annotations` is active (attempt 2) ([#10524](https://github.com/astral-sh/ruff/pull/10524)) +- \[`pyflakes`\] Make `unnecessary-lambda` an always-unsafe fix ([#10668](https://github.com/astral-sh/ruff/pull/10668)) +- \[`pylint`\] Fixed false-positive on the rule `PLW1641` (`eq-without-hash`) ([#10566](https://github.com/astral-sh/ruff/pull/10566)) +- \[`ruff`\] Fix panic in unused `# noqa` removal with multi-byte space (`RUF100`) ([#10682](https://github.com/astral-sh/ruff/pull/10682)) + +### Documentation + +- Add PR title format to `CONTRIBUTING.md` ([#10665](https://github.com/astral-sh/ruff/pull/10665)) +- Fix list markup to include blank lines required ([#10591](https://github.com/astral-sh/ruff/pull/10591)) +- Put `flake8-logging` next to the other flake8 plugins in registry ([#10587](https://github.com/astral-sh/ruff/pull/10587)) +- \[`flake8-bandit`\] Update warning message for rule `S305` to address insecure block cipher mode use ([#10602](https://github.com/astral-sh/ruff/pull/10602)) +- \[`flake8-bugbear`\] Document use of anonymous assignment in `useless-expression` ([#10551](https://github.com/astral-sh/ruff/pull/10551)) +- \[`flake8-datetimez`\] Clarify error messages and docs for `DTZ` rules ([#10621](https://github.com/astral-sh/ruff/pull/10621)) +- \[`pycodestyle`\] Use same before vs. after numbers for `space-around-operator` ([#10640](https://github.com/astral-sh/ruff/pull/10640)) +- \[`ruff`\] Change `quadratic-list-summation` docs to use `iadd` consistently ([#10666](https://github.com/astral-sh/ruff/pull/10666)) + ## 0.3.4 ### Preview features @@ -97,7 +146,7 @@ - Fix unstable `with` items formatting ([#10274](https://github.com/astral-sh/ruff/pull/10274)) - Avoid repeating function calls in f-string conversions ([#10265](https://github.com/astral-sh/ruff/pull/10265)) - Fix E203 false positive for slices in format strings ([#10280](https://github.com/astral-sh/ruff/pull/10280)) -- Fix incorrect `Parameter` range for `*args` and `**kwargs` ([#10283](https://github.com/astral-sh/ruff/pull/10283)) +- Fix incorrect `Parameter` range for `*args` and `**kwargs` ([#10283](https://github.com/astral-sh/ruff/pull/10283)) - Treat `typing.Annotated` subscripts as type definitions ([#10285](https://github.com/astral-sh/ruff/pull/10285)) ## 0.3.1 @@ -205,8 +254,7 @@ This release introduces the Ruff 2024.2 style, stabilizing the following changes Highlights include: - Initial support formatting f-strings (in `--preview`). -- Support for overriding arbitrary configuration options via the CLI through an expanded `--config` - argument (e.g., `--config "lint.isort.combine-as-imports=false"`). +- Support for overriding arbitrary configuration options via the CLI through an expanded `--config` argument (e.g., `--config "lint.isort.combine-as-imports=false"`). - Significant performance improvements in Ruff's lexer, parser, and lint rules. ### Preview features @@ -854,7 +902,7 @@ docstrings via the `docstring-code-format` setting. - \[`pylint`\] Default `max-positional-args` to `max-args` ([#8998](https://github.com/astral-sh/ruff/pull/8998)) - \[`pylint`\] Add `allow-dunder-method-names` setting for `bad-dunder-method-name` (`PLW3201`) ([#8812](https://github.com/astral-sh/ruff/pull/8812)) - \[`isort`\] Add support for `from-first` setting ([#8663](https://github.com/astral-sh/ruff/pull/8663)) -- \[`isort`\] Add support for `length-sort` settings ([#8841](https://github.com/astral-sh/ruff/pull/8841)) +- \[`isort`\] Add support for `length-sort` settings ([#8841](https://github.com/astral-sh/ruff/pull/8841)) ### Bug fixes @@ -983,7 +1031,7 @@ docstrings via the `docstring-code-format` setting. - \[`flake8-trio`\] Implement `TRIO115` ([#8486](https://github.com/astral-sh/ruff/pull/8486)) - \[`refurb`\] Implement `type-none-comparison` (`FURB169`) ([#8487](https://github.com/astral-sh/ruff/pull/8487)) - Flag all comparisons against builtin types in `E721` ([#8491](https://github.com/astral-sh/ruff/pull/8491)) -- Make `SIM118` fix as safe when the expression is a known dictionary ([#8525](https://github.com/astral-sh/ruff/pull/8525)) +- Make `SIM118` fix as safe when the expression is a known dictionary ([#8525](https://github.com/astral-sh/ruff/pull/8525)) ### Formatter @@ -1151,7 +1199,7 @@ Try it today with `ruff format`! [Check out the blog post](https://astral.sh/blo - Add `backports.strenum` to `deprecated-imports` ([#8113](https://github.com/astral-sh/ruff/pull/8113)) - Update `SIM112` to ignore `https_proxy`, `http_proxy`, and `no_proxy` ([#8140](https://github.com/astral-sh/ruff/pull/8140)) - Update fix for `literal-membership` (`PLR6201`) to be unsafe ([#8097](https://github.com/astral-sh/ruff/pull/8097)) -- Update fix for `mutable-argument-defaults` (`B006`) to be unsafe ([#8108](https://github.com/astral-sh/ruff/pull/8108)) +- Update fix for `mutable-argument-defaults` (`B006`) to be unsafe ([#8108](https://github.com/astral-sh/ruff/pull/8108)) ### Formatter @@ -1279,7 +1327,7 @@ Read Ruff's new [versioning policy](https://docs.astral.sh/ruff/versioning/). - \[`refurb`\] Add `single-item-membership-test` (`FURB171`) ([#7815](https://github.com/astral-sh/ruff/pull/7815)) - \[`pylint`\] Add `and-or-ternary` (`R1706`) ([#7811](https://github.com/astral-sh/ruff/pull/7811)) -*New rules are added in [preview](https://docs.astral.sh/ruff/preview/).* +_New rules are added in [preview](https://docs.astral.sh/ruff/preview/)._ ### Configuration diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7eca0a04cd72f..c4def89956e56 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -123,8 +123,8 @@ prior to merging. Ruff is structured as a monorepo with a [flat crate structure](https://matklad.github.io/2021/08/22/large-rust-workspaces.html), such that all crates are contained in a flat `crates` directory. -The vast majority of the code, including all lint rules, lives in the `ruff` crate (located at -`crates/ruff_linter`). As a contributor, that's the crate that'll be most relevant to you. +The vast majority of the code, including all lint rules, lives in the `ruff_linter` crate (located +at `crates/ruff_linter`). As a contributor, that's the crate that'll be most relevant to you. At the time of writing, the repository includes the following crates: diff --git a/Cargo.lock b/Cargo.lock index ad3740b61c111..d3a0680b71b3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -365,7 +365,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.57", ] [[package]] @@ -596,7 +596,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.10.0", - "syn 2.0.55", + "syn 2.0.57", ] [[package]] @@ -607,7 +607,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" dependencies = [ "darling_core", "quote", - "syn 2.0.55", + "syn 2.0.57", ] [[package]] @@ -711,17 +711,27 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +[[package]] +name = "env_filter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" +dependencies = [ + "log", + "regex", +] + [[package]] name = "env_logger" -version = "0.10.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9" dependencies = [ + "anstream", + "anstyle", + "env_filter", "humantime", - "is-terminal", "log", - "regex", - "termcolor", ] [[package]] @@ -1046,9 +1056,9 @@ dependencies = [ [[package]] name = "insta" -version = "1.35.1" +version = "1.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c985c1bef99cf13c58fade470483d81a2bfe846ebde60ed28cc2dddec2df9e2" +checksum = "3eab73f58e59ca6526037208f0e98851159ec1633cf17b6cd2e1f2c3fd5d53cc" dependencies = [ "console", "globset", @@ -1058,14 +1068,13 @@ dependencies = [ "serde", "similar", "walkdir", - "yaml-rust", ] [[package]] name = "insta-cmd" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809d3023d1d6e8d5c2206f199251f75cb26180e41f18cb0f22dd119161cb5127" +checksum = "1980f17994b79f75670aa90cfc8d35edc4aa248f16aa48b5e27835b080e452a2" dependencies = [ "insta", "serde", @@ -1099,7 +1108,7 @@ dependencies = [ "Inflector", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.57", ] [[package]] @@ -1282,7 +1291,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dbd2f3cd9346422ebdc3a614aed6969d4e0b3e9c10517f33b30326acf894c11" dependencies = [ "quote", - "syn 2.0.55", + "syn 2.0.57", ] [[package]] @@ -1348,9 +1357,9 @@ dependencies = [ [[package]] name = "lsp-types" -version = "0.95.0" +version = "0.95.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "158c1911354ef73e8fe42da6b10c0484cb65c7f1007f28022e847706c1ab6984" +checksum = "8e34d33a8e9b006cd3fc4fe69a921affa097bae4bb65f76271f4644f9a334365" dependencies = [ "bitflags 1.3.2", "serde", @@ -1376,9 +1385,9 @@ checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "mimalloc" @@ -1484,6 +1493,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "nu-ansi-term" +version = "0.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c073d3c1930d0751774acf49e66653acecb416c3a54c6ec095a9b11caddb5a68" +dependencies = [ + "windows-sys 0.48.0", +] + [[package]] name = "num-traits" version = "0.2.18" @@ -1634,6 +1652,18 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "pep440_rs" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15efd4d885c29126cc93e12af3087896e2518bd5ca0fb328c19c4ef9cecfa8be" +dependencies = [ + "once_cell", + "serde", + "unicode-width", + "unscanny", +] + [[package]] name = "pep508_rs" version = "0.3.0" @@ -1641,7 +1671,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "910c513bea0f4f833122321c0f20e8c704e01de98692f6989c2ec21f43d88b1e" dependencies = [ "once_cell", - "pep440_rs", + "pep440_rs 0.4.0", "regex", "serde", "thiserror", @@ -1727,7 +1757,7 @@ checksum = "52a40bc70c2c58040d2d8b167ba9a5ff59fc9dab7ad44771cfde3dcfde7a09c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.57", ] [[package]] @@ -1774,7 +1804,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95c3dd745f99aa3c554b7bb00859f7d18c2f1d6afd749ccc86d60b61e702abd9" dependencies = [ "indexmap", - "pep440_rs", + "pep440_rs 0.4.0", "pep508_rs", "serde", "toml", @@ -1885,9 +1915,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.3" +version = "1.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" dependencies = [ "aho-corasick", "memchr", @@ -1945,7 +1975,7 @@ dependencies = [ "pmutil", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.57", ] [[package]] @@ -1965,7 +1995,7 @@ dependencies = [ [[package]] name = "ruff" -version = "0.3.4" +version = "0.3.5" dependencies = [ "anyhow", "argfile", @@ -2127,7 +2157,7 @@ dependencies = [ [[package]] name = "ruff_linter" -version = "0.3.4" +version = "0.3.5" dependencies = [ "aho-corasick", "annotate-snippets 0.9.2", @@ -2151,7 +2181,7 @@ dependencies = [ "once_cell", "path-absolutize", "pathdiff", - "pep440_rs", + "pep440_rs 0.5.0", "pyproject-toml", "quick-junit", "regex", @@ -2195,7 +2225,7 @@ dependencies = [ "proc-macro2", "quote", "ruff_python_trivia", - "syn 2.0.55", + "syn 2.0.57", ] [[package]] @@ -2307,7 +2337,6 @@ name = "ruff_python_parser" version = "0.0.0" dependencies = [ "anyhow", - "bitflags 2.5.0", "bstr", "insta", "is-macro", @@ -2400,7 +2429,7 @@ dependencies = [ [[package]] name = "ruff_shrinking" -version = "0.3.4" +version = "0.3.5" dependencies = [ "anyhow", "clap", @@ -2473,7 +2502,7 @@ dependencies = [ "itertools 0.12.1", "log", "path-absolutize", - "pep440_rs", + "pep440_rs 0.5.0", "regex", "ruff_cache", "ruff_formatter", @@ -2642,7 +2671,7 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.57", ] [[package]] @@ -2658,9 +2687,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.114" +version = "1.0.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" +checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" dependencies = [ "itoa", "ryu", @@ -2675,7 +2704,7 @@ checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.57", ] [[package]] @@ -2698,9 +2727,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.6.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15d167997bd841ec232f5b2b8e0e26606df2e7caa4c31b95ea9ca52b200bd270" +checksum = "ee80b0e361bbf88fd2f6e242ccd19cfda072cb0faa6ae694ecee08199938569a" dependencies = [ "serde", "serde_derive", @@ -2709,14 +2738,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.6.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "865f9743393e638991566a8b7a479043c2c8da94a33e0a31f18214c9cae0a64d" +checksum = "6561dc161a9224638a31d876ccdfefbc1df91d3f3a8342eddb35f055d48c7655" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.57", ] [[package]] @@ -2745,9 +2774,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "similar" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32fea41aca09ee824cc9724996433064c89f7777e60762749a4170a14abbfa21" +checksum = "fa42c91313f1d05da9b26f267f931cf178d4aba455b4c4622dd7355eb80c6640" [[package]] name = "siphasher" @@ -2809,24 +2838,24 @@ checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" [[package]] name = "strum" -version = "0.25.0" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" dependencies = [ "strum_macros", ] [[package]] name = "strum_macros" -version = "0.25.3" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" dependencies = [ "heck 0.4.1", "proc-macro2", "quote", "rustversion", - "syn 2.0.55", + "syn 2.0.57", ] [[package]] @@ -2848,9 +2877,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.55" +version = "2.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "002a1b3dbf967edfafc32655d0f377ab0bb7b994aa1d32c8cc7e9b8bf3ebb8f0" +checksum = "11a6ae1e52eb25aab8f3fb9fca13be982a373b8f1157ca14b897a825ba4a2d35" dependencies = [ "proc-macro2", "quote", @@ -2880,15 +2909,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - [[package]] name = "terminal_size" version = "0.3.0" @@ -2930,7 +2950,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.57", ] [[package]] @@ -2941,7 +2961,7 @@ checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.57", "test-case-core", ] @@ -2962,7 +2982,7 @@ checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.57", ] [[package]] @@ -3031,9 +3051,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "toml" -version = "0.8.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af06656561d28735e9c1cd63dfd57132c8155426aa6af24f36a00a351f88c48e" +checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" dependencies = [ "serde", "serde_spanned", @@ -3052,9 +3072,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.7" +version = "0.22.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18769cd1cec395d70860ceb4d932812a0b4d06b1a4bb336745a4d21b9496e992" +checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4" dependencies = [ "indexmap", "serde", @@ -3083,7 +3103,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.57", ] [[package]] @@ -3108,17 +3128,6 @@ dependencies = [ "tracing-subscriber", ] -[[package]] -name = "tracing-log" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f751112709b4e791d8ce53e32c4ed2d353565a795ce84da2285393f41557bdf2" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - [[package]] name = "tracing-log" version = "0.2.0" @@ -3137,7 +3146,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ "matchers", - "nu-ansi-term", + "nu-ansi-term 0.46.0", "once_cell", "regex", "sharded-slab", @@ -3145,18 +3154,18 @@ dependencies = [ "thread_local", "tracing", "tracing-core", - "tracing-log 0.2.0", + "tracing-log", ] [[package]] name = "tracing-tree" -version = "0.2.5" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ec6adcab41b1391b08a308cc6302b79f8095d1673f6947c2dc65ffb028b0b2d" +checksum = "65139ecd2c3f6484c3b99bc01c77afe21e95473630747c7aca525e78b0666675" dependencies = [ - "nu-ansi-term", + "nu-ansi-term 0.49.0", "tracing-core", - "tracing-log 0.1.4", + "tracing-log", "tracing-subscriber", ] @@ -3263,6 +3272,12 @@ dependencies = [ "rand", ] +[[package]] +name = "unscanny" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9df2af067a7953e9c3831320f35c1cc0600c30d44d9f7a12b01db1cd88d6b47" + [[package]] name = "untrusted" version = "0.9.0" @@ -3306,9 +3321,9 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uuid" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" +checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" dependencies = [ "getrandom", "rand", @@ -3318,13 +3333,13 @@ dependencies = [ [[package]] name = "uuid-macro-internal" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7abb14ae1a50dad63eaa768a458ef43d298cd1bd44951677bd10b732a9ba2a2d" +checksum = "9881bea7cbe687e36c9ab3b778c36cd0487402e270304e8b1296d5085303c1a2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.57", ] [[package]] @@ -3409,7 +3424,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.57", "wasm-bindgen-shared", ] @@ -3443,7 +3458,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.57", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3476,7 +3491,7 @@ checksum = "b7f89739351a2e03cb94beb799d47fb2cac01759b40ec441f7de39b00cbf7ef0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.57", ] [[package]] @@ -3700,15 +3715,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "yaml-rust" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" -dependencies = [ - "linked-hash-map", -] - [[package]] name = "yansi" version = "0.5.1" @@ -3741,7 +3747,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.57", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index b6f731973f2a2..f967ca8cd3a33 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ criterion = { version = "0.5.1", default-features = false } crossbeam = { version = "0.8.4" } dirs = { version = "5.0.0" } drop_bomb = { version = "0.1.5" } -env_logger = { version = "0.10.1" } +env_logger = { version = "0.11.0" } fern = { version = "0.6.1" } filetime = { version = "0.2.23" } fs-err = { version = "2.11.0" } @@ -46,7 +46,7 @@ imperative = { version = "1.0.4" } indicatif = { version = "0.17.8" } indoc = { version = "2.0.4" } insta = { version = "1.35.1", feature = ["filters", "glob"] } -insta-cmd = { version = "0.4.0" } +insta-cmd = { version = "0.5.0" } is-macro = { version = "0.3.5" } is-wsl = { version = "0.4.0" } itertools = { version = "0.12.1" } @@ -67,7 +67,7 @@ num_cpus = { version = "1.16.0" } once_cell = { version = "1.19.0" } path-absolutize = { version = "3.1.1" } pathdiff = { version = "0.2.1" } -pep440_rs = { version = "0.4.0", features = ["serde"] } +pep440_rs = { version = "0.5.0", features = ["serde"] } pretty_assertions = "1.3.0" proc-macro2 = { version = "1.0.79" } pyproject-toml = { version = "0.9.0" } @@ -90,8 +90,8 @@ shlex = { version = "1.3.0" } similar = { version = "2.4.0", features = ["inline"] } smallvec = { version = "1.13.2" } static_assertions = "1.1.0" -strum = { version = "0.25.0", features = ["strum_macros"] } -strum_macros = { version = "0.25.3" } +strum = { version = "0.26.0", features = ["strum_macros"] } +strum_macros = { version = "0.26.0" } syn = { version = "2.0.55" } tempfile = { version = "3.9.0" } test-case = { version = "3.3.1" } @@ -101,7 +101,7 @@ toml = { version = "0.8.11" } tracing = { version = "0.1.40" } tracing-indicatif = { version = "0.3.6" } tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } -tracing-tree = { version = "0.2.4" } +tracing-tree = { version = "0.3.0" } typed-arena = { version = "2.0.2" } unic-ucd-category = { version = "0.9" } unicode-ident = { version = "1.0.12" } diff --git a/README.md b/README.md index 5d7c185f3394c..9096df4e74dad 100644 --- a/README.md +++ b/README.md @@ -151,7 +151,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.3.4 + rev: v0.3.5 hooks: # Run the linter. - id: ruff diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml index 3a95c40f470ce..c7ab422c8b91f 100644 --- a/crates/ruff/Cargo.toml +++ b/crates/ruff/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff" -version = "0.3.4" +version = "0.3.5" publish = false authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff/src/version.rs b/crates/ruff/src/version.rs index f79b938f653c4..09ed1a5c95d15 100644 --- a/crates/ruff/src/version.rs +++ b/crates/ruff/src/version.rs @@ -70,7 +70,7 @@ pub(crate) fn version() -> VersionInfo { #[cfg(test)] mod tests { - use insta::{assert_display_snapshot, assert_json_snapshot}; + use insta::{assert_json_snapshot, assert_snapshot}; use super::{CommitInfo, VersionInfo}; @@ -80,7 +80,7 @@ mod tests { version: "0.0.0".to_string(), commit_info: None, }; - assert_display_snapshot!(version); + assert_snapshot!(version); } #[test] @@ -95,7 +95,7 @@ mod tests { commits_since_last_tag: 0, }), }; - assert_display_snapshot!(version); + assert_snapshot!(version); } #[test] @@ -110,7 +110,7 @@ mod tests { commits_since_last_tag: 24, }), }; - assert_display_snapshot!(version); + assert_snapshot!(version); } #[test] diff --git a/crates/ruff_diagnostics/src/diagnostic.rs b/crates/ruff_diagnostics/src/diagnostic.rs index 006df346365fd..84da5f3904607 100644 --- a/crates/ruff_diagnostics/src/diagnostic.rs +++ b/crates/ruff_diagnostics/src/diagnostic.rs @@ -71,6 +71,14 @@ impl Diagnostic { } } + /// Consumes `self` and returns a new `Diagnostic` with the given parent node. + #[inline] + #[must_use] + pub fn with_parent(mut self, parent: TextSize) -> Self { + self.set_parent(parent); + self + } + /// Set the location of the diagnostic's parent node. #[inline] pub fn set_parent(&mut self, parent: TextSize) { diff --git a/crates/ruff_linter/Cargo.toml b/crates/ruff_linter/Cargo.toml index c7c49f86fdfd2..cee9e18a6b559 100644 --- a/crates/ruff_linter/Cargo.toml +++ b/crates/ruff_linter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_linter" -version = "0.3.4" +version = "0.3.5" publish = false authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C419.py b/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C419.py index 4a9671b1ee020..b0a15cf2d6aac 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C419.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C419.py @@ -13,6 +13,10 @@ all(x.id for x in bar) any(x.id for x in bar) all((x.id for x in bar)) +# we don't lint on these in stable yet +sum([x.val for x in bar]) +min([x.val for x in bar]) +max([x.val for x in bar]) async def f() -> bool: diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C419_1.py b/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C419_1.py new file mode 100644 index 0000000000000..b0a521e2363d2 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C419_1.py @@ -0,0 +1,8 @@ +sum([x.val for x in bar]) +min([x.val for x in bar]) +max([x.val for x in bar]) + +# Ok +sum(x.val for x in bar) +min(x.val for x in bar) +max(x.val for x in bar) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C419_2.py b/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C419_2.py new file mode 100644 index 0000000000000..f8848634547f1 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C419_2.py @@ -0,0 +1,3 @@ +# no lint if shadowed +def all(x): pass +all([x.id for x in bar]) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_import_conventions/defaults.py b/crates/ruff_linter/resources/test/fixtures/flake8_import_conventions/defaults.py index c1c528deca16e..0342d4203495f 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_import_conventions/defaults.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_import_conventions/defaults.py @@ -21,6 +21,7 @@ def unconventional_aliases(): import tkinter as tkr import networkx as nxy + def conventional_aliases(): import altair as alt import matplotlib.pyplot as plt diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_import_conventions/same_name.py b/crates/ruff_linter/resources/test/fixtures/flake8_import_conventions/same_name.py new file mode 100644 index 0000000000000..31618078cd030 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_import_conventions/same_name.py @@ -0,0 +1,10 @@ +def no_alias(): + from django.conf import settings + + +def conventional_alias(): + from django.conf import settings as settings + + +def unconventional_alias(): + from django.conf import settings as s diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_quotes/doubles_all.py b/crates/ruff_linter/resources/test/fixtures/flake8_quotes/doubles_all.py new file mode 100644 index 0000000000000..80c0d8e22c11e --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_quotes/doubles_all.py @@ -0,0 +1,7 @@ +"""This is a docstring.""" + +this_is_an_inline_string = "double quote string" + +this_is_a_multiline_string = """ +double quote string +""" diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_quotes/singles.py b/crates/ruff_linter/resources/test/fixtures/flake8_quotes/singles.py index e9a96a105cb6e..06aa7504f7140 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_quotes/singles.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_quotes/singles.py @@ -5,3 +5,5 @@ # https://github.com/astral-sh/ruff/issues/10546 x: "Literal['foo', 'bar']" +# https://github.com/astral-sh/ruff/issues/10761 +f"Before {f'x {x}' if y else f'foo {z}'} after" diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_return/RET504.py b/crates/ruff_linter/resources/test/fixtures/flake8_return/RET504.py index 8480aafaa1c01..91a60a7540dc9 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_return/RET504.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_return/RET504.py @@ -406,3 +406,18 @@ def foo(): with contextlib.suppress(Exception): y = 2 return y + + +# See: https://github.com/astral-sh/ruff/issues/10732 +def func(a: dict[str, int]) -> list[dict[str, int]]: + services: list[dict[str, int]] + if "services" in a: + services = a["services"] + return services + + +# See: https://github.com/astral-sh/ruff/issues/10732 +def func(a: dict[str, int]) -> list[dict[str, int]]: + if "services" in a: + services = a["services"] + return services diff --git a/crates/ruff_linter/resources/test/fixtures/pep8_naming/N815.py b/crates/ruff_linter/resources/test/fixtures/pep8_naming/N815.py index d3578d3cfbd7d..41a9f46fd8f96 100644 --- a/crates/ruff_linter/resources/test/fixtures/pep8_naming/N815.py +++ b/crates/ruff_linter/resources/test/fixtures/pep8_naming/N815.py @@ -21,3 +21,10 @@ class D(TypedDict): mixedCase: bool _mixedCase: list mixed_Case: set + +class E(D): + lower: int + CONSTANT: str + mixedCase: bool + _mixedCase: list + mixed_Case: set diff --git a/crates/ruff_linter/resources/test/fixtures/pycodestyle/E731.py b/crates/ruff_linter/resources/test/fixtures/pycodestyle/E731.py index 7c01e74a0343c..a8acec8eec562 100644 --- a/crates/ruff_linter/resources/test/fixtures/pycodestyle/E731.py +++ b/crates/ruff_linter/resources/test/fixtures/pycodestyle/E731.py @@ -60,7 +60,7 @@ class Scope: class Scope: from typing import Callable - # E731 + # OK f: Callable[[int], int] = lambda x: 2 * x @@ -147,3 +147,12 @@ def scope(): f = lambda: ( i := 1, ) + + +from dataclasses import dataclass +from typing import Callable + +@dataclass +class FilterDataclass: + # OK + filter: Callable[[str], bool] = lambda _: True diff --git a/crates/ruff_linter/resources/test/fixtures/pydocstyle/D403.py b/crates/ruff_linter/resources/test/fixtures/pydocstyle/D403.py index d90acb358efab..bd90d60bc84c2 100644 --- a/crates/ruff_linter/resources/test/fixtures/pydocstyle/D403.py +++ b/crates/ruff_linter/resources/test/fixtures/pydocstyle/D403.py @@ -25,3 +25,9 @@ def non_ascii(): def all_caps(): """th•s is not capitalized.""" + +def single_word(): + """singleword.""" + +def single_word_no_dot(): + """singleword""" diff --git a/crates/ruff_linter/resources/test/fixtures/pyflakes/F822_3.py b/crates/ruff_linter/resources/test/fixtures/pyflakes/F822_3.py new file mode 100644 index 0000000000000..682b69597223e --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pyflakes/F822_3.py @@ -0,0 +1,13 @@ +"""Respect `# noqa` directives on `__all__` definitions.""" + +__all__ = [ # noqa: F822 + "Bernoulli", + "Beta", + "Binomial", +] + + +__all__ += [ + "ContinuousBernoulli", # noqa: F822 + "ExponentialFamily", +] diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/bad_staticmethod_argument.py b/crates/ruff_linter/resources/test/fixtures/pylint/bad_staticmethod_argument.py new file mode 100644 index 0000000000000..6de4d74ee485f --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pylint/bad_staticmethod_argument.py @@ -0,0 +1,44 @@ +class Wolf: + @staticmethod + def eat(self): # [bad-staticmethod-argument] + pass + + +class Wolf: + @staticmethod + def eat(sheep): + pass + + +class Sheep: + @staticmethod + def eat(cls, x, y, z): # [bad-staticmethod-argument] + pass + + @staticmethod + def sleep(self, x, y, z): # [bad-staticmethod-argument] + pass + + def grow(self, x, y, z): + pass + + @classmethod + def graze(cls, x, y, z): + pass + + +class Foo: + @staticmethod + def eat(x, self, z): + pass + + @staticmethod + def sleep(x, cls, z): + pass + + def grow(self, x, y, z): + pass + + @classmethod + def graze(cls, x, y, z): + pass diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/singledispatch_method.py b/crates/ruff_linter/resources/test/fixtures/pylint/singledispatch_method.py index 72e813c2df80e..08f1e0c4c608e 100644 --- a/crates/ruff_linter/resources/test/fixtures/pylint/singledispatch_method.py +++ b/crates/ruff_linter/resources/test/fixtures/pylint/singledispatch_method.py @@ -20,7 +20,7 @@ def move(self, position): def place(self, position): pass - @singledispatch + @singledispatch # [singledispatch-method] @staticmethod def do(position): pass diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/singledispatchmethod_function.py b/crates/ruff_linter/resources/test/fixtures/pylint/singledispatchmethod_function.py index cf249f184fc83..c4ead1ce41de8 100644 --- a/crates/ruff_linter/resources/test/fixtures/pylint/singledispatchmethod_function.py +++ b/crates/ruff_linter/resources/test/fixtures/pylint/singledispatchmethod_function.py @@ -17,7 +17,7 @@ def convert_position(cls, position): def move(self, position): pass - @singledispatchmethod # [singledispatchmethod-function] + @singledispatchmethod # Ok @staticmethod def do(position): pass diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP042.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP042.py new file mode 100644 index 0000000000000..392cf0ff5eee0 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP042.py @@ -0,0 +1,13 @@ +from enum import Enum + + +class A(str, Enum): ... + + +class B(Enum, str): ... + + +class D(int, str, Enum): ... + + +class E(str, int, Enum): ... diff --git a/crates/ruff_linter/resources/test/fixtures/refurb/FURB166.py b/crates/ruff_linter/resources/test/fixtures/refurb/FURB166.py new file mode 100644 index 0000000000000..1c4764787fb06 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/refurb/FURB166.py @@ -0,0 +1,29 @@ +# Errors + +_ = int("0b1010"[2:], 2) +_ = int("0o777"[2:], 8) +_ = int("0xFFFF"[2:], 16) + +b = "0b11" +_ = int(b[2:], 2) + +_ = int("0xFFFF"[2:], base=16) + +_ = int(b"0xFFFF"[2:], 16) + + +def get_str(): + return "0xFFF" + + +_ = int(get_str()[2:], 16) + +# OK + +_ = int("0b1100", 0) +_ = int("123", 3) +_ = int("123", 10) +_ = int("0b1010"[3:], 2) +_ = int("0b1010"[:2], 2) +_ = int("12345"[2:]) +_ = int("12345"[2:], xyz=1) # type: ignore diff --git a/crates/ruff_linter/src/checkers/ast/analyze/deferred_scopes.rs b/crates/ruff_linter/src/checkers/ast/analyze/deferred_scopes.rs index 0a85c041b0bdf..c4d0ee7944b78 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/deferred_scopes.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/deferred_scopes.rs @@ -15,15 +15,19 @@ use crate::rules::{ pub(crate) fn deferred_scopes(checker: &mut Checker) { if !checker.any_enabled(&[ Rule::AsyncioDanglingTask, + Rule::BadStaticmethodArgument, + Rule::BuiltinAttributeShadowing, Rule::GlobalVariableNotAssigned, Rule::ImportPrivateName, Rule::ImportShadowedByLoopVar, - Rule::InvalidFirstArgumentNameForMethod, Rule::InvalidFirstArgumentNameForClassMethod, + Rule::InvalidFirstArgumentNameForMethod, Rule::NoSelfUse, Rule::RedefinedArgumentFromLocal, Rule::RedefinedWhileUnused, Rule::RuntimeImportInTypeCheckingBlock, + Rule::SingledispatchMethod, + Rule::SingledispatchmethodFunction, Rule::TooManyLocals, Rule::TypingOnlyFirstPartyImport, Rule::TypingOnlyStandardLibraryImport, @@ -31,19 +35,16 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) { Rule::UndefinedLocal, Rule::UnusedAnnotation, Rule::UnusedClassMethodArgument, - Rule::BuiltinAttributeShadowing, Rule::UnusedFunctionArgument, Rule::UnusedImport, Rule::UnusedLambdaArgument, Rule::UnusedMethodArgument, Rule::UnusedPrivateProtocol, Rule::UnusedPrivateTypeAlias, - Rule::UnusedPrivateTypeVar, Rule::UnusedPrivateTypedDict, + Rule::UnusedPrivateTypeVar, Rule::UnusedStaticMethodArgument, Rule::UnusedVariable, - Rule::SingledispatchMethod, - Rule::SingledispatchmethodFunction, ]) { return; } @@ -424,6 +425,10 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) { pylint::rules::singledispatchmethod_function(checker, scope, &mut diagnostics); } + if checker.enabled(Rule::BadStaticmethodArgument) { + pylint::rules::bad_staticmethod_argument(checker, scope, &mut diagnostics); + } + if checker.any_enabled(&[ Rule::InvalidFirstArgumentNameForClassMethod, Rule::InvalidFirstArgumentNameForMethod, diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index 350bb7fcfcfa9..97624a9aaff52 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -223,14 +223,10 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) { } } if checker.enabled(Rule::MixedCaseVariableInClassScope) { - if let ScopeKind::Class(ast::StmtClassDef { arguments, .. }) = - &checker.semantic.current_scope().kind + if let ScopeKind::Class(class_def) = &checker.semantic.current_scope().kind { pep8_naming::rules::mixed_case_variable_in_class_scope( - checker, - expr, - id, - arguments.as_deref(), + checker, expr, id, class_def, ); } } @@ -709,8 +705,8 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) { args, ); } - if checker.enabled(Rule::UnnecessaryComprehensionAnyAll) { - flake8_comprehensions::rules::unnecessary_comprehension_any_all( + if checker.enabled(Rule::UnnecessaryComprehensionInCall) { + flake8_comprehensions::rules::unnecessary_comprehension_in_call( checker, expr, func, args, keywords, ); } @@ -977,6 +973,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) { if checker.enabled(Rule::UnnecessaryIterableAllocationForFirstElement) { ruff::rules::unnecessary_iterable_allocation_for_first_element(checker, expr); } + if checker.enabled(Rule::IntOnSlicedStr) { + refurb::rules::int_on_sliced_str(checker, call); + } } Expr::Dict(dict) => { if checker.any_enabled(&[ diff --git a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs index 7e8293172916f..cc027dc208dc0 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs @@ -406,6 +406,11 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { if checker.enabled(Rule::UselessObjectInheritance) { pyupgrade::rules::useless_object_inheritance(checker, class_def); } + if checker.enabled(Rule::ReplaceStrEnum) { + if checker.settings.target_version >= PythonVersion::Py311 { + pyupgrade::rules::replace_str_enum(checker, class_def); + } + } if checker.enabled(Rule::UnnecessaryClassParentheses) { pyupgrade::rules::unnecessary_class_parentheses(checker, class_def); } diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 62de2dfe0d0e4..990fa0abefe33 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -31,15 +31,14 @@ use std::path::Path; use itertools::Itertools; use log::debug; use ruff_python_ast::{ - self as ast, all::DunderAllName, Comprehension, ElifElseClause, ExceptHandler, Expr, - ExprContext, Keyword, MatchCase, Parameter, ParameterWithDefault, Parameters, Pattern, Stmt, - Suite, UnaryOp, + self as ast, Comprehension, ElifElseClause, ExceptHandler, Expr, ExprContext, FStringElement, + Keyword, MatchCase, Parameter, ParameterWithDefault, Parameters, Pattern, Stmt, Suite, UnaryOp, }; use ruff_text_size::{Ranged, TextRange, TextSize}; use ruff_diagnostics::{Diagnostic, IsolationLevel}; use ruff_notebook::{CellOffsets, NotebookIndex}; -use ruff_python_ast::all::{extract_all_names, DunderAllFlags}; +use ruff_python_ast::all::{extract_all_names, DunderAllDefinition, DunderAllFlags}; use ruff_python_ast::helpers::{ collect_import_from_member, extract_handled_exceptions, is_docstring_stmt, to_module_path, }; @@ -1580,6 +1579,15 @@ impl<'a> Visitor<'a> for Checker<'a> { .push((bound, self.semantic.snapshot())); } } + + fn visit_f_string_element(&mut self, f_string_element: &'a FStringElement) { + let snapshot = self.semantic.flags; + if f_string_element.is_expression() { + self.semantic.flags |= SemanticModelFlags::F_STRING_REPLACEMENT_FIELD; + } + visitor::walk_f_string_element(self, f_string_element); + self.semantic.flags = snapshot; + } } impl<'a> Checker<'a> { @@ -2100,45 +2108,54 @@ impl<'a> Checker<'a> { fn visit_exports(&mut self) { let snapshot = self.semantic.snapshot(); - let exports: Vec = self + let definitions: Vec = 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().copied()), + BindingKind::Export(Export { names }) => { + Some(DunderAllDefinition::new(binding.range(), names.to_vec())) + } _ => None, }) - .flatten() .collect(); - for export in exports { - let (name, range) = (export.name(), export.range()); - if let Some(binding_id) = self.semantic.global_scope().get(name) { - self.semantic.flags |= SemanticModelFlags::DUNDER_ALL_DEFINITION; - // Mark anything referenced in `__all__` as used. - self.semantic - .add_global_reference(binding_id, ExprContext::Load, range); - self.semantic.flags -= SemanticModelFlags::DUNDER_ALL_DEFINITION; - } else { - if self.semantic.global_scope().uses_star_imports() { - if self.enabled(Rule::UndefinedLocalWithImportStarUsage) { - self.diagnostics.push(Diagnostic::new( - pyflakes::rules::UndefinedLocalWithImportStarUsage { - name: name.to_string(), - }, - range, - )); - } + for definition in definitions { + for export in definition.names() { + let (name, range) = (export.name(), export.range()); + if let Some(binding_id) = self.semantic.global_scope().get(name) { + self.semantic.flags |= SemanticModelFlags::DUNDER_ALL_DEFINITION; + // Mark anything referenced in `__all__` as used. + self.semantic + .add_global_reference(binding_id, ExprContext::Load, range); + self.semantic.flags -= SemanticModelFlags::DUNDER_ALL_DEFINITION; } else { - if self.enabled(Rule::UndefinedExport) { - if !self.path.ends_with("__init__.py") { - self.diagnostics.push(Diagnostic::new( - pyflakes::rules::UndefinedExport { - name: name.to_string(), - }, - range, - )); + if self.semantic.global_scope().uses_star_imports() { + if self.enabled(Rule::UndefinedLocalWithImportStarUsage) { + self.diagnostics.push( + Diagnostic::new( + pyflakes::rules::UndefinedLocalWithImportStarUsage { + name: name.to_string(), + }, + range, + ) + .with_parent(definition.start()), + ); + } + } else { + if self.enabled(Rule::UndefinedExport) { + if !self.path.ends_with("__init__.py") { + self.diagnostics.push( + Diagnostic::new( + pyflakes::rules::UndefinedExport { + name: name.to_string(), + }, + range, + ) + .with_parent(definition.start()), + ); + } } } } diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index 446829fd122b6..a8a828a2a4fb5 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -225,12 +225,12 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Pylint, "C0208") => (RuleGroup::Stable, rules::pylint::rules::IterationOverSet), (Pylint, "C0414") => (RuleGroup::Stable, rules::pylint::rules::UselessImportAlias), (Pylint, "C0415") => (RuleGroup::Preview, rules::pylint::rules::ImportOutsideTopLevel), - (Pylint, "C2401") => (RuleGroup::Preview, rules::pylint::rules::NonAsciiName), - (Pylint, "C2403") => (RuleGroup::Preview, rules::pylint::rules::NonAsciiImportName), - (Pylint, "C2801") => (RuleGroup::Preview, rules::pylint::rules::UnnecessaryDunderCall), #[allow(deprecated)] (Pylint, "C1901") => (RuleGroup::Nursery, rules::pylint::rules::CompareToEmptyString), + (Pylint, "C2401") => (RuleGroup::Preview, rules::pylint::rules::NonAsciiName), + (Pylint, "C2403") => (RuleGroup::Preview, rules::pylint::rules::NonAsciiImportName), (Pylint, "C2701") => (RuleGroup::Preview, rules::pylint::rules::ImportPrivateName), + (Pylint, "C2801") => (RuleGroup::Preview, rules::pylint::rules::UnnecessaryDunderCall), (Pylint, "C3002") => (RuleGroup::Stable, rules::pylint::rules::UnnecessaryDirectLambdaCall), (Pylint, "E0100") => (RuleGroup::Stable, rules::pylint::rules::YieldInInit), (Pylint, "E0101") => (RuleGroup::Stable, rules::pylint::rules::ReturnInInit), @@ -272,6 +272,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Pylint, "R0203") => (RuleGroup::Preview, rules::pylint::rules::NoStaticmethodDecorator), (Pylint, "R0206") => (RuleGroup::Stable, rules::pylint::rules::PropertyWithParameters), (Pylint, "R0402") => (RuleGroup::Stable, rules::pylint::rules::ManualFromImport), + (Pylint, "R0904") => (RuleGroup::Preview, rules::pylint::rules::TooManyPublicMethods), (Pylint, "R0911") => (RuleGroup::Stable, rules::pylint::rules::TooManyReturnStatements), (Pylint, "R0912") => (RuleGroup::Stable, rules::pylint::rules::TooManyBranches), (Pylint, "R0913") => (RuleGroup::Stable, rules::pylint::rules::TooManyArguments), @@ -282,9 +283,9 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Pylint, "R1701") => (RuleGroup::Stable, rules::pylint::rules::RepeatedIsinstanceCalls), (Pylint, "R1702") => (RuleGroup::Preview, rules::pylint::rules::TooManyNestedBlocks), (Pylint, "R1704") => (RuleGroup::Preview, rules::pylint::rules::RedefinedArgumentFromLocal), + (Pylint, "R1706") => (RuleGroup::Removed, rules::pylint::rules::AndOrTernary), (Pylint, "R1711") => (RuleGroup::Stable, rules::pylint::rules::UselessReturn), (Pylint, "R1714") => (RuleGroup::Stable, rules::pylint::rules::RepeatedEqualityComparison), - (Pylint, "R1706") => (RuleGroup::Removed, rules::pylint::rules::AndOrTernary), (Pylint, "R1722") => (RuleGroup::Stable, rules::pylint::rules::SysExitAlias), (Pylint, "R1733") => (RuleGroup::Preview, rules::pylint::rules::UnnecessaryDictIndexLookup), (Pylint, "R1736") => (RuleGroup::Preview, rules::pylint::rules::UnnecessaryListIndexLookup), @@ -302,11 +303,12 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Pylint, "W0129") => (RuleGroup::Stable, rules::pylint::rules::AssertOnStringLiteral), (Pylint, "W0131") => (RuleGroup::Stable, rules::pylint::rules::NamedExprWithoutContext), (Pylint, "W0133") => (RuleGroup::Preview, rules::pylint::rules::UselessExceptionStatement), + (Pylint, "W0211") => (RuleGroup::Preview, rules::pylint::rules::BadStaticmethodArgument), (Pylint, "W0245") => (RuleGroup::Preview, rules::pylint::rules::SuperWithoutBrackets), (Pylint, "W0406") => (RuleGroup::Stable, rules::pylint::rules::ImportSelf), (Pylint, "W0602") => (RuleGroup::Stable, rules::pylint::rules::GlobalVariableNotAssigned), - (Pylint, "W0604") => (RuleGroup::Preview, rules::pylint::rules::GlobalAtModuleLevel), (Pylint, "W0603") => (RuleGroup::Stable, rules::pylint::rules::GlobalStatement), + (Pylint, "W0604") => (RuleGroup::Preview, rules::pylint::rules::GlobalAtModuleLevel), (Pylint, "W0711") => (RuleGroup::Stable, rules::pylint::rules::BinaryOpException), (Pylint, "W1501") => (RuleGroup::Preview, rules::pylint::rules::BadOpenMode), (Pylint, "W1508") => (RuleGroup::Stable, rules::pylint::rules::InvalidEnvvarDefault), @@ -316,7 +318,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { #[allow(deprecated)] (Pylint, "W1641") => (RuleGroup::Nursery, rules::pylint::rules::EqWithoutHash), (Pylint, "W2101") => (RuleGroup::Preview, rules::pylint::rules::UselessWithLock), - (Pylint, "R0904") => (RuleGroup::Preview, rules::pylint::rules::TooManyPublicMethods), (Pylint, "W2901") => (RuleGroup::Stable, rules::pylint::rules::RedefinedLoopName), #[allow(deprecated)] (Pylint, "W3201") => (RuleGroup::Nursery, rules::pylint::rules::BadDunderMethodName), @@ -398,7 +399,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Flake8Comprehensions, "16") => (RuleGroup::Stable, rules::flake8_comprehensions::rules::UnnecessaryComprehension), (Flake8Comprehensions, "17") => (RuleGroup::Stable, rules::flake8_comprehensions::rules::UnnecessaryMap), (Flake8Comprehensions, "18") => (RuleGroup::Stable, rules::flake8_comprehensions::rules::UnnecessaryLiteralWithinDictCall), - (Flake8Comprehensions, "19") => (RuleGroup::Stable, rules::flake8_comprehensions::rules::UnnecessaryComprehensionAnyAll), + (Flake8Comprehensions, "19") => (RuleGroup::Stable, rules::flake8_comprehensions::rules::UnnecessaryComprehensionInCall), // flake8-debugger (Flake8Debugger, "0") => (RuleGroup::Stable, rules::flake8_debugger::rules::Debugger), @@ -546,6 +547,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Pyupgrade, "039") => (RuleGroup::Stable, rules::pyupgrade::rules::UnnecessaryClassParentheses), (Pyupgrade, "040") => (RuleGroup::Stable, rules::pyupgrade::rules::NonPEP695TypeAlias), (Pyupgrade, "041") => (RuleGroup::Stable, rules::pyupgrade::rules::TimeoutErrorAlias), + (Pyupgrade, "042") => (RuleGroup::Preview, rules::pyupgrade::rules::ReplaceStrEnum), // pydocstyle (Pydocstyle, "100") => (RuleGroup::Stable, rules::pydocstyle::rules::UndocumentedPublicModule), @@ -1054,6 +1056,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Refurb, "161") => (RuleGroup::Preview, rules::refurb::rules::BitCount), (Refurb, "163") => (RuleGroup::Preview, rules::refurb::rules::RedundantLogBase), (Refurb, "164") => (RuleGroup::Preview, rules::refurb::rules::UnnecessaryFromFloat), + (Refurb, "166") => (RuleGroup::Preview, rules::refurb::rules::IntOnSlicedStr), (Refurb, "167") => (RuleGroup::Preview, rules::refurb::rules::RegexFlagAlias), (Refurb, "168") => (RuleGroup::Preview, rules::refurb::rules::IsinstanceTypeNone), (Refurb, "169") => (RuleGroup::Preview, rules::refurb::rules::TypeNoneComparison), diff --git a/crates/ruff_linter/src/rules/flake8_annotations/settings.rs b/crates/ruff_linter/src/rules/flake8_annotations/settings.rs index 011cf01f4a926..342a56023a0e1 100644 --- a/crates/ruff_linter/src/rules/flake8_annotations/settings.rs +++ b/crates/ruff_linter/src/rules/flake8_annotations/settings.rs @@ -4,7 +4,7 @@ use crate::display_settings; use ruff_macros::CacheKey; use std::fmt::{Display, Formatter}; -#[derive(Debug, Default, CacheKey)] +#[derive(Debug, Clone, Default, CacheKey)] #[allow(clippy::struct_excessive_bools)] pub struct Settings { pub mypy_init_return: bool, diff --git a/crates/ruff_linter/src/rules/flake8_bandit/settings.rs b/crates/ruff_linter/src/rules/flake8_bandit/settings.rs index 17a018a25c64c..ee96e6ee667d2 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/settings.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/settings.rs @@ -10,7 +10,7 @@ pub fn default_tmp_dirs() -> Vec { .to_vec() } -#[derive(Debug, CacheKey)] +#[derive(Debug, Clone, CacheKey)] pub struct Settings { pub hardcoded_tmp_directory: Vec, pub check_typed_exception: bool, diff --git a/crates/ruff_linter/src/rules/flake8_boolean_trap/settings.rs b/crates/ruff_linter/src/rules/flake8_boolean_trap/settings.rs index 3b88395c9847a..9825442ad55ff 100644 --- a/crates/ruff_linter/src/rules/flake8_boolean_trap/settings.rs +++ b/crates/ruff_linter/src/rules/flake8_boolean_trap/settings.rs @@ -6,7 +6,7 @@ use ruff_macros::CacheKey; use crate::display_settings; -#[derive(Debug, CacheKey, Default)] +#[derive(Debug, Clone, CacheKey, Default)] pub struct Settings { pub extend_allowed_calls: Vec, } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/settings.rs b/crates/ruff_linter/src/rules/flake8_bugbear/settings.rs index 03c4d5cdf1d98..6a13a3c8b79ba 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/settings.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/settings.rs @@ -4,7 +4,7 @@ use crate::display_settings; use ruff_macros::CacheKey; use std::fmt::{Display, Formatter}; -#[derive(Debug, Default, CacheKey)] +#[derive(Debug, Clone, Default, CacheKey)] pub struct Settings { pub extend_immutable_calls: Vec, } diff --git a/crates/ruff_linter/src/rules/flake8_builtins/settings.rs b/crates/ruff_linter/src/rules/flake8_builtins/settings.rs index d3fc3a70f74bd..e11537efb7ff4 100644 --- a/crates/ruff_linter/src/rules/flake8_builtins/settings.rs +++ b/crates/ruff_linter/src/rules/flake8_builtins/settings.rs @@ -4,7 +4,7 @@ use crate::display_settings; use ruff_macros::CacheKey; use std::fmt::{Display, Formatter}; -#[derive(Debug, Default, CacheKey)] +#[derive(Debug, Clone, Default, CacheKey)] pub struct Settings { pub builtins_ignorelist: Vec, } diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/fixes.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/fixes.rs index 1febae119798b..295d8463b8015 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/fixes.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/fixes.rs @@ -793,7 +793,7 @@ pub(crate) fn fix_unnecessary_map( } /// (C419) Convert `[i for i in a]` into `i for i in a` -pub(crate) fn fix_unnecessary_comprehension_any_all( +pub(crate) fn fix_unnecessary_comprehension_in_call( expr: &Expr, locator: &Locator, stylist: &Stylist, diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs index 74c2a69ac323f..f1c765dff0646 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs @@ -12,13 +12,15 @@ mod tests { use crate::assert_messages; use crate::registry::Rule; + use crate::settings::types::PreviewMode; use crate::settings::LinterSettings; use crate::test::test_path; #[test_case(Rule::UnnecessaryCallAroundSorted, Path::new("C413.py"))] #[test_case(Rule::UnnecessaryCollectionCall, Path::new("C408.py"))] #[test_case(Rule::UnnecessaryComprehension, Path::new("C416.py"))] - #[test_case(Rule::UnnecessaryComprehensionAnyAll, Path::new("C419.py"))] + #[test_case(Rule::UnnecessaryComprehensionInCall, Path::new("C419.py"))] + #[test_case(Rule::UnnecessaryComprehensionInCall, Path::new("C419_2.py"))] #[test_case(Rule::UnnecessaryDoubleCastOrProcess, Path::new("C414.py"))] #[test_case(Rule::UnnecessaryGeneratorDict, Path::new("C402.py"))] #[test_case(Rule::UnnecessaryGeneratorList, Path::new("C400.py"))] @@ -43,6 +45,24 @@ mod tests { Ok(()) } + #[test_case(Rule::UnnecessaryComprehensionInCall, Path::new("C419_1.py"))] + fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> { + let snapshot = format!( + "preview__{}_{}", + rule_code.noqa_code(), + path.to_string_lossy() + ); + let diagnostics = test_path( + Path::new("flake8_comprehensions").join(path).as_path(), + &LinterSettings { + preview: PreviewMode::Enabled, + ..LinterSettings::for_rule(rule_code) + }, + )?; + assert_messages!(snapshot, diagnostics); + Ok(()) + } + #[test_case(Rule::UnnecessaryCollectionCall, Path::new("C408.py"))] fn allow_dict_calls_with_keyword_arguments(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!( diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/mod.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/mod.rs index 2c24ecfc2fa47..ff54ed3c38137 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/mod.rs @@ -1,7 +1,7 @@ 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_comprehension_in_call::*; pub(crate) use unnecessary_double_cast_or_process::*; pub(crate) use unnecessary_generator_dict::*; pub(crate) use unnecessary_generator_list::*; @@ -21,7 +21,7 @@ mod helpers; mod unnecessary_call_around_sorted; mod unnecessary_collection_call; mod unnecessary_comprehension; -mod unnecessary_comprehension_any_all; +mod unnecessary_comprehension_in_call; mod unnecessary_double_cast_or_process; mod unnecessary_generator_dict; mod unnecessary_generator_list; diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_comprehension_any_all.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_comprehension_in_call.rs similarity index 51% rename from crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_comprehension_any_all.rs rename to crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_comprehension_in_call.rs index a713059f3d15c..901fb1e54abdf 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_comprehension_any_all.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_comprehension_in_call.rs @@ -11,15 +11,18 @@ use crate::checkers::ast::Checker; use crate::rules::flake8_comprehensions::fixes; /// ## What it does -/// Checks for unnecessary list comprehensions passed to `any` and `all`. +/// Checks for unnecessary list comprehensions passed to builtin functions that take an iterable. /// /// ## Why is this bad? -/// `any` and `all` take any iterators, including generators. Converting a generator to a list -/// by way of a list comprehension is unnecessary and reduces performance due to the -/// overhead of creating the list. +/// Many builtin functions (this rule currently covers `any`, `all`, `min`, `max`, and `sum`) take +/// any iterable, including a generator. Constructing a temporary list via list comprehension is +/// unnecessary and wastes memory for large iterables. /// -/// For example, compare the performance of `all` with a list comprehension against that -/// of a generator (~40x faster here): +/// `any` and `all` can also short-circuit iteration, saving a lot of time. The unnecessary +/// comprehension forces a full iteration of the input iterable, giving up the benefits of +/// short-circuiting. For example, compare the performance of `all` with a list comprehension +/// against that of a generator in a case where an early short-circuit is possible (almost 40x +/// faster): /// /// ```console /// In [1]: %timeit all([i for i in range(1000)]) @@ -29,26 +32,41 @@ use crate::rules::flake8_comprehensions::fixes; /// 212 ns ± 0.892 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each) /// ``` /// +/// This performance improvement is due to short-circuiting. If the entire iterable has to be +/// traversed, the comprehension version may even be a bit faster: list allocation overhead is not +/// necessarily greater than generator overhead. +/// +/// Applying this rule simplifies the code and will usually save memory, but in the absence of +/// short-circuiting it may not improve performance. (It may even slightly regress performance, +/// though the difference will usually be small.) +/// /// ## Examples /// ```python /// any([x.id for x in bar]) /// all([x.id for x in bar]) +/// sum([x.val for x in bar]) +/// min([x.val for x in bar]) +/// max([x.val for x in bar]) /// ``` /// /// Use instead: /// ```python /// any(x.id for x in bar) /// all(x.id for x in bar) +/// sum(x.val for x in bar) +/// min(x.val for x in bar) +/// max(x.val for x in bar) /// ``` /// /// ## Fix safety -/// This rule's fix is marked as unsafe, as it may occasionally drop comments -/// when rewriting the comprehension. In most cases, though, comments will be -/// preserved. +/// This rule's fix is marked as unsafe, as it can change the behavior of the code if the iteration +/// has side effects (due to laziness and short-circuiting). The fix may also drop comments when +/// rewriting some comprehensions. +/// #[violation] -pub struct UnnecessaryComprehensionAnyAll; +pub struct UnnecessaryComprehensionInCall; -impl Violation for UnnecessaryComprehensionAnyAll { +impl Violation for UnnecessaryComprehensionInCall { const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; #[derive_message_formats] @@ -62,7 +80,7 @@ impl Violation for UnnecessaryComprehensionAnyAll { } /// C419 -pub(crate) fn unnecessary_comprehension_any_all( +pub(crate) fn unnecessary_comprehension_in_call( checker: &mut Checker, expr: &Expr, func: &Expr, @@ -72,10 +90,13 @@ pub(crate) fn unnecessary_comprehension_any_all( if !keywords.is_empty() { return; } + let Expr::Name(ast::ExprName { id, .. }) = func else { return; }; - if !matches!(id.as_str(), "all" | "any") { + if !(matches!(id.as_str(), "any" | "all") + || (checker.settings.preview.is_enabled() && matches!(id.as_str(), "sum" | "min" | "max"))) + { return; } let [arg] = args else { @@ -93,9 +114,9 @@ pub(crate) fn unnecessary_comprehension_any_all( return; } - let mut diagnostic = Diagnostic::new(UnnecessaryComprehensionAnyAll, arg.range()); + let mut diagnostic = Diagnostic::new(UnnecessaryComprehensionInCall, arg.range()); diagnostic.try_set_fix(|| { - fixes::fix_unnecessary_comprehension_any_all(expr, checker.locator(), checker.stylist()) + fixes::fix_unnecessary_comprehension_in_call(expr, checker.locator(), checker.stylist()) }); checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/settings.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/settings.rs index 41110886a5d4e..778d5601ef01d 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/settings.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/settings.rs @@ -4,7 +4,7 @@ use crate::display_settings; use ruff_macros::CacheKey; use std::fmt::{Display, Formatter}; -#[derive(Debug, Default, CacheKey)] +#[derive(Debug, Clone, Default, CacheKey)] pub struct Settings { pub allow_dict_calls_with_keyword_arguments: bool, } diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C419_C419.py.snap b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C419_C419.py.snap index 4fd150bc02551..026bbd7fe75ef 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C419_C419.py.snap +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C419_C419.py.snap @@ -98,67 +98,65 @@ C419.py:9:5: C419 [*] Unnecessary list comprehension 11 11 | # OK 12 12 | all(x.id for x in bar) -C419.py:24:5: C419 [*] Unnecessary list comprehension +C419.py:28:5: C419 [*] Unnecessary list comprehension | -22 | # Special comment handling -23 | any( -24 | [ # lbracket comment +26 | # Special comment handling +27 | any( +28 | [ # lbracket comment | _____^ -25 | | # second line comment -26 | | i.bit_count() -27 | | # random middle comment -28 | | for i in range(5) # rbracket comment -29 | | ] # rpar comment +29 | | # second line comment +30 | | i.bit_count() +31 | | # random middle comment +32 | | for i in range(5) # rbracket comment +33 | | ] # rpar comment | |_____^ C419 -30 | # trailing comment -31 | ) +34 | # trailing comment +35 | ) | = help: Remove unnecessary list comprehension ℹ Unsafe fix -21 21 | -22 22 | # Special comment handling -23 23 | any( -24 |- [ # lbracket comment -25 |- # second line comment -26 |- i.bit_count() - 24 |+ # lbracket comment - 25 |+ # second line comment - 26 |+ i.bit_count() -27 27 | # random middle comment -28 |- for i in range(5) # rbracket comment -29 |- ] # rpar comment - 28 |+ for i in range(5) # rbracket comment # rpar comment -30 29 | # trailing comment -31 30 | ) -32 31 | +25 25 | +26 26 | # Special comment handling +27 27 | any( +28 |- [ # lbracket comment +29 |- # second line comment +30 |- i.bit_count() + 28 |+ # lbracket comment + 29 |+ # second line comment + 30 |+ i.bit_count() +31 31 | # random middle comment +32 |- for i in range(5) # rbracket comment +33 |- ] # rpar comment + 32 |+ for i in range(5) # rbracket comment # rpar comment +34 33 | # trailing comment +35 34 | ) +36 35 | -C419.py:35:5: C419 [*] Unnecessary list comprehension +C419.py:39:5: C419 [*] Unnecessary list comprehension | -33 | # Weird case where the function call, opening bracket, and comment are all -34 | # on the same line. -35 | any([ # lbracket comment +37 | # Weird case where the function call, opening bracket, and comment are all +38 | # on the same line. +39 | any([ # lbracket comment | _____^ -36 | | # second line comment -37 | | i.bit_count() for i in range(5) # rbracket comment -38 | | ] # rpar comment +40 | | # second line comment +41 | | i.bit_count() for i in range(5) # rbracket comment +42 | | ] # rpar comment | |_____^ C419 -39 | ) +43 | ) | = help: Remove unnecessary list comprehension ℹ Unsafe fix -32 32 | -33 33 | # Weird case where the function call, opening bracket, and comment are all -34 34 | # on the same line. -35 |-any([ # lbracket comment -36 |- # second line comment -37 |- i.bit_count() for i in range(5) # rbracket comment -38 |- ] # rpar comment - 35 |+any( - 36 |+# lbracket comment - 37 |+# second line comment - 38 |+i.bit_count() for i in range(5) # rbracket comment # rpar comment -39 39 | ) - - +36 36 | +37 37 | # Weird case where the function call, opening bracket, and comment are all +38 38 | # on the same line. +39 |-any([ # lbracket comment +40 |- # second line comment +41 |- i.bit_count() for i in range(5) # rbracket comment +42 |- ] # rpar comment + 39 |+any( + 40 |+# lbracket comment + 41 |+# second line comment + 42 |+i.bit_count() for i in range(5) # rbracket comment # rpar comment +43 43 | ) diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C419_C419_2.py.snap b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C419_C419_2.py.snap new file mode 100644 index 0000000000000..d9845b4ae92f9 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C419_C419_2.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs +--- + diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__preview__C419_C419_1.py.snap b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__preview__C419_C419_1.py.snap new file mode 100644 index 0000000000000..559a6bed9ef02 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__preview__C419_C419_1.py.snap @@ -0,0 +1,55 @@ +--- +source: crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs +--- +C419_1.py:1:5: C419 [*] Unnecessary list comprehension + | +1 | sum([x.val for x in bar]) + | ^^^^^^^^^^^^^^^^^^^^ C419 +2 | min([x.val for x in bar]) +3 | max([x.val for x in bar]) + | + = help: Remove unnecessary list comprehension + +ℹ Unsafe fix +1 |-sum([x.val for x in bar]) + 1 |+sum(x.val for x in bar) +2 2 | min([x.val for x in bar]) +3 3 | max([x.val for x in bar]) +4 4 | + +C419_1.py:2:5: C419 [*] Unnecessary list comprehension + | +1 | sum([x.val for x in bar]) +2 | min([x.val for x in bar]) + | ^^^^^^^^^^^^^^^^^^^^ C419 +3 | max([x.val for x in bar]) + | + = help: Remove unnecessary list comprehension + +ℹ Unsafe fix +1 1 | sum([x.val for x in bar]) +2 |-min([x.val for x in bar]) + 2 |+min(x.val for x in bar) +3 3 | max([x.val for x in bar]) +4 4 | +5 5 | # Ok + +C419_1.py:3:5: C419 [*] Unnecessary list comprehension + | +1 | sum([x.val for x in bar]) +2 | min([x.val for x in bar]) +3 | max([x.val for x in bar]) + | ^^^^^^^^^^^^^^^^^^^^ C419 +4 | +5 | # Ok + | + = help: Remove unnecessary list comprehension + +ℹ Unsafe fix +1 1 | sum([x.val for x in bar]) +2 2 | min([x.val for x in bar]) +3 |-max([x.val for x in bar]) + 3 |+max(x.val for x in bar) +4 4 | +5 5 | # Ok +6 6 | sum(x.val for x in bar) diff --git a/crates/ruff_linter/src/rules/flake8_copyright/settings.rs b/crates/ruff_linter/src/rules/flake8_copyright/settings.rs index b62c221c769fa..03551b0a0a93f 100644 --- a/crates/ruff_linter/src/rules/flake8_copyright/settings.rs +++ b/crates/ruff_linter/src/rules/flake8_copyright/settings.rs @@ -7,7 +7,7 @@ use std::fmt::{Display, Formatter}; use crate::display_settings; use ruff_macros::CacheKey; -#[derive(Debug, CacheKey)] +#[derive(Debug, Clone, CacheKey)] pub struct Settings { pub notice_rgx: Regex, pub author: Option, diff --git a/crates/ruff_linter/src/rules/flake8_errmsg/settings.rs b/crates/ruff_linter/src/rules/flake8_errmsg/settings.rs index ec239435cee07..c51ec3a31356d 100644 --- a/crates/ruff_linter/src/rules/flake8_errmsg/settings.rs +++ b/crates/ruff_linter/src/rules/flake8_errmsg/settings.rs @@ -4,7 +4,7 @@ use crate::display_settings; use ruff_macros::CacheKey; use std::fmt::{Display, Formatter}; -#[derive(Debug, Default, CacheKey)] +#[derive(Debug, Clone, Default, CacheKey)] pub struct Settings { pub max_string_length: usize, } diff --git a/crates/ruff_linter/src/rules/flake8_gettext/settings.rs b/crates/ruff_linter/src/rules/flake8_gettext/settings.rs index 6e3a6c367cd10..76180b45ac59d 100644 --- a/crates/ruff_linter/src/rules/flake8_gettext/settings.rs +++ b/crates/ruff_linter/src/rules/flake8_gettext/settings.rs @@ -2,7 +2,7 @@ use crate::display_settings; use ruff_macros::CacheKey; use std::fmt::{Display, Formatter}; -#[derive(Debug, CacheKey)] +#[derive(Debug, Clone, CacheKey)] pub struct Settings { pub functions_names: Vec, } diff --git a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/settings.rs b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/settings.rs index 90c6a9a1812fe..ab805839b67a0 100644 --- a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/settings.rs +++ b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/settings.rs @@ -4,7 +4,7 @@ use crate::display_settings; use ruff_macros::CacheKey; use std::fmt::{Display, Formatter}; -#[derive(Debug, CacheKey)] +#[derive(Debug, Clone, CacheKey)] pub struct Settings { pub allow_multiline: bool, } diff --git a/crates/ruff_linter/src/rules/flake8_import_conventions/mod.rs b/crates/ruff_linter/src/rules/flake8_import_conventions/mod.rs index fc5c6034af93d..de5d8a89155a8 100644 --- a/crates/ruff_linter/src/rules/flake8_import_conventions/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_import_conventions/mod.rs @@ -27,7 +27,7 @@ mod tests { #[test] fn custom() -> Result<()> { - let mut aliases = super::settings::default_aliases(); + let mut aliases = default_aliases(); aliases.extend(FxHashMap::from_iter([ ("dask.array".to_string(), "da".to_string()), ("dask.dataframe".to_string(), "dd".to_string()), @@ -126,7 +126,7 @@ mod tests { #[test] fn override_defaults() -> Result<()> { - let mut aliases = super::settings::default_aliases(); + let mut aliases = default_aliases(); aliases.extend(FxHashMap::from_iter([( "numpy".to_string(), "nmp".to_string(), @@ -149,7 +149,7 @@ mod tests { #[test] fn from_imports() -> Result<()> { - let mut aliases = super::settings::default_aliases(); + let mut aliases = default_aliases(); aliases.extend(FxHashMap::from_iter([ ("xml.dom.minidom".to_string(), "md".to_string()), ( @@ -182,4 +182,26 @@ mod tests { assert_messages!(diagnostics); Ok(()) } + + #[test] + fn same_name() -> Result<()> { + let mut aliases = default_aliases(); + aliases.extend(FxHashMap::from_iter([( + "django.conf.settings".to_string(), + "settings".to_string(), + )])); + let diagnostics = test_path( + Path::new("flake8_import_conventions/same_name.py"), + &LinterSettings { + flake8_import_conventions: super::settings::Settings { + aliases, + banned_aliases: FxHashMap::default(), + banned_from: FxHashSet::default(), + }, + ..LinterSettings::for_rule(Rule::UnconventionalImportAlias) + }, + )?; + assert_messages!(diagnostics); + Ok(()) + } } diff --git a/crates/ruff_linter/src/rules/flake8_import_conventions/rules/unconventional_import_alias.rs b/crates/ruff_linter/src/rules/flake8_import_conventions/rules/unconventional_import_alias.rs index 4db3974c965cf..9a16bb8ed325d 100644 --- a/crates/ruff_linter/src/rules/flake8_import_conventions/rules/unconventional_import_alias.rs +++ b/crates/ruff_linter/src/rules/flake8_import_conventions/rules/unconventional_import_alias.rs @@ -66,7 +66,7 @@ pub(crate) fn unconventional_import_alias( let expected_alias = conventions.get(qualified_name.as_str())?; let name = binding.name(checker.locator()); - if binding.is_alias() && name == expected_alias { + if name == expected_alias { return None; } diff --git a/crates/ruff_linter/src/rules/flake8_import_conventions/settings.rs b/crates/ruff_linter/src/rules/flake8_import_conventions/settings.rs index 50c5aacc67eb1..292658a6cbca0 100644 --- a/crates/ruff_linter/src/rules/flake8_import_conventions/settings.rs +++ b/crates/ruff_linter/src/rules/flake8_import_conventions/settings.rs @@ -57,7 +57,7 @@ impl FromIterator for BannedAliases { } } -#[derive(Debug, CacheKey)] +#[derive(Debug, Clone, CacheKey)] pub struct Settings { pub aliases: FxHashMap, pub banned_aliases: FxHashMap, diff --git a/crates/ruff_linter/src/rules/flake8_import_conventions/snapshots/ruff_linter__rules__flake8_import_conventions__tests__defaults.snap b/crates/ruff_linter/src/rules/flake8_import_conventions/snapshots/ruff_linter__rules__flake8_import_conventions__tests__defaults.snap index 09c79038f15b2..d3f4b0a501404 100644 --- a/crates/ruff_linter/src/rules/flake8_import_conventions/snapshots/ruff_linter__rules__flake8_import_conventions__tests__defaults.snap +++ b/crates/ruff_linter/src/rules/flake8_import_conventions/snapshots/ruff_linter__rules__flake8_import_conventions__tests__defaults.snap @@ -256,7 +256,7 @@ defaults.py:21:23: ICN001 [*] `tkinter` should be imported as `tk` 21 |+ import tkinter as tk 22 22 | import networkx as nxy 23 23 | -24 24 | def conventional_aliases(): +24 24 | defaults.py:22:24: ICN001 [*] `networkx` should be imported as `nx` | @@ -264,8 +264,6 @@ defaults.py:22:24: ICN001 [*] `networkx` should be imported as `nx` 21 | import tkinter as tkr 22 | import networkx as nxy | ^^^ ICN001 -23 | -24 | def conventional_aliases(): | = help: Alias `networkx` to `nx` @@ -276,7 +274,5 @@ defaults.py:22:24: ICN001 [*] `networkx` should be imported as `nx` 22 |- import networkx as nxy 22 |+ import networkx as nx 23 23 | -24 24 | def conventional_aliases(): -25 25 | import altair as alt - - +24 24 | +25 25 | def conventional_aliases(): diff --git a/crates/ruff_linter/src/rules/flake8_import_conventions/snapshots/ruff_linter__rules__flake8_import_conventions__tests__same_name.snap b/crates/ruff_linter/src/rules/flake8_import_conventions/snapshots/ruff_linter__rules__flake8_import_conventions__tests__same_name.snap new file mode 100644 index 0000000000000..c30be9f37554c --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_import_conventions/snapshots/ruff_linter__rules__flake8_import_conventions__tests__same_name.snap @@ -0,0 +1,17 @@ +--- +source: crates/ruff_linter/src/rules/flake8_import_conventions/mod.rs +--- +same_name.py:10:41: ICN001 [*] `django.conf.settings` should be imported as `settings` + | + 9 | def unconventional_alias(): +10 | from django.conf import settings as s + | ^ ICN001 + | + = help: Alias `django.conf.settings` to `settings` + +ℹ Unsafe fix +7 7 | +8 8 | +9 9 | def unconventional_alias(): +10 |- from django.conf import settings as s + 10 |+ from django.conf import settings as settings diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/settings.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/settings.rs index cab3d2d5a3eed..50b08c48c1f91 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/settings.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/settings.rs @@ -24,7 +24,7 @@ pub fn default_broad_exceptions() -> Vec { .to_vec() } -#[derive(Debug, CacheKey)] +#[derive(Debug, Clone, CacheKey)] pub struct Settings { pub fixture_parentheses: bool, pub parametrize_names_type: types::ParametrizeNameType, diff --git a/crates/ruff_linter/src/rules/flake8_quotes/mod.rs b/crates/ruff_linter/src/rules/flake8_quotes/mod.rs index 8a69e6c294a51..9025b10e67e55 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_quotes/mod.rs @@ -195,4 +195,61 @@ mod tests { assert_messages!(snapshot, diagnostics); Ok(()) } + + #[test_case(Path::new("doubles_all.py"))] + fn only_inline(path: &Path) -> Result<()> { + let snapshot = format!("only_inline_{}", path.to_string_lossy()); + let diagnostics = test_path( + Path::new("flake8_quotes").join(path).as_path(), + &LinterSettings { + flake8_quotes: super::settings::Settings { + inline_quotes: Quote::Single, + multiline_quotes: Quote::Single, + docstring_quotes: Quote::Single, + avoid_escape: true, + }, + ..LinterSettings::for_rules(vec![Rule::BadQuotesInlineString]) + }, + )?; + assert_messages!(snapshot, diagnostics); + Ok(()) + } + + #[test_case(Path::new("doubles_all.py"))] + fn only_multiline(path: &Path) -> Result<()> { + let snapshot = format!("only_multiline_{}", path.to_string_lossy()); + let diagnostics = test_path( + Path::new("flake8_quotes").join(path).as_path(), + &LinterSettings { + flake8_quotes: super::settings::Settings { + inline_quotes: Quote::Single, + multiline_quotes: Quote::Single, + docstring_quotes: Quote::Single, + avoid_escape: true, + }, + ..LinterSettings::for_rules(vec![Rule::BadQuotesMultilineString]) + }, + )?; + assert_messages!(snapshot, diagnostics); + Ok(()) + } + + #[test_case(Path::new("doubles_all.py"))] + fn only_docstring(path: &Path) -> Result<()> { + let snapshot = format!("only_docstring_{}", path.to_string_lossy()); + let diagnostics = test_path( + Path::new("flake8_quotes").join(path).as_path(), + &LinterSettings { + flake8_quotes: super::settings::Settings { + inline_quotes: Quote::Single, + multiline_quotes: Quote::Single, + docstring_quotes: Quote::Single, + avoid_escape: true, + }, + ..LinterSettings::for_rules(vec![Rule::BadQuotesDocstring]) + }, + )?; + assert_messages!(snapshot, diagnostics); + Ok(()) + } } diff --git a/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs b/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs index e5eb47a0dea8c..7c01db8c5af8f 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs +++ b/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs @@ -5,6 +5,7 @@ use ruff_source_file::Locator; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; +use crate::registry::Rule; use super::super::settings::Quote; @@ -334,6 +335,11 @@ fn strings(checker: &mut Checker, sequence: &[TextRange]) { for (range, trivia) in sequence.iter().zip(trivia) { if trivia.is_multiline { + // If multiline strings aren't enforced, ignore it. + if !checker.enabled(Rule::BadQuotesMultilineString) { + continue; + } + // If our string is or contains a known good string, ignore it. if trivia .raw_text @@ -375,6 +381,11 @@ fn strings(checker: &mut Checker, sequence: &[TextRange]) { // If we're not using the preferred type, only allow use to avoid escapes. && !relax_quote { + // If inline strings aren't enforced, ignore it. + if !checker.enabled(Rule::BadQuotesInlineString) { + continue; + } + if trivia.has_empty_text() && text_ends_at_quote(locator, *range, quotes_settings.inline_quotes) { @@ -438,13 +449,8 @@ pub(crate) fn check_string_quotes(checker: &mut Checker, string_like: StringLike return; } - // If the string is part of a f-string, ignore it. - if checker - .indexer() - .fstring_ranges() - .outermost(string_like.start()) - .is_some_and(|outer| outer.start() < string_like.start() && string_like.end() < outer.end()) - { + // TODO(dhruvmanila): Support checking for escaped quotes in f-strings. + if checker.semantic().in_f_string_replacement_field() { return; } @@ -455,10 +461,14 @@ pub(crate) fn check_string_quotes(checker: &mut Checker, string_like: StringLike }; if checker.semantic().in_docstring() { - for range in ranges { - docstring(checker, range); + if checker.enabled(Rule::BadQuotesDocstring) { + for range in ranges { + docstring(checker, range); + } } } else { - strings(checker, &ranges); + if checker.any_enabled(&[Rule::BadQuotesInlineString, Rule::BadQuotesMultilineString]) { + strings(checker, &ranges); + } } } diff --git a/crates/ruff_linter/src/rules/flake8_quotes/settings.rs b/crates/ruff_linter/src/rules/flake8_quotes/settings.rs index 5e0c93beadab0..b241e70b49350 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/settings.rs +++ b/crates/ruff_linter/src/rules/flake8_quotes/settings.rs @@ -31,7 +31,7 @@ impl From for Quote { } } -#[derive(Debug, CacheKey)] +#[derive(Debug, Clone, CacheKey)] pub struct Settings { pub inline_quotes: Quote, pub multiline_quotes: Quote, diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__only_docstring_doubles_all.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__only_docstring_doubles_all.py.snap new file mode 100644 index 0000000000000..6a87062097567 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__only_docstring_doubles_all.py.snap @@ -0,0 +1,18 @@ +--- +source: crates/ruff_linter/src/rules/flake8_quotes/mod.rs +--- +doubles_all.py:1:1: Q002 [*] Double quote docstring found but single quotes preferred + | +1 | """This is a docstring.""" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ Q002 +2 | +3 | this_is_an_inline_string = "double quote string" + | + = help: Replace double quotes docstring with single quotes + +ℹ Safe fix +1 |-"""This is a docstring.""" + 1 |+'''This is a docstring.''' +2 2 | +3 3 | this_is_an_inline_string = "double quote string" +4 4 | diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__only_inline_doubles_all.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__only_inline_doubles_all.py.snap new file mode 100644 index 0000000000000..0ca50ac62e93a --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__only_inline_doubles_all.py.snap @@ -0,0 +1,22 @@ +--- +source: crates/ruff_linter/src/rules/flake8_quotes/mod.rs +--- +doubles_all.py:3:28: Q000 [*] Double quotes found but single quotes preferred + | +1 | """This is a docstring.""" +2 | +3 | this_is_an_inline_string = "double quote string" + | ^^^^^^^^^^^^^^^^^^^^^ Q000 +4 | +5 | this_is_a_multiline_string = """ + | + = help: Replace double quotes with single quotes + +ℹ Safe fix +1 1 | """This is a docstring.""" +2 2 | +3 |-this_is_an_inline_string = "double quote string" + 3 |+this_is_an_inline_string = 'double quote string' +4 4 | +5 5 | this_is_a_multiline_string = """ +6 6 | double quote string diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__only_multiline_doubles_all.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__only_multiline_doubles_all.py.snap new file mode 100644 index 0000000000000..ddffe1bb80a5f --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__only_multiline_doubles_all.py.snap @@ -0,0 +1,24 @@ +--- +source: crates/ruff_linter/src/rules/flake8_quotes/mod.rs +--- +doubles_all.py:5:30: Q001 [*] Double quote multiline found but single quotes preferred + | +3 | this_is_an_inline_string = "double quote string" +4 | +5 | this_is_a_multiline_string = """ + | ______________________________^ +6 | | double quote string +7 | | """ + | |___^ Q001 + | + = help: Replace double multiline quotes with single quotes + +ℹ Safe fix +2 2 | +3 3 | this_is_an_inline_string = "double quote string" +4 4 | +5 |-this_is_a_multiline_string = """ + 5 |+this_is_a_multiline_string = ''' +6 6 | double quote string +7 |-""" + 7 |+''' diff --git a/crates/ruff_linter/src/rules/flake8_return/rules/function.rs b/crates/ruff_linter/src/rules/flake8_return/rules/function.rs index 9e0f90c4077e4..124abdfee953e 100644 --- a/crates/ruff_linter/src/rules/flake8_return/rules/function.rs +++ b/crates/ruff_linter/src/rules/flake8_return/rules/function.rs @@ -567,6 +567,12 @@ fn unnecessary_assign(checker: &mut Checker, stack: &Stack) { continue; } + // Ignore variables that have an annotation defined elsewhere. + if stack.annotations.contains(assigned_id.as_str()) { + continue; + } + + // Ignore `nonlocal` and `global` variables. if stack.non_locals.contains(assigned_id.as_str()) { continue; } diff --git a/crates/ruff_linter/src/rules/flake8_return/snapshots/ruff_linter__rules__flake8_return__tests__RET504_RET504.py.snap b/crates/ruff_linter/src/rules/flake8_return/snapshots/ruff_linter__rules__flake8_return__tests__RET504_RET504.py.snap index 3656e71afbd2d..46ffb2df68b29 100644 --- a/crates/ruff_linter/src/rules/flake8_return/snapshots/ruff_linter__rules__flake8_return__tests__RET504_RET504.py.snap +++ b/crates/ruff_linter/src/rules/flake8_return/snapshots/ruff_linter__rules__flake8_return__tests__RET504_RET504.py.snap @@ -241,4 +241,19 @@ RET504.py:400:12: RET504 [*] Unnecessary assignment to `y` before `return` state 402 401 | 403 402 | def foo(): +RET504.py:423:16: RET504 [*] Unnecessary assignment to `services` before `return` statement + | +421 | if "services" in a: +422 | services = a["services"] +423 | return services + | ^^^^^^^^ RET504 + | + = help: Remove unnecessary assignment +ℹ Unsafe fix +419 419 | # See: https://github.com/astral-sh/ruff/issues/10732 +420 420 | def func(a: dict[str, int]) -> list[dict[str, int]]: +421 421 | if "services" in a: +422 |- services = a["services"] +423 |- return services + 422 |+ return a["services"] diff --git a/crates/ruff_linter/src/rules/flake8_return/visitor.rs b/crates/ruff_linter/src/rules/flake8_return/visitor.rs index f583c22d6cab0..c88abc6676bd5 100644 --- a/crates/ruff_linter/src/rules/flake8_return/visitor.rs +++ b/crates/ruff_linter/src/rules/flake8_return/visitor.rs @@ -13,6 +13,21 @@ pub(super) struct Stack<'data> { pub(super) elifs_elses: Vec<(&'data [Stmt], &'data ElifElseClause)>, /// The non-local variables in the current function. pub(super) non_locals: FxHashSet<&'data str>, + /// The annotated variables in the current function. + /// + /// For example, consider: + /// ```python + /// x: int + /// + /// if True: + /// x = foo() + /// return x + /// ``` + /// + /// In this case, the annotation on `x` is used to cast the return value + /// of `foo()` to an `int`. Removing the `x = foo()` statement would + /// change the return type of the function. + pub(super) annotations: FxHashSet<&'data str>, /// Whether the current function is a generator. pub(super) is_generator: bool, /// The `assignment`-to-`return` statement pairs in the current function. @@ -86,6 +101,14 @@ impl<'semantic, 'a> Visitor<'a> for ReturnVisitor<'semantic, 'a> { .non_locals .extend(names.iter().map(Identifier::as_str)); } + Stmt::AnnAssign(ast::StmtAnnAssign { target, value, .. }) => { + // Ex) `x: int` + if value.is_none() { + if let Expr::Name(name) = target.as_ref() { + self.stack.annotations.insert(name.id.as_str()); + } + } + } Stmt::Return(stmt_return) => { // If the `return` statement is preceded by an `assignment` statement, then the // `assignment` statement may be redundant. @@ -163,19 +186,6 @@ impl<'semantic, 'a> Visitor<'a> for ReturnVisitor<'semantic, 'a> { } } -/// RET504 -/// If the last statement is a `return` statement, and the second-to-last statement is a -/// `with` statement that suppresses an exception, then we should not analyze the `return` -/// statement for unnecessary assignments. Otherwise we will suggest removing the assignment -/// and the `with` statement, which would change the behavior of the code. -/// -/// Example: -/// ```python -/// def foo(data): -/// with suppress(JSONDecoderError): -/// data = data.decode() -/// return data - /// Returns `true` if the [`With`] statement is known to have a conditional body. In other words: /// if the [`With`] statement's body may or may not run. /// diff --git a/crates/ruff_linter/src/rules/flake8_self/settings.rs b/crates/ruff_linter/src/rules/flake8_self/settings.rs index c59a0ec89d221..cb3027fa90c23 100644 --- a/crates/ruff_linter/src/rules/flake8_self/settings.rs +++ b/crates/ruff_linter/src/rules/flake8_self/settings.rs @@ -17,7 +17,7 @@ pub const IGNORE_NAMES: [&str; 7] = [ "_value_", ]; -#[derive(Debug, CacheKey)] +#[derive(Debug, Clone, CacheKey)] pub struct Settings { pub ignore_names: Vec, } diff --git a/crates/ruff_linter/src/rules/flake8_tidy_imports/settings.rs b/crates/ruff_linter/src/rules/flake8_tidy_imports/settings.rs index 8f9e29ea21aac..fee7a12482cff 100644 --- a/crates/ruff_linter/src/rules/flake8_tidy_imports/settings.rs +++ b/crates/ruff_linter/src/rules/flake8_tidy_imports/settings.rs @@ -39,7 +39,7 @@ impl Display for Strictness { } } -#[derive(Debug, CacheKey, Default)] +#[derive(Debug, Clone, CacheKey, Default)] pub struct Settings { pub ban_relative_imports: Strictness, pub banned_api: FxHashMap, diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/settings.rs b/crates/ruff_linter/src/rules/flake8_type_checking/settings.rs index fa8214e5b74af..11f5f87e500cf 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/settings.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/settings.rs @@ -4,7 +4,7 @@ use crate::display_settings; use ruff_macros::CacheKey; use std::fmt::{Display, Formatter}; -#[derive(Debug, CacheKey)] +#[derive(Debug, Clone, CacheKey)] pub struct Settings { pub strict: bool, pub exempt_modules: Vec, diff --git a/crates/ruff_linter/src/rules/flake8_unused_arguments/settings.rs b/crates/ruff_linter/src/rules/flake8_unused_arguments/settings.rs index 7e13bc6495a9a..5cd02d876a890 100644 --- a/crates/ruff_linter/src/rules/flake8_unused_arguments/settings.rs +++ b/crates/ruff_linter/src/rules/flake8_unused_arguments/settings.rs @@ -4,7 +4,7 @@ use crate::display_settings; use ruff_macros::CacheKey; use std::fmt::{Display, Formatter}; -#[derive(Debug, Default, CacheKey)] +#[derive(Debug, Clone, Default, CacheKey)] pub struct Settings { pub ignore_variadic_names: bool, } diff --git a/crates/ruff_linter/src/rules/isort/categorize.rs b/crates/ruff_linter/src/rules/isort/categorize.rs index 874070135c21e..7f5a10bfd0983 100644 --- a/crates/ruff_linter/src/rules/isort/categorize.rs +++ b/crates/ruff_linter/src/rules/isort/categorize.rs @@ -270,7 +270,7 @@ pub(crate) fn categorize_imports<'a>( block_by_type } -#[derive(Debug, Default, CacheKey)] +#[derive(Debug, Clone, Default, CacheKey)] pub struct KnownModules { /// A map of known modules to their section. known: Vec<(glob::Pattern, ImportSection)>, diff --git a/crates/ruff_linter/src/rules/isort/settings.rs b/crates/ruff_linter/src/rules/isort/settings.rs index 8ae6464932123..7307b6664a08d 100644 --- a/crates/ruff_linter/src/rules/isort/settings.rs +++ b/crates/ruff_linter/src/rules/isort/settings.rs @@ -44,7 +44,7 @@ impl Display for RelativeImportsOrder { } } -#[derive(Debug, CacheKey)] +#[derive(Debug, Clone, CacheKey)] #[allow(clippy::struct_excessive_bools)] pub struct Settings { pub required_imports: BTreeSet, diff --git a/crates/ruff_linter/src/rules/mccabe/settings.rs b/crates/ruff_linter/src/rules/mccabe/settings.rs index 65abe3c91d1a0..d5e2db45f73a4 100644 --- a/crates/ruff_linter/src/rules/mccabe/settings.rs +++ b/crates/ruff_linter/src/rules/mccabe/settings.rs @@ -4,7 +4,7 @@ use crate::display_settings; use ruff_macros::CacheKey; use std::fmt::{Display, Formatter}; -#[derive(Debug, CacheKey)] +#[derive(Debug, Clone, CacheKey)] pub struct Settings { pub max_complexity: usize, } diff --git a/crates/ruff_linter/src/rules/pep8_naming/helpers.rs b/crates/ruff_linter/src/rules/pep8_naming/helpers.rs index 068c4b143b95e..e94ace527c638 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/helpers.rs +++ b/crates/ruff_linter/src/rules/pep8_naming/helpers.rs @@ -1,8 +1,8 @@ use itertools::Itertools; use ruff_python_ast::name::UnqualifiedName; -use ruff_python_ast::{self as ast, Arguments, Expr, Stmt}; -use ruff_python_semantic::SemanticModel; +use ruff_python_ast::{self as ast, Expr, Stmt}; +use ruff_python_semantic::{analyze, SemanticModel}; use ruff_python_stdlib::str::{is_cased_lowercase, is_cased_uppercase}; pub(super) fn is_camelcase(name: &str) -> bool { @@ -86,16 +86,13 @@ pub(super) fn is_type_alias_assignment(stmt: &Stmt, semantic: &SemanticModel) -> } /// Returns `true` if the statement is an assignment to a `TypedDict`. -pub(super) fn is_typed_dict_class(arguments: Option<&Arguments>, semantic: &SemanticModel) -> bool { +pub(super) fn is_typed_dict_class(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool { if !semantic.seen_typing() { return false; } - arguments.is_some_and(|arguments| { - arguments - .args - .iter() - .any(|base| semantic.match_typing_expr(base, "TypedDict")) + analyze::class::any_qualified_name(class_def, semantic, &|qualified_name| { + semantic.match_typing_qualified_name(&qualified_name, "TypedDict") }) } diff --git a/crates/ruff_linter/src/rules/pep8_naming/rules/mixed_case_variable_in_class_scope.rs b/crates/ruff_linter/src/rules/pep8_naming/rules/mixed_case_variable_in_class_scope.rs index 6ebed08650a61..15b11ac894bd1 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/rules/mixed_case_variable_in_class_scope.rs +++ b/crates/ruff_linter/src/rules/pep8_naming/rules/mixed_case_variable_in_class_scope.rs @@ -1,7 +1,6 @@ -use ruff_python_ast::{Arguments, Expr}; - use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::{self as ast, Expr}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; @@ -55,7 +54,7 @@ pub(crate) fn mixed_case_variable_in_class_scope( checker: &mut Checker, expr: &Expr, name: &str, - arguments: Option<&Arguments>, + class_def: &ast::StmtClassDef, ) { if !helpers::is_mixed_case(name) { return; @@ -64,7 +63,7 @@ pub(crate) fn mixed_case_variable_in_class_scope( let parent = checker.semantic().current_statement(); if helpers::is_named_tuple_assignment(parent, checker.semantic()) - || helpers::is_typed_dict_class(arguments, checker.semantic()) + || helpers::is_typed_dict_class(class_def, checker.semantic()) { return; } diff --git a/crates/ruff_linter/src/rules/pep8_naming/settings.rs b/crates/ruff_linter/src/rules/pep8_naming/settings.rs index 7ad3830c8107f..9705b7cde05d5 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/settings.rs +++ b/crates/ruff_linter/src/rules/pep8_naming/settings.rs @@ -11,7 +11,7 @@ use ruff_macros::CacheKey; use crate::display_settings; -#[derive(Debug, CacheKey)] +#[derive(Debug, Clone, CacheKey)] pub struct Settings { pub ignore_names: IgnoreNames, pub classmethod_decorators: Vec, @@ -85,7 +85,7 @@ static DEFAULTS: &[&str] = &[ "maxDiff", ]; -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum IgnoreNames { Default, UserProvided { diff --git a/crates/ruff_linter/src/rules/pycodestyle/helpers.rs b/crates/ruff_linter/src/rules/pycodestyle/helpers.rs index 0c7b133787f99..2b39a6dad110b 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/helpers.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/helpers.rs @@ -1,3 +1,4 @@ +/// Returns `true` if the name should be considered "ambiguous". pub(super) fn is_ambiguous_name(name: &str) -> bool { name == "l" || name == "I" || name == "O" } diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/lambda_assignment.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/lambda_assignment.rs index 26242791398b0..776b2e8842d0c 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/lambda_assignment.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/lambda_assignment.rs @@ -1,14 +1,13 @@ +use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; +use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::{ self as ast, Expr, Identifier, Parameter, ParameterWithDefault, Parameters, Stmt, }; -use ruff_text_size::{Ranged, TextRange}; - -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; -use ruff_macros::{derive_message_formats, violation}; use ruff_python_codegen::Generator; use ruff_python_semantic::SemanticModel; use ruff_python_trivia::{has_leading_content, has_trailing_content, leading_indentation}; use ruff_source_file::UniversalNewlines; +use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; @@ -106,12 +105,17 @@ pub(crate) fn lambda_assignment( } } - // 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. Even if it _doesn't_ have an annotation, rewriting safely would require - // making this a static method. - // See: https://github.com/astral-sh/ruff/issues/3046 + // If the assignment is a class attribute (with an annotation), ignore it. + // + // This is most common for, e.g., dataclasses and Pydantic models. Those libraries will + // treat the lambda as an assignable field, and the use of a lambda is almost certainly + // intentional. + if annotation.is_some() && checker.semantic().current_scope().kind.is_class() { + return; + } + + // Otherwise, if the assignment is in a class body, flag it, but use a display-only fix. + // Rewriting safely would require making this a static method. // // Similarly, if the lambda is shadowing a variable in the current scope, // rewriting it as a function declaration may break type-checking. @@ -179,6 +183,7 @@ fn extract_types(annotation: &Expr, semantic: &SemanticModel) -> Option<(Vec, diff --git a/crates/ruff_linter/src/rules/pycodestyle/settings.rs b/crates/ruff_linter/src/rules/pycodestyle/settings.rs index 1ce1d1c029ea6..b034a778740a7 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/settings.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/settings.rs @@ -6,7 +6,7 @@ use std::fmt; use crate::line_width::LineLength; -#[derive(Debug, Default, CacheKey)] +#[derive(Debug, Clone, Default, CacheKey)] pub struct Settings { pub max_line_length: LineLength, pub max_doc_length: Option, diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E731_E731.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E731_E731.py.snap index 9711dbafcdd9f..32d034f1e76d7 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E731_E731.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E731_E731.py.snap @@ -120,25 +120,6 @@ E731.py:57:5: E731 Do not assign a `lambda` expression, use a `def` 59 60 | 60 61 | class Scope: -E731.py:64:5: E731 Do not assign a `lambda` expression, use a `def` - | -63 | # E731 -64 | f: Callable[[int], int] = lambda x: 2 * x - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E731 - | - = help: Rewrite `f` as a `def` - -ℹ Display-only 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] @@ -383,5 +364,6 @@ E731.py:147:5: E731 [*] Do not assign a `lambda` expression, use a `def` 149 |- ) 147 |+ def f(): 148 |+ return (i := 1), - - +150 149 | +151 150 | +152 151 | from dataclasses import dataclass diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/capitalized.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/capitalized.rs index 7988490f6c18f..88f02716764d0 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/capitalized.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/capitalized.rs @@ -59,26 +59,36 @@ pub(crate) fn capitalized(checker: &mut Checker, docstring: &Docstring) { } let body = docstring.body(); - let Some(first_word) = body.split(' ').next() else { - return; - }; - - // Like pydocstyle, we only support ASCII for now. - for char in first_word.chars() { - if !char.is_ascii_alphabetic() && char != '\'' { - return; - } - } + let first_word = body.split_once(' ').map_or_else( + || { + // If the docstring is a single word, trim the punctuation marks because + // it makes the ASCII test below fail. + body.trim_end_matches(['.', '!', '?']) + }, + |(first_word, _)| first_word, + ); let mut first_word_chars = first_word.chars(); let Some(first_char) = first_word_chars.next() else { return; }; + + if !first_char.is_ascii() { + return; + } + let uppercase_first_char = first_char.to_ascii_uppercase(); if first_char == uppercase_first_char { return; } + // Like pydocstyle, we only support ASCII for now. + for char in first_word.chars().skip(1) { + if !char.is_ascii_alphabetic() && char != '\'' { + return; + } + } + let capitalized_word = uppercase_first_char.to_string() + first_word_chars.as_str(); let mut diagnostic = Diagnostic::new( diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs index c9074b377eb42..ea02dc57309c7 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs @@ -2,6 +2,7 @@ use itertools::Itertools; use once_cell::sync::Lazy; use regex::Regex; use rustc_hash::FxHashSet; +use std::ops::Add; use ruff_diagnostics::{AlwaysFixableViolation, Violation}; use ruff_diagnostics::{Diagnostic, Edit, Fix}; @@ -10,8 +11,8 @@ use ruff_python_ast::docstrings::{clean_space, leading_space}; use ruff_python_ast::identifier::Identifier; use ruff_python_ast::ParameterWithDefault; use ruff_python_semantic::analyze::visibility::is_staticmethod; -use ruff_python_trivia::{textwrap::dedent, PythonWhitespace}; -use ruff_source_file::NewlineWithTrailingNewline; +use ruff_python_trivia::{textwrap::dedent, Cursor}; +use ruff_source_file::{Line, NewlineWithTrailingNewline}; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use crate::checkers::ast::Checker; @@ -25,13 +26,15 @@ use crate::rules::pydocstyle::settings::Convention; /// 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. +/// This rule enforces a consistent style for docstrings with multiple +/// sections. /// -/// 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. +/// Multiline 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 convention is that all sections should use +/// consistent indentation. In each section, the header should match the +/// indentation of the docstring's opening quotes, and the body should be +/// indented one level further. /// /// ## Example /// ```python @@ -105,15 +108,20 @@ impl AlwaysFixableViolation for SectionNotOverIndented { /// 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. +/// This rule enforces a consistent style for multiline numpy-style docstrings, +/// and helps prevent incorrect syntax in docstrings using reStructuredText. /// -/// Some docstring formats (like reStructuredText) use underlines to separate -/// section bodies from section headers. +/// Multiline numpy-style docstrings are typically composed of a summary line, +/// followed by a blank line, followed by a series of sections. Each section +/// has a section header and a section body, and there should be a series of +/// underline characters in the line following the header. The underline should +/// have the same indentation as the header. /// -/// Avoid over-indenting the section underlines, as this can cause syntax -/// errors in reStructuredText. +/// This rule enforces a consistent style for multiline numpy-style docstrings +/// with sections. If your docstring uses reStructuredText, the rule also +/// helps protect against incorrect reStructuredText syntax, which would cause +/// errors if you tried to use a tool such as Sphinx to generate documentation +/// from the docstring. /// /// This rule is enabled when using the `numpy` convention, and disabled when /// using the `google` or `pep257` conventions. @@ -131,12 +139,12 @@ impl AlwaysFixableViolation for SectionNotOverIndented { /// Time spent traveling. /// /// Returns -/// ------- +/// ------- /// float /// Speed as distance divided by time. /// /// Raises -/// ------ +/// ------ /// FasterThanLightError /// If speed is greater than the speed of light. /// """ @@ -204,11 +212,12 @@ impl AlwaysFixableViolation for SectionUnderlineNotOverIndented { /// 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. +/// For stylistic consistency, all section headers in a docstring should be +/// capitalized. /// -/// Section headers should be capitalized, for consistency. +/// Multiline docstrings are typically composed of a summary line, followed by +/// a blank line, followed by a series of sections. Each section typically has +/// a header and a body. /// /// ## Example /// ```python @@ -279,22 +288,24 @@ impl AlwaysFixableViolation for CapitalizeSectionName { } /// ## What it does -/// Checks that section headers in docstrings that are not followed by a -/// newline. +/// Checks for section headers in docstrings that are followed by non-newline +/// characters. /// /// ## 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. +/// This rule enforces a consistent style for multiline numpy-style docstrings. /// -/// Section headers should be followed by a newline, and not by another -/// character (like a colon), for consistency. +/// Multiline numpy-style docstrings are typically composed of a summary line, +/// followed by a blank line, followed by a series of sections. Each section +/// has a section header and a section body. The section header should be +/// followed by a newline, rather than by some other character (like a colon). /// /// This rule is enabled when using the `numpy` convention, and disabled /// when using the `google` or `pep257` conventions. /// /// ## Example /// ```python +/// # The `Parameters`, `Returns` and `Raises` section headers are all followed +/// # by a colon in this function's docstring: /// def calculate_speed(distance: float, time: float) -> float: /// """Calculate speed as distance divided by time. /// @@ -379,12 +390,19 @@ impl AlwaysFixableViolation for NewLineAfterSectionName { /// 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. +/// This rule enforces a consistent style for multiline numpy-style docstrings, +/// and helps prevent incorrect syntax in docstrings using reStructuredText. +/// +/// Multiline numpy-style docstrings are typically composed of a summary line, +/// followed by a blank line, followed by a series of sections. Each section +/// has a section header and a section body, and the header should be followed +/// by a series of underline characters in the following line. /// -/// Some docstring formats (like reStructuredText) use underlines to separate -/// section bodies from section headers. +/// This rule enforces a consistent style for multiline numpy-style docstrings +/// with sections. If your docstring uses reStructuredText, the rule also +/// helps protect against incorrect reStructuredText syntax, which would cause +/// errors if you tried to use a tool such as Sphinx to generate documentation +/// from the docstring. /// /// This rule is enabled when using the `numpy` convention, and disabled /// when using the `google` or `pep257` conventions. @@ -475,15 +493,19 @@ impl AlwaysFixableViolation for DashedUnderlineAfterSection { /// 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. +/// This rule enforces a consistent style for multiline numpy-style docstrings, +/// and helps prevent incorrect syntax in docstrings using reStructuredText. /// -/// Some docstring formats (like reStructuredText) use underlines to separate -/// section bodies from section headers. +/// Multiline numpy-style docstrings are typically composed of a summary line, +/// followed by a blank line, followed by a series of sections. Each section +/// has a header and a body. There should be a series of underline characters +/// in the line immediately below the header. /// -/// When present, section underlines should be positioned on the line -/// immediately following the section header. +/// This rule enforces a consistent style for multiline numpy-style docstrings +/// with sections. If your docstring uses reStructuredText, the rule also +/// helps protect against incorrect reStructuredText syntax, which would cause +/// errors if you tried to use a tool such as Sphinx to generate documentation +/// from the docstring. /// /// This rule is enabled when using the `numpy` convention, and disabled /// when using the `google` or `pep257` conventions. @@ -577,15 +599,20 @@ impl AlwaysFixableViolation for SectionUnderlineAfterName { /// 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. +/// This rule enforces a consistent style for multiline numpy-style docstrings, +/// and helps prevent incorrect syntax in docstrings using reStructuredText. /// -/// Some docstring formats (like reStructuredText) use underlines to separate -/// section bodies from section headers. +/// Multiline numpy-style docstrings are typically composed of a summary line, +/// followed by a blank line, followed by a series of sections. Each section +/// has a section header and a section body, and there should be a series of +/// underline characters in the line following the header. The length of the +/// underline should exactly match the length of the section header. /// -/// When present, section underlines should match the length of the -/// corresponding section header. +/// This rule enforces a consistent style for multiline numpy-style docstrings +/// with sections. If your docstring uses reStructuredText, the rule also +/// helps protect against incorrect reStructuredText syntax, which would cause +/// errors if you tried to use a tool such as Sphinx to generate documentation +/// from the docstring. /// /// This rule is enabled when using the `numpy` convention, and disabled /// when using the `google` or `pep257` conventions. @@ -676,13 +703,15 @@ impl AlwaysFixableViolation for SectionUnderlineMatchesSectionLength { /// 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 +/// This rule enforces consistency in your docstrings, and helps ensure /// compatibility with documentation tooling. /// +/// Multiline 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. If a multiline numpy-style or Google-style docstring +/// consists of multiple sections, each section should be separated by a single +/// blank line. +/// /// This rule is enabled when using the `numpy` and `google` conventions, and /// disabled when using the `pep257` convention. /// @@ -767,15 +796,15 @@ impl AlwaysFixableViolation for NoBlankLineAfterSection { } /// ## What it does -/// Checks for docstring sections that are separated by a blank line. +/// Checks for docstring sections that are not 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. +/// This rule enforces consistency in numpy-style and Google-style docstrings, +/// and helps ensure compatibility with documentation tooling. /// -/// Docstring sections should be separated by a blank line, for consistency and -/// compatibility with documentation tooling. +/// Multiline 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. Sections should be separated by a single blank line. /// /// This rule is enabled when using the `numpy` and `google` conventions, and /// disabled when using the `pep257` convention. @@ -860,19 +889,18 @@ impl AlwaysFixableViolation for NoBlankLineBeforeSection { } /// ## What it does -/// Checks for missing blank lines after the last section of a multi-line +/// Checks for missing blank lines after the last section of a multiline /// docstring. /// /// ## Why is this bad? -/// Multi-line docstrings are typically composed of a summary line, followed by +/// This rule enforces a consistent style for multiline docstrings. +/// +/// Multiline 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 some projects, the last section of a docstring is followed by a blank line, -/// for consistency and compatibility. -/// /// 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`, +/// convention. By default, the rule is disabled when using the `google`, /// `numpy`, and `pep257` conventions. /// /// ## Example @@ -956,15 +984,16 @@ impl AlwaysFixableViolation for BlankLineAfterLastSection { } /// ## What it does -/// Checks for docstrings that contain empty sections. +/// Checks for docstrings with 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. +/// An empty section in a multiline docstring likely indicates an unfinished +/// or incomplete docstring. /// -/// Empty docstring sections are indicative of missing documentation. Empty -/// sections should either be removed or filled in with relevant documentation. +/// Multiline 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 body should be non-empty; empty sections +/// should either have content added to them, or be removed entirely. /// /// ## Example /// ```python @@ -1045,13 +1074,14 @@ impl Violation for EmptyDocstringSection { /// 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 +/// This rule enforces a consistent style for multiline Google-style +/// docstrings. If a multiline Google-style docstring consists of multiple +/// sections, each section header should end with a colon. +/// +/// Multiline 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. /// @@ -1127,13 +1157,14 @@ impl AlwaysFixableViolation for SectionNameEndsInColon { /// 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. +/// This rule helps prevent you from leaving Google-style docstrings unfinished +/// or incomplete. Multiline Google-style docstrings should describe all +/// parameters for the function they are documenting. /// -/// Function docstrings often include a section for function arguments, which -/// should include documentation for every argument. Undocumented arguments are -/// indicative of missing documentation. +/// Multiline 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; this rule is concerned with that section only. /// /// This rule is enabled when using the `google` convention, and disabled when /// using the `pep257` and `numpy` conventions. @@ -1209,17 +1240,17 @@ impl Violation for UndocumentedParam { } /// ## What it does -/// Checks for docstring sections that contain blank lines between the section -/// header and the section body. +/// Checks for docstring sections that contain blank lines between a section +/// header and a section body. /// /// ## Why is this bad? -/// Multi-line docstrings are typically composed of a summary line, followed by +/// This rule enforces a consistent style for multiline docstrings. +/// +/// Multiline 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. There should be no blank lines between 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: @@ -1377,50 +1408,41 @@ fn blanks_and_section_underline( } if let Some(non_blank_line) = following_lines.next() { - let dash_line_found = is_dashed_underline(&non_blank_line); - - if dash_line_found { + if let Some(dashed_line) = find_underline(&non_blank_line, '-') { if blank_lines_after_header > 0 { if checker.enabled(Rule::SectionUnderlineAfterName) { let mut diagnostic = Diagnostic::new( SectionUnderlineAfterName { name: context.section_name().to_string(), }, - docstring.range(), + dashed_line, ); - let range = TextRange::new(context.following_range().start(), blank_lines_end); + // Delete any blank lines between the header and the underline. - diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(range))); + diagnostic.set_fix(Fix::safe_edit(Edit::deletion( + context.following_range().start(), + blank_lines_end, + ))); + checker.diagnostics.push(diagnostic); } } - if non_blank_line - .trim() - .chars() - .filter(|char| *char == '-') - .count() - != context.section_name().len() - { + if dashed_line.len().to_usize() != context.section_name().len() { if checker.enabled(Rule::SectionUnderlineMatchesSectionLength) { let mut diagnostic = Diagnostic::new( SectionUnderlineMatchesSectionLength { name: context.section_name().to_string(), }, - docstring.range(), + dashed_line, ); + // Replace the existing underline with a line of the appropriate length. - let content = format!( - "{}{}{}", - clean_space(docstring.indentation), + diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( "-".repeat(context.section_name().len()), - checker.stylist().line_ending().as_str() - ); - diagnostic.set_fix(Fix::safe_edit(Edit::replacement( - content, - blank_lines_end, - non_blank_line.full_end(), + dashed_line, ))); + checker.diagnostics.push(diagnostic); } } @@ -1432,8 +1454,9 @@ fn blanks_and_section_underline( SectionUnderlineNotOverIndented { name: context.section_name().to_string(), }, - docstring.range(), + dashed_line, ); + // Replace the existing indentation with whitespace of the appropriate length. let range = TextRange::at( blank_lines_end, @@ -1445,6 +1468,7 @@ fn blanks_and_section_underline( } else { Edit::range_replacement(contents, range) })); + checker.diagnostics.push(diagnostic); } } @@ -1467,7 +1491,7 @@ fn blanks_and_section_underline( EmptyDocstringSection { name: context.section_name().to_string(), }, - docstring.range(), + context.section_name_range(), )); } } else if checker.enabled(Rule::BlankLinesBetweenHeaderAndContent) { @@ -1475,7 +1499,7 @@ fn blanks_and_section_underline( BlankLinesBetweenHeaderAndContent { name: context.section_name().to_string(), }, - docstring.range(), + context.section_name_range(), ); // Delete any blank lines between the header and content. diagnostic.set_fix(Fix::safe_edit(Edit::deletion( @@ -1491,47 +1515,50 @@ fn blanks_and_section_underline( EmptyDocstringSection { name: context.section_name().to_string(), }, - docstring.range(), + context.section_name_range(), )); } } } 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 { - name: context.section_name().to_string(), - }, - docstring.range(), - ); - // Add a dashed line (of the appropriate length) under the section header. - let content = format!( - "{}{}{}", - checker.stylist().line_ending().as_str(), - clean_space(docstring.indentation), - "-".repeat(context.section_name().len()), - ); - if equal_line_found - && non_blank_line.trim_whitespace().len() == context.section_name().len() - { + if let Some(equal_line) = find_underline(&non_blank_line, '=') { + let mut diagnostic = Diagnostic::new( + DashedUnderlineAfterSection { + name: context.section_name().to_string(), + }, + equal_line, + ); + // If an existing underline is an equal sign line of the appropriate length, // replace it with a dashed line. - diagnostic.set_fix(Fix::safe_edit(Edit::replacement( - content, - context.summary_range().end(), - non_blank_line.end(), + diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( + "-".repeat(context.section_name().len()), + equal_line, ))); + + checker.diagnostics.push(diagnostic); } else { - // Otherwise, insert a dashed line after the section header. + let mut diagnostic = Diagnostic::new( + DashedUnderlineAfterSection { + name: context.section_name().to_string(), + }, + context.section_name_range(), + ); + + // Add a dashed line (of the appropriate length) under the section header. + let content = format!( + "{}{}{}", + checker.stylist().line_ending().as_str(), + clean_space(docstring.indentation), + "-".repeat(context.section_name().len()), + ); diagnostic.set_fix(Fix::safe_edit(Edit::insertion( content, context.summary_range().end(), ))); + + checker.diagnostics.push(diagnostic); } - checker.diagnostics.push(diagnostic); } if blank_lines_after_header > 0 { if checker.enabled(Rule::BlankLinesBetweenHeaderAndContent) { @@ -1539,7 +1566,7 @@ fn blanks_and_section_underline( BlankLinesBetweenHeaderAndContent { name: context.section_name().to_string(), }, - docstring.range(), + context.section_name_range(), ); let range = TextRange::new(context.following_range().start(), blank_lines_end); // Delete any blank lines between the header and content. @@ -1548,16 +1575,16 @@ fn blanks_and_section_underline( } } } - } - // Nothing but blank lines after the section header. - else { + } else { + // Nothing but blank lines after the section header. if checker.enabled(Rule::DashedUnderlineAfterSection) { let mut diagnostic = Diagnostic::new( DashedUnderlineAfterSection { name: context.section_name().to_string(), }, - docstring.range(), + context.section_name_range(), ); + // Add a dashed line (of the appropriate length) under the section header. let content = format!( "{}{}{}", @@ -1565,11 +1592,11 @@ fn blanks_and_section_underline( clean_space(docstring.indentation), "-".repeat(context.section_name().len()), ); - diagnostic.set_fix(Fix::safe_edit(Edit::insertion( content, context.summary_range().end(), ))); + checker.diagnostics.push(diagnostic); } if checker.enabled(Rule::EmptyDocstringSection) { @@ -1577,7 +1604,7 @@ fn blanks_and_section_underline( EmptyDocstringSection { name: context.section_name().to_string(), }, - docstring.range(), + context.section_name_range(), )); } } @@ -1592,15 +1619,15 @@ fn common_section( if checker.enabled(Rule::CapitalizeSectionName) { let capitalized_section_name = context.kind().as_str(); if context.section_name() != capitalized_section_name { + let section_range = context.section_name_range(); let mut diagnostic = Diagnostic::new( CapitalizeSectionName { name: context.section_name().to_string(), }, - docstring.range(), + section_range, ); // 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(); diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( capitalized_section_name.to_string(), section_range, @@ -1612,16 +1639,17 @@ fn common_section( if checker.enabled(Rule::SectionNotOverIndented) { let leading_space = leading_space(context.summary_line()); if leading_space.len() > docstring.indentation.len() { + let section_range = context.section_name_range(); let mut diagnostic = Diagnostic::new( SectionNotOverIndented { name: context.section_name().to_string(), }, - docstring.range(), + section_range, ); + // Replace the existing indentation with whitespace of the appropriate length. let content = clean_space(docstring.indentation); let fix_range = TextRange::at(context.start(), leading_space.text_len()); - diagnostic.set_fix(Fix::safe_edit(if content.is_empty() { Edit::range_deletion(fix_range) } else { @@ -1641,11 +1669,12 @@ fn common_section( .take_while(|line| line.trim().is_empty()) .count(); if num_blank_lines < 2 { + let section_range = context.section_name_range(); let mut diagnostic = Diagnostic::new( NoBlankLineAfterSection { name: context.section_name().to_string(), }, - docstring.range(), + section_range, ); // Add a newline at the beginning of the next section. diagnostic.set_fix(Fix::safe_edit(Edit::insertion( @@ -1682,11 +1711,12 @@ fn common_section( context.end(), ); + let section_range = context.section_name_range(); let mut diagnostic = Diagnostic::new( BlankLineAfterLastSection { name: context.section_name().to_string(), }, - docstring.range(), + section_range, ); diagnostic.set_fix(Fix::safe_edit(edit)); checker.diagnostics.push(diagnostic); @@ -1699,11 +1729,12 @@ fn common_section( .previous_line() .is_some_and(|line| line.trim().is_empty()) { + let section_range = context.section_name_range(); let mut diagnostic = Diagnostic::new( NoBlankLineBeforeSection { name: context.section_name().to_string(), }, - docstring.range(), + section_range, ); // Add a blank line before the section. diagnostic.set_fix(Fix::safe_edit(Edit::insertion( @@ -1800,10 +1831,11 @@ fn args_section(context: &SectionContext) -> FxHashSet { let leading_space = leading_space(first_line.as_str()); let relevant_lines = std::iter::once(first_line) .chain(following_lines) - .map(|l| l.as_str()) .filter(|line| { - line.is_empty() || (line.starts_with(leading_space) && !is_dashed_underline(line)) + line.is_empty() + || (line.starts_with(leading_space) && find_underline(line, '-').is_none()) }) + .map(|line| line.as_str()) .join("\n"); let args_content = dedent(&relevant_lines); @@ -1897,7 +1929,7 @@ fn numpy_section( NewLineAfterSectionName { name: context.section_name().to_string(), }, - docstring.range(), + context.section_name_range(), ); let section_range = context.section_name_range(); diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(TextRange::at( @@ -1931,7 +1963,7 @@ fn google_section( SectionNameEndsInColon { name: context.section_name().to_string(), }, - docstring.range(), + context.section_name_range(), ); // Replace the suffix. let section_name_range = context.section_name_range(); @@ -1991,7 +2023,35 @@ fn parse_google_sections( } } -fn is_dashed_underline(line: &str) -> bool { - let trimmed_line = line.trim(); - !trimmed_line.is_empty() && trimmed_line.chars().all(|char| char == '-') +/// Returns the [`TextRange`] of the underline, if a line consists of only dashes. +fn find_underline(line: &Line, dash: char) -> Option { + let mut cursor = Cursor::new(line.as_str()); + + // Eat leading whitespace. + cursor.eat_while(char::is_whitespace); + + // Determine the start of the dashes. + let offset = cursor.token_len(); + + // Consume the dashes. + cursor.start_token(); + cursor.eat_while(|c| c == dash); + + // Determine the end of the dashes. + let len = cursor.token_len(); + + // If there are no dashes, return None. + if len == TextSize::new(0) { + return None; + } + + // Eat trailing whitespace. + cursor.eat_while(char::is_whitespace); + + // If there are any characters after the dashes, return None. + if !cursor.is_eof() { + return None; + } + + Some(TextRange::at(offset, len).add(line.start())) } diff --git a/crates/ruff_linter/src/rules/pydocstyle/settings.rs b/crates/ruff_linter/src/rules/pydocstyle/settings.rs index 1b3a177af64eb..974c8742f9ec0 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/settings.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/settings.rs @@ -83,7 +83,7 @@ impl fmt::Display for Convention { } } -#[derive(Debug, Default, CacheKey)] +#[derive(Debug, Clone, Default, CacheKey)] pub struct Settings { pub convention: Option, pub ignore_decorators: BTreeSet, diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D214_D214_module.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D214_D214_module.py.snap index 13c7174f438ef..c95abffdad67b 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D214_D214_module.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D214_D214_module.py.snap @@ -1,23 +1,16 @@ --- source: crates/ruff_linter/src/rules/pydocstyle/mod.rs --- -D214_module.py:1:1: D214 [*] Section is over-indented ("Returns") - | - 1 | / """A module docstring with D214 violations - 2 | | - 3 | | Returns - 4 | | ----- - 5 | | valid returns - 6 | | - 7 | | Args - 8 | | ----- - 9 | | valid args -10 | | """ - | |___^ D214 -11 | -12 | import os - | - = help: Remove over-indentation from "Returns" +D214_module.py:3:5: D214 [*] Section is over-indented ("Returns") + | +1 | """A module docstring with D214 violations +2 | +3 | Returns + | ^^^^^^^ D214 +4 | ----- +5 | valid returns + | + = help: Remove over-indentation from "Returns" ℹ Safe fix 1 1 | """A module docstring with D214 violations @@ -28,23 +21,16 @@ D214_module.py:1:1: D214 [*] Section is over-indented ("Returns") 5 5 | valid returns 6 6 | -D214_module.py:1:1: D214 [*] Section is over-indented ("Args") - | - 1 | / """A module docstring with D214 violations - 2 | | - 3 | | Returns - 4 | | ----- - 5 | | valid returns - 6 | | - 7 | | Args - 8 | | ----- - 9 | | valid args -10 | | """ - | |___^ D214 -11 | -12 | import os - | - = help: Remove over-indentation from "Args" +D214_module.py:7:5: D214 [*] Section is over-indented ("Args") + | +5 | valid returns +6 | +7 | Args + | ^^^^ D214 +8 | ----- +9 | valid args + | + = help: Remove over-indentation from "Args" ℹ Safe fix 4 4 | ----- @@ -55,5 +41,3 @@ D214_module.py:1:1: D214 [*] Section is over-indented ("Args") 8 8 | ----- 9 9 | valid args 10 10 | """ - - diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D214_sections.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D214_sections.py.snap index d8ce888327b5c..89f47fa2c67b6 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D214_sections.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D214_sections.py.snap @@ -1,19 +1,14 @@ --- source: crates/ruff_linter/src/rules/pydocstyle/mod.rs --- -sections.py:144:5: D214 [*] Section is over-indented ("Returns") +sections.py:146:9: D214 [*] Section is over-indented ("Returns") | -142 | @expect("D214: Section is over-indented ('Returns')") -143 | def section_overindented(): # noqa: D416 -144 | """Toggle the gizmo. - | _____^ -145 | | -146 | | Returns -147 | | ------- -148 | | A value of some sort. -149 | | -150 | | """ - | |_______^ D214 +144 | """Toggle the gizmo. +145 | +146 | Returns + | ^^^^^^^ D214 +147 | ------- +148 | A value of some sort. | = help: Remove over-indentation from "Returns" @@ -27,18 +22,13 @@ sections.py:144:5: D214 [*] Section is over-indented ("Returns") 148 148 | A value of some sort. 149 149 | -sections.py:558:5: D214 [*] Section is over-indented ("Returns") +sections.py:563:9: D214 [*] Section is over-indented ("Returns") | -557 | def titlecase_sub_section_header(): -558 | """Below, `Returns:` should be considered a section header. - | _____^ -559 | | -560 | | Args: -561 | | Here's a note. -562 | | -563 | | Returns: -564 | | """ - | |_______^ D214 +561 | Here's a note. +562 | +563 | Returns: + | ^^^^^^^ D214 +564 | """ | = help: Remove over-indentation from "Returns" @@ -50,6 +40,4 @@ sections.py:558:5: D214 [*] Section is over-indented ("Returns") 563 |+ Returns: 564 564 | """ 565 565 | -566 566 | - - +566 566 | diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D215_D215.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D215_D215.py.snap index 813615fb5e180..b93074fda8c98 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D215_D215.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D215_D215.py.snap @@ -1,13 +1,13 @@ --- source: crates/ruff_linter/src/rules/pydocstyle/mod.rs --- -D215.py:1:1: D215 [*] Section underline is over-indented ("TODO") +D215.py:3:5: D215 [*] Section underline is over-indented ("TODO") | -1 | / """ -2 | | TODO: -3 | | - -4 | | """ - | |___^ D215 +1 | """ +2 | TODO: +3 | - + | ^ D215 +4 | """ | = help: Remove over-indentation from "TODO" underline @@ -17,5 +17,3 @@ D215.py:1:1: D215 [*] Section underline is over-indented ("TODO") 3 |- - 3 |+ 4 4 | """ - - diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D215_sections.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D215_sections.py.snap index 7ff2484755e39..afbd747fbd769 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D215_sections.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D215_sections.py.snap @@ -1,19 +1,12 @@ --- source: crates/ruff_linter/src/rules/pydocstyle/mod.rs --- -sections.py:156:5: D215 [*] Section underline is over-indented ("Returns") +sections.py:159:9: D215 [*] Section underline is over-indented ("Returns") | -154 | @expect("D215: Section underline is over-indented (in section 'Returns')") -155 | def section_underline_overindented(): # noqa: D416 -156 | """Toggle the gizmo. - | _____^ -157 | | -158 | | Returns -159 | | ------- -160 | | A value of some sort. -161 | | -162 | | """ - | |_______^ D215 +158 | Returns +159 | ------- + | ^^^^^^^ D215 +160 | A value of some sort. | = help: Remove over-indentation from "Returns" underline @@ -27,17 +20,12 @@ sections.py:156:5: D215 [*] Section underline is over-indented ("Returns") 161 161 | 162 162 | """ -sections.py:170:5: D215 [*] Section underline is over-indented ("Returns") +sections.py:173:9: D215 [*] Section underline is over-indented ("Returns") | -168 | @expect("D414: Section has no content ('Returns')") -169 | def section_underline_overindented_and_contentless(): # noqa: D416 -170 | """Toggle the gizmo. - | _____^ -171 | | -172 | | Returns -173 | | ------- -174 | | """ - | |_______^ D215 +172 | Returns +173 | ------- + | ^^^^^^^ D215 +174 | """ | = help: Remove over-indentation from "Returns" underline @@ -49,6 +37,4 @@ sections.py:170:5: D215 [*] Section underline is over-indented ("Returns") 173 |+ ------ 174 174 | """ 175 175 | -176 176 | - - +176 176 | diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D403_D403.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D403_D403.py.snap index 0ac0eed67f2ff..07a0589fb11fc 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D403_D403.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D403_D403.py.snap @@ -19,4 +19,37 @@ D403.py:2:5: D403 [*] First word of the first line should be capitalized: `this` 4 4 | def good_function(): 5 5 | """This docstring is capitalized.""" +D403.py:30:5: D403 [*] First word of the first line should be capitalized: `singleword` -> `Singleword` + | +29 | def single_word(): +30 | """singleword.""" + | ^^^^^^^^^^^^^^^^^ D403 +31 | +32 | def single_word_no_dot(): + | + = help: Capitalize `singleword` to `Singleword` +ℹ Safe fix +27 27 | """th•s is not capitalized.""" +28 28 | +29 29 | def single_word(): +30 |- """singleword.""" + 30 |+ """Singleword.""" +31 31 | +32 32 | def single_word_no_dot(): +33 33 | """singleword""" + +D403.py:33:5: D403 [*] First word of the first line should be capitalized: `singleword` -> `Singleword` + | +32 | def single_word_no_dot(): +33 | """singleword""" + | ^^^^^^^^^^^^^^^^ D403 + | + = help: Capitalize `singleword` to `Singleword` + +ℹ Safe fix +30 30 | """singleword.""" +31 31 | +32 32 | def single_word_no_dot(): +33 |- """singleword""" + 33 |+ """Singleword""" diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D405_sections.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D405_sections.py.snap index 3ad8fb7ea6b84..2aff3caaa0427 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D405_sections.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D405_sections.py.snap @@ -1,19 +1,14 @@ --- source: crates/ruff_linter/src/rules/pydocstyle/mod.rs --- -sections.py:17:5: D405 [*] Section name should be properly capitalized ("returns") +sections.py:19:5: D405 [*] Section name should be properly capitalized ("returns") | -15 | "('Returns', not 'returns')") -16 | def not_capitalized(): # noqa: D416 -17 | """Toggle the gizmo. - | _____^ -18 | | -19 | | returns -20 | | ------- -21 | | A value of some sort. -22 | | -23 | | """ - | |_______^ D405 +17 | """Toggle the gizmo. +18 | +19 | returns + | ^^^^^^^ D405 +20 | ------- +21 | A value of some sort. | = help: Capitalize "returns" @@ -27,27 +22,13 @@ sections.py:17:5: D405 [*] Section name should be properly capitalized ("returns 21 21 | A value of some sort. 22 22 | -sections.py:216:5: D405 [*] Section name should be properly capitalized ("Short summary") +sections.py:218:5: D405 [*] Section name should be properly capitalized ("Short summary") | -214 | @expect("D407: Missing dashed underline after section ('Raises')") -215 | def multiple_sections(): # noqa: D416 -216 | """Toggle the gizmo. - | _____^ -217 | | -218 | | Short summary -219 | | ------------- -220 | | -221 | | This is the function's description, which will also specify what it -222 | | returns. -223 | | -224 | | Returns -225 | | ------ -226 | | Many many wonderful things. -227 | | Raises: -228 | | My attention. -229 | | -230 | | """ - | |_______^ D405 +216 | """Toggle the gizmo. +217 | +218 | Short summary + | ^^^^^^^^^^^^^ D405 +219 | ------------- | = help: Capitalize "Short summary" @@ -60,5 +41,3 @@ sections.py:216:5: D405 [*] Section name should be properly capitalized ("Short 219 219 | ------------- 220 220 | 221 221 | This is the function's description, which will also specify what it - - diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D406_sections.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D406_sections.py.snap index 14530459e76bd..b64a36dc51e68 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D406_sections.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D406_sections.py.snap @@ -1,19 +1,14 @@ --- source: crates/ruff_linter/src/rules/pydocstyle/mod.rs --- -sections.py:30:5: D406 [*] Section name should end with a newline ("Returns") +sections.py:32:5: D406 [*] Section name should end with a newline ("Returns") | -28 | "('Returns', not 'Returns:')") -29 | def superfluous_suffix(): # noqa: D416 -30 | """Toggle the gizmo. - | _____^ -31 | | -32 | | Returns: -33 | | ------- -34 | | A value of some sort. -35 | | -36 | | """ - | |_______^ D406 +30 | """Toggle the gizmo. +31 | +32 | Returns: + | ^^^^^^^ D406 +33 | ------- +34 | A value of some sort. | = help: Add newline after "Returns" @@ -27,27 +22,13 @@ sections.py:30:5: D406 [*] Section name should end with a newline ("Returns") 34 34 | A value of some sort. 35 35 | -sections.py:216:5: D406 [*] Section name should end with a newline ("Raises") +sections.py:227:5: D406 [*] Section name should end with a newline ("Raises") | -214 | @expect("D407: Missing dashed underline after section ('Raises')") -215 | def multiple_sections(): # noqa: D416 -216 | """Toggle the gizmo. - | _____^ -217 | | -218 | | Short summary -219 | | ------------- -220 | | -221 | | This is the function's description, which will also specify what it -222 | | returns. -223 | | -224 | | Returns -225 | | ------ -226 | | Many many wonderful things. -227 | | Raises: -228 | | My attention. -229 | | -230 | | """ - | |_______^ D406 +225 | ------ +226 | Many many wonderful things. +227 | Raises: + | ^^^^^^ D406 +228 | My attention. | = help: Add newline after "Raises" @@ -61,20 +42,14 @@ sections.py:216:5: D406 [*] Section name should end with a newline ("Raises") 229 229 | 230 230 | """ -sections.py:588:5: D406 [*] Section name should end with a newline ("Parameters") +sections.py:590:5: D406 [*] Section name should end with a newline ("Parameters") | -587 | def test_lowercase_sub_section_header_should_be_valid(parameters: list[str], value: int): # noqa: D213 -588 | """Test that lower case subsection header is valid even if it has the same name as section kind. - | _____^ -589 | | -590 | | Parameters: -591 | | ---------- -592 | | parameters: -593 | | A list of string parameters -594 | | value: -595 | | Some value -596 | | """ - | |_______^ D406 +588 | """Test that lower case subsection header is valid even if it has the same name as section kind. +589 | +590 | Parameters: + | ^^^^^^^^^^ D406 +591 | ---------- +592 | parameters: | = help: Add newline after "Parameters" @@ -87,5 +62,3 @@ sections.py:588:5: D406 [*] Section name should end with a newline ("Parameters" 591 591 | ---------- 592 592 | parameters: 593 593 | A list of string parameters - - diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D407_sections.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D407_sections.py.snap index 95efe19803e69..61fe5b4c6da24 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D407_sections.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D407_sections.py.snap @@ -1,18 +1,13 @@ --- source: crates/ruff_linter/src/rules/pydocstyle/mod.rs --- -sections.py:42:5: D407 [*] Missing dashed underline after section ("Returns") +sections.py:44:5: D407 [*] Missing dashed underline after section ("Returns") | -40 | @expect("D407: Missing dashed underline after section ('Returns')") -41 | def no_underline(): # noqa: D416 -42 | """Toggle the gizmo. - | _____^ -43 | | -44 | | Returns -45 | | A value of some sort. -46 | | -47 | | """ - | |_______^ D407 +42 | """Toggle the gizmo. +43 | +44 | Returns + | ^^^^^^^ D407 +45 | A value of some sort. | = help: Add dashed line under "Returns" @@ -25,17 +20,14 @@ sections.py:42:5: D407 [*] Missing dashed underline after section ("Returns") 46 47 | 47 48 | """ -sections.py:54:5: D407 [*] Missing dashed underline after section ("Returns") +sections.py:56:5: D407 [*] Missing dashed underline after section ("Returns") | -52 | @expect("D414: Section has no content ('Returns')") -53 | def no_underline_and_no_description(): # noqa: D416 -54 | """Toggle the gizmo. - | _____^ -55 | | -56 | | Returns -57 | | -58 | | """ - | |_______^ D407 +54 | """Toggle the gizmo. +55 | +56 | Returns + | ^^^^^^^ D407 +57 | +58 | """ | = help: Add dashed line under "Returns" @@ -48,15 +40,12 @@ sections.py:54:5: D407 [*] Missing dashed underline after section ("Returns") 58 59 | """ 59 60 | -sections.py:65:5: D407 [*] Missing dashed underline after section ("Returns") +sections.py:67:5: D407 [*] Missing dashed underline after section ("Returns") | -63 | @expect("D414: Section has no content ('Returns')") -64 | def no_underline_and_no_newline(): # noqa: D416 -65 | """Toggle the gizmo. - | _____^ -66 | | -67 | | Returns""" - | |______________^ D407 +65 | """Toggle the gizmo. +66 | +67 | Returns""" + | ^^^^^^^ D407 | = help: Add dashed line under "Returns" @@ -71,27 +60,13 @@ sections.py:65:5: D407 [*] Missing dashed underline after section ("Returns") 69 70 | 70 71 | @expect(_D213) -sections.py:216:5: D407 [*] Missing dashed underline after section ("Raises") - | -214 | @expect("D407: Missing dashed underline after section ('Raises')") -215 | def multiple_sections(): # noqa: D416 -216 | """Toggle the gizmo. - | _____^ -217 | | -218 | | Short summary -219 | | ------------- -220 | | -221 | | This is the function's description, which will also specify what it -222 | | returns. -223 | | -224 | | Returns -225 | | ------ -226 | | Many many wonderful things. -227 | | Raises: -228 | | My attention. -229 | | -230 | | """ - | |_______^ D407 +sections.py:227:5: D407 [*] Missing dashed underline after section ("Raises") + | +225 | ------ +226 | Many many wonderful things. +227 | Raises: + | ^^^^^^ D407 +228 | My attention. | = help: Add dashed line under "Raises" @@ -104,23 +79,13 @@ sections.py:216:5: D407 [*] Missing dashed underline after section ("Raises") 229 230 | 230 231 | """ -sections.py:261:5: D407 [*] Missing dashed underline after section ("Args") - | -259 | @expect("D414: Section has no content ('Returns')") -260 | def valid_google_style_section(): # noqa: D406, D407 -261 | """Toggle the gizmo. - | _____^ -262 | | -263 | | Args: -264 | | note: A random string. -265 | | -266 | | Returns: -267 | | -268 | | Raises: -269 | | RandomError: A random error that occurs randomly. -270 | | -271 | | """ - | |_______^ D407 +sections.py:263:5: D407 [*] Missing dashed underline after section ("Args") + | +261 | """Toggle the gizmo. +262 | +263 | Args: + | ^^^^ D407 +264 | note: A random string. | = help: Add dashed line under "Args" @@ -133,23 +98,14 @@ sections.py:261:5: D407 [*] Missing dashed underline after section ("Args") 265 266 | 266 267 | Returns: -sections.py:261:5: D407 [*] Missing dashed underline after section ("Returns") - | -259 | @expect("D414: Section has no content ('Returns')") -260 | def valid_google_style_section(): # noqa: D406, D407 -261 | """Toggle the gizmo. - | _____^ -262 | | -263 | | Args: -264 | | note: A random string. -265 | | -266 | | Returns: -267 | | -268 | | Raises: -269 | | RandomError: A random error that occurs randomly. -270 | | -271 | | """ - | |_______^ D407 +sections.py:266:5: D407 [*] Missing dashed underline after section ("Returns") + | +264 | note: A random string. +265 | +266 | Returns: + | ^^^^^^^ D407 +267 | +268 | Raises: | = help: Add dashed line under "Returns" @@ -162,23 +118,13 @@ sections.py:261:5: D407 [*] Missing dashed underline after section ("Returns") 268 269 | Raises: 269 270 | RandomError: A random error that occurs randomly. -sections.py:261:5: D407 [*] Missing dashed underline after section ("Raises") - | -259 | @expect("D414: Section has no content ('Returns')") -260 | def valid_google_style_section(): # noqa: D406, D407 -261 | """Toggle the gizmo. - | _____^ -262 | | -263 | | Args: -264 | | note: A random string. -265 | | -266 | | Returns: -267 | | -268 | | Raises: -269 | | RandomError: A random error that occurs randomly. -270 | | -271 | | """ - | |_______^ D407 +sections.py:268:5: D407 [*] Missing dashed underline after section ("Raises") + | +266 | Returns: +267 | +268 | Raises: + | ^^^^^^ D407 +269 | RandomError: A random error that occurs randomly. | = help: Add dashed line under "Raises" @@ -191,18 +137,13 @@ sections.py:261:5: D407 [*] Missing dashed underline after section ("Raises") 270 271 | 271 272 | """ -sections.py:278:5: D407 [*] Missing dashed underline after section ("Args") +sections.py:280:5: D407 [*] Missing dashed underline after section ("Args") | -276 | "('Args:', not 'Args')") -277 | def missing_colon_google_style_section(): # noqa: D406, D407 -278 | """Toggle the gizmo. - | _____^ -279 | | -280 | | Args -281 | | note: A random string. -282 | | -283 | | """ - | |_______^ D407 +278 | """Toggle the gizmo. +279 | +280 | Args + | ^^^^ D407 +281 | note: A random string. | = help: Add dashed line under "Args" @@ -215,21 +156,14 @@ sections.py:278:5: D407 [*] Missing dashed underline after section ("Args") 282 283 | 283 284 | """ -sections.py:293:9: D407 [*] Missing dashed underline after section ("Args") - | -292 | def bar(y=2): # noqa: D207, D213, D406, D407 -293 | """Nested function test for docstrings. - | _________^ -294 | | -295 | | Will this work when referencing x? -296 | | -297 | | Args: -298 | | x: Test something -299 | | that is broken. -300 | | -301 | | """ - | |___________^ D407 -302 | print(x) +sections.py:297:9: D407 [*] Missing dashed underline after section ("Args") + | +295 | Will this work when referencing x? +296 | +297 | Args: + | ^^^^ D407 +298 | x: Test something +299 | that is broken. | = help: Add dashed line under "Args" @@ -242,18 +176,13 @@ sections.py:293:9: D407 [*] Missing dashed underline after section ("Args") 299 300 | that is broken. 300 301 | -sections.py:310:5: D407 [*] Missing dashed underline after section ("Args") +sections.py:312:5: D407 [*] Missing dashed underline after section ("Args") | -308 | "'test_missing_google_args' docstring)") -309 | def test_missing_google_args(x=1, y=2, _private=3): # noqa: D406, D407 -310 | """Toggle the gizmo. - | _____^ -311 | | -312 | | Args: -313 | | x (int): The greatest integer. -314 | | -315 | | """ - | |_______^ D407 +310 | """Toggle the gizmo. +311 | +312 | Args: + | ^^^^ D407 +313 | x (int): The greatest integer. | = help: Add dashed line under "Args" @@ -266,20 +195,14 @@ sections.py:310:5: D407 [*] Missing dashed underline after section ("Args") 314 315 | 315 316 | """ -sections.py:322:9: D407 [*] Missing dashed underline after section ("Args") - | -321 | def test_method(self, test, another_test, _): # noqa: D213, D407 -322 | """Test a valid args section. - | _________^ -323 | | -324 | | Args: -325 | | test: A parameter. -326 | | another_test: Another parameter. -327 | | -328 | | """ - | |___________^ D407 -329 | -330 | @expect("D417: Missing argument descriptions in the docstring " +sections.py:324:9: D407 [*] Missing dashed underline after section ("Args") + | +322 | """Test a valid args section. +323 | +324 | Args: + | ^^^^ D407 +325 | test: A parameter. +326 | another_test: Another parameter. | = help: Add dashed line under "Args" @@ -292,20 +215,13 @@ sections.py:322:9: D407 [*] Missing dashed underline after section ("Args") 326 327 | another_test: Another parameter. 327 328 | -sections.py:334:9: D407 [*] Missing dashed underline after section ("Args") - | -332 | "'test_missing_args' docstring)", arg_count=5) -333 | def test_missing_args(self, test, x, y, z=3, _private_arg=3): # noqa: D213, D407 -334 | """Test a valid args section. - | _________^ -335 | | -336 | | Args: -337 | | x: Another parameter. -338 | | -339 | | """ - | |___________^ D407 -340 | -341 | @classmethod +sections.py:336:9: D407 [*] Missing dashed underline after section ("Args") + | +334 | """Test a valid args section. +335 | +336 | Args: + | ^^^^ D407 +337 | x: Another parameter. | = help: Add dashed line under "Args" @@ -318,21 +234,14 @@ sections.py:334:9: D407 [*] Missing dashed underline after section ("Args") 338 339 | 339 340 | """ -sections.py:346:9: D407 [*] Missing dashed underline after section ("Args") - | -344 | "'test_missing_args_class_method' docstring)", arg_count=5) -345 | def test_missing_args_class_method(cls, test, x, y, _, z=3): # noqa: D213, D407 -346 | """Test a valid args section. - | _________^ -347 | | -348 | | Args: -349 | | x: Another parameter. The parameter below is missing description. -350 | | y: -351 | | -352 | | """ - | |___________^ D407 -353 | -354 | @staticmethod +sections.py:348:9: D407 [*] Missing dashed underline after section ("Args") + | +346 | """Test a valid args section. +347 | +348 | Args: + | ^^^^ D407 +349 | x: Another parameter. The parameter below is missing description. +350 | y: | = help: Add dashed line under "Args" @@ -345,20 +254,13 @@ sections.py:346:9: D407 [*] Missing dashed underline after section ("Args") 350 351 | y: 351 352 | -sections.py:359:9: D407 [*] Missing dashed underline after section ("Args") - | -357 | "'test_missing_args_static_method' docstring)", arg_count=4) -358 | def test_missing_args_static_method(a, x, y, _test, z=3): # noqa: D213, D407 -359 | """Test a valid args section. - | _________^ -360 | | -361 | | Args: -362 | | x: Another parameter. -363 | | -364 | | """ - | |___________^ D407 -365 | -366 | @staticmethod +sections.py:361:9: D407 [*] Missing dashed underline after section ("Args") + | +359 | """Test a valid args section. +360 | +361 | Args: + | ^^^^ D407 +362 | x: Another parameter. | = help: Add dashed line under "Args" @@ -371,20 +273,13 @@ sections.py:359:9: D407 [*] Missing dashed underline after section ("Args") 363 364 | 364 365 | """ -sections.py:371:9: D407 [*] Missing dashed underline after section ("Args") - | -369 | "'test_missing_docstring' docstring)", arg_count=2) -370 | def test_missing_docstring(a, b): # noqa: D213, D407 -371 | """Test a valid args section. - | _________^ -372 | | -373 | | Args: -374 | | a: -375 | | -376 | | """ - | |___________^ D407 -377 | -378 | @staticmethod +sections.py:373:9: D407 [*] Missing dashed underline after section ("Args") + | +371 | """Test a valid args section. +372 | +373 | Args: + | ^^^^ D407 +374 | a: | = help: Add dashed line under "Args" @@ -397,24 +292,14 @@ sections.py:371:9: D407 [*] Missing dashed underline after section ("Args") 375 376 | 376 377 | """ -sections.py:380:9: D407 [*] Missing dashed underline after section ("Args") - | -378 | @staticmethod -379 | def test_hanging_indent(skip, verbose): # noqa: D213, D407 -380 | """Do stuff. - | _________^ -381 | | -382 | | Args: -383 | | skip (:attr:`.Skip`): -384 | | Lorem ipsum dolor sit amet, consectetur adipiscing elit. -385 | | Etiam at tellus a tellus faucibus maximus. Curabitur tellus -386 | | mauris, semper id vehicula ac, feugiat ut tortor. -387 | | verbose (bool): -388 | | If True, print out as much information as possible. -389 | | If False, print out concise "one-liner" information. -390 | | -391 | | """ - | |___________^ D407 +sections.py:382:9: D407 [*] Missing dashed underline after section ("Args") + | +380 | """Do stuff. +381 | +382 | Args: + | ^^^^ D407 +383 | skip (:attr:`.Skip`): +384 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. | = help: Add dashed line under "Args" @@ -427,20 +312,13 @@ sections.py:380:9: D407 [*] Missing dashed underline after section ("Args") 384 385 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. 385 386 | Etiam at tellus a tellus faucibus maximus. Curabitur tellus -sections.py:499:9: D407 [*] Missing dashed underline after section ("Args") - | -497 | "'test_incorrect_indent' docstring)", arg_count=3) -498 | def test_incorrect_indent(self, x=1, y=2): # noqa: D207, D213, D407 -499 | """Reproducing issue #437. - | _________^ -500 | | -501 | | Testing this incorrectly indented docstring. -502 | | -503 | | Args: -504 | | x: Test argument. -505 | | -506 | | """ - | |___________^ D407 +sections.py:503:9: D407 [*] Missing dashed underline after section ("Args") + | +501 | Testing this incorrectly indented docstring. +502 | +503 | Args: + | ^^^^ D407 +504 | x: Test argument. | = help: Add dashed line under "Args" @@ -453,16 +331,12 @@ 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") +sections.py:522: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 +521 | Parameters +522 | ========== + | ^^^^^^^^^^ D407 +523 | """ | = help: Add dashed line under "Parameters" @@ -476,16 +350,12 @@ sections.py:519:5: D407 [*] Missing dashed underline after section ("Parameters" 524 524 | 525 525 | -sections.py:527:5: D407 [*] Missing dashed underline after section ("Parameters") +sections.py:530: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 +529 | Parameters +530 | =========== + | ^^^^^^^^^^^ D407 +531 | """ | = help: Add dashed line under "Parameters" @@ -493,23 +363,19 @@ sections.py:527:5: D407 [*] Missing dashed underline after section ("Parameters" 527 527 | """Here, the length of equals is not the same. 528 528 | 529 529 | Parameters +530 |- =========== 530 |+ ---------- -530 531 | =========== -531 532 | """ -532 533 | - -sections.py:548:5: D407 [*] Missing dashed underline after section ("Args") - | -547 | def lowercase_sub_section_header(): -548 | """Below, `returns:` should _not_ be considered a section header. - | _____^ -549 | | -550 | | Args: -551 | | Here's a note. -552 | | -553 | | returns: -554 | | """ - | |_______^ D407 +531 531 | """ +532 532 | +533 533 | + +sections.py:550:5: D407 [*] Missing dashed underline after section ("Args") + | +548 | """Below, `returns:` should _not_ be considered a section header. +549 | +550 | Args: + | ^^^^ D407 +551 | Here's a note. | = help: Add dashed line under "Args" @@ -522,18 +388,13 @@ sections.py:548:5: D407 [*] Missing dashed underline after section ("Args") 552 553 | 553 554 | returns: -sections.py:558:5: D407 [*] Missing dashed underline after section ("Args") +sections.py:560:5: D407 [*] Missing dashed underline after section ("Args") | -557 | def titlecase_sub_section_header(): -558 | """Below, `Returns:` should be considered a section header. - | _____^ -559 | | -560 | | Args: -561 | | Here's a note. -562 | | -563 | | Returns: -564 | | """ - | |_______^ D407 +558 | """Below, `Returns:` should be considered a section header. +559 | +560 | Args: + | ^^^^ D407 +561 | Here's a note. | = help: Add dashed line under "Args" @@ -546,18 +407,13 @@ sections.py:558:5: D407 [*] Missing dashed underline after section ("Args") 562 563 | 563 564 | Returns: -sections.py:558:5: D407 [*] Missing dashed underline after section ("Returns") +sections.py:563:9: D407 [*] Missing dashed underline after section ("Returns") | -557 | def titlecase_sub_section_header(): -558 | """Below, `Returns:` should be considered a section header. - | _____^ -559 | | -560 | | Args: -561 | | Here's a note. -562 | | -563 | | Returns: -564 | | """ - | |_______^ D407 +561 | Here's a note. +562 | +563 | Returns: + | ^^^^^^^ D407 +564 | """ | = help: Add dashed line under "Returns" @@ -570,19 +426,14 @@ sections.py:558:5: D407 [*] Missing dashed underline after section ("Returns") 565 566 | 566 567 | -sections.py:600:4: D407 [*] Missing dashed underline after section ("Parameters") +sections.py:602:4: D407 [*] Missing dashed underline after section ("Parameters") | -599 | def test_lowercase_sub_section_header_different_kind(returns: int): -600 | """Test that lower case subsection header is valid even if it is of a different kind. - | ____^ -601 | | -602 | | Parameters -603 | | -‐----------------- -604 | | returns: -605 | | some value -606 | | -607 | | """ - | |______^ D407 +600 | """Test that lower case subsection header is valid even if it is of a different kind. +601 | +602 | Parameters + | ^^^^^^^^^^ D407 +603 | -‐----------------- +604 | returns: | = help: Add dashed line under "Parameters" @@ -594,5 +445,3 @@ sections.py:600:4: D407 [*] Missing dashed underline after section ("Parameters" 603 604 | -‐----------------- 604 605 | returns: 605 606 | some value - - diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D408_sections.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D408_sections.py.snap index 8ee2ee32490da..38bec4612b427 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D408_sections.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D408_sections.py.snap @@ -1,22 +1,15 @@ --- source: crates/ruff_linter/src/rules/pydocstyle/mod.rs --- -sections.py:94:5: D408 [*] Section underline should be in the line following the section's name ("Returns") - | - 92 | "section's name ('Returns')") - 93 | def blank_line_before_underline(): # noqa: D416 - 94 | """Toggle the gizmo. - | _____^ - 95 | | - 96 | | Returns - 97 | | - 98 | | ------- - 99 | | A value of some sort. -100 | | -101 | | """ - | |_______^ D408 - | - = help: Add underline to "Returns" +sections.py:98:5: D408 [*] Section underline should be in the line following the section's name ("Returns") + | +96 | Returns +97 | +98 | ------- + | ^^^^^^^ D408 +99 | A value of some sort. + | + = help: Add underline to "Returns" ℹ Safe fix 94 94 | """Toggle the gizmo. @@ -25,6 +18,4 @@ sections.py:94:5: D408 [*] Section underline should be in the line following the 97 |- 98 97 | ------- 99 98 | A value of some sort. -100 99 | - - +100 99 | diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D409_sections.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D409_sections.py.snap index 881b5a0f0f099..1b59fad56deee 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D409_sections.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D409_sections.py.snap @@ -1,19 +1,12 @@ --- source: crates/ruff_linter/src/rules/pydocstyle/mod.rs --- -sections.py:108:5: D409 [*] Section underline should match the length of its name ("Returns") +sections.py:111:5: D409 [*] Section underline should match the length of its name ("Returns") | -106 | "(Expected 7 dashes in section 'Returns', got 2)") -107 | def bad_underline_length(): # noqa: D416 -108 | """Toggle the gizmo. - | _____^ -109 | | -110 | | Returns -111 | | -- -112 | | A value of some sort. -113 | | -114 | | """ - | |_______^ D409 +110 | Returns +111 | -- + | ^^ D409 +112 | A value of some sort. | = help: Adjust underline length to match "Returns" @@ -27,27 +20,13 @@ sections.py:108:5: D409 [*] Section underline should match the length of its nam 113 113 | 114 114 | """ -sections.py:216:5: D409 [*] Section underline should match the length of its name ("Returns") +sections.py:225:5: D409 [*] Section underline should match the length of its name ("Returns") | -214 | @expect("D407: Missing dashed underline after section ('Raises')") -215 | def multiple_sections(): # noqa: D416 -216 | """Toggle the gizmo. - | _____^ -217 | | -218 | | Short summary -219 | | ------------- -220 | | -221 | | This is the function's description, which will also specify what it -222 | | returns. -223 | | -224 | | Returns -225 | | ------ -226 | | Many many wonderful things. -227 | | Raises: -228 | | My attention. -229 | | -230 | | """ - | |_______^ D409 +224 | Returns +225 | ------ + | ^^^^^^ D409 +226 | Many many wonderful things. +227 | Raises: | = help: Adjust underline length to match "Returns" @@ -61,28 +40,13 @@ sections.py:216:5: D409 [*] Section underline should match the length of its nam 227 227 | Raises: 228 228 | My attention. -sections.py:568:5: D409 [*] Section underline should match the length of its name ("Other Parameters") +sections.py:578:5: D409 [*] Section underline should match the length of its name ("Other Parameters") | -567 | def test_method_should_be_correctly_capitalized(parameters: list[str], other_parameters: dict[str, str]): # noqa: D213 -568 | """Test parameters and attributes sections are capitalized correctly. - | _____^ -569 | | -570 | | Parameters -571 | | ---------- -572 | | parameters: -573 | | A list of string parameters -574 | | other_parameters: -575 | | A dictionary of string attributes -576 | | -577 | | Other Parameters -578 | | ---------- -579 | | other_parameters: -580 | | A dictionary of string attributes -581 | | parameters: -582 | | A list of string parameters -583 | | -584 | | """ - | |_______^ D409 +577 | Other Parameters +578 | ---------- + | ^^^^^^^^^^ D409 +579 | other_parameters: +580 | A dictionary of string attributes | = help: Adjust underline length to match "Other Parameters" @@ -95,5 +59,3 @@ sections.py:568:5: D409 [*] Section underline should match the length of its nam 579 579 | other_parameters: 580 580 | A dictionary of string attributes 581 581 | parameters: - - diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D410_D410.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D410_D410.py.snap index 7631ab868735c..2f7b0e4f92977 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D410_D410.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D410_D410.py.snap @@ -1,27 +1,16 @@ --- source: crates/ruff_linter/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" +D410.py:4:5: D410 [*] Missing blank line after section ("Parameters") + | +2 | """Showcase function. +3 | +4 | Parameters + | ^^^^^^^^^^ D410 +5 | ---------- +6 | a : int + | + = help: Add blank line after "Parameters" ℹ Safe fix 7 7 | _description_ @@ -32,18 +21,14 @@ D410.py:2:5: D410 [*] Missing blank line after section ("Parameters") 11 12 | ------- 12 13 | int -D410.py:19:5: D410 [*] Missing blank line after section ("Parameters") +D410.py:21:5: D410 [*] Missing blank line after section ("Parameters") | -18 | def f() -> int: -19 | """Showcase function. - | _____^ -20 | | -21 | | Parameters -22 | | ---------- -23 | | Returns -24 | | ------- -25 | | """ - | |_______^ D410 +19 | """Showcase function. +20 | +21 | Parameters + | ^^^^^^^^^^ D410 +22 | ---------- +23 | Returns | = help: Add blank line after "Parameters" @@ -55,5 +40,3 @@ D410.py:19:5: D410 [*] Missing blank line after section ("Parameters") 23 24 | Returns 24 25 | ------- 25 26 | """ - - diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D410_sections.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D410_sections.py.snap index 00d15acee3dfc..e8c466e1387ca 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D410_sections.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D410_sections.py.snap @@ -1,24 +1,14 @@ --- source: crates/ruff_linter/src/rules/pydocstyle/mod.rs --- -sections.py:76:5: D410 [*] Missing blank line after section ("Returns") +sections.py:78:5: D410 [*] Missing blank line after section ("Returns") | -74 | @expect("D414: Section has no content ('Yields')") -75 | def consecutive_sections(): # noqa: D416 -76 | """Toggle the gizmo. - | _____^ -77 | | -78 | | Returns -79 | | ------- -80 | | Yields -81 | | ------ -82 | | -83 | | Raises -84 | | ------ -85 | | Questions. -86 | | -87 | | """ - | |_______^ D410 +76 | """Toggle the gizmo. +77 | +78 | Returns + | ^^^^^^^ D410 +79 | ------- +80 | Yields | = help: Add blank line after "Returns" @@ -31,27 +21,14 @@ sections.py:76:5: D410 [*] Missing blank line after section ("Returns") 81 82 | ------ 82 83 | -sections.py:216:5: D410 [*] Missing blank line after section ("Returns") +sections.py:224:5: D410 [*] Missing blank line after section ("Returns") | -214 | @expect("D407: Missing dashed underline after section ('Raises')") -215 | def multiple_sections(): # noqa: D416 -216 | """Toggle the gizmo. - | _____^ -217 | | -218 | | Short summary -219 | | ------------- -220 | | -221 | | This is the function's description, which will also specify what it -222 | | returns. -223 | | -224 | | Returns -225 | | ------ -226 | | Many many wonderful things. -227 | | Raises: -228 | | My attention. -229 | | -230 | | """ - | |_______^ D410 +222 | returns. +223 | +224 | Returns + | ^^^^^^^ D410 +225 | ------ +226 | Many many wonderful things. | = help: Add blank line after "Returns" @@ -62,6 +39,4 @@ sections.py:216:5: D410 [*] Missing blank line after section ("Returns") 227 |+ 227 228 | Raises: 228 229 | My attention. -229 230 | - - +229 230 | diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D411_sections.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D411_sections.py.snap index f93054611928b..b516028f32fe3 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D411_sections.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D411_sections.py.snap @@ -1,24 +1,13 @@ --- source: crates/ruff_linter/src/rules/pydocstyle/mod.rs --- -sections.py:76:5: D411 [*] Missing blank line before section ("Yields") +sections.py:80:5: D411 [*] Missing blank line before section ("Yields") | -74 | @expect("D414: Section has no content ('Yields')") -75 | def consecutive_sections(): # noqa: D416 -76 | """Toggle the gizmo. - | _____^ -77 | | -78 | | Returns -79 | | ------- -80 | | Yields -81 | | ------ -82 | | -83 | | Raises -84 | | ------ -85 | | Questions. -86 | | -87 | | """ - | |_______^ D411 +78 | Returns +79 | ------- +80 | Yields + | ^^^^^^ D411 +81 | ------ | = help: Add blank line before "Yields" @@ -31,20 +20,13 @@ sections.py:76:5: D411 [*] Missing blank line before section ("Yields") 81 82 | ------ 82 83 | -sections.py:131:5: D411 [*] Missing blank line before section ("Returns") +sections.py:134:5: D411 [*] Missing blank line before section ("Returns") | -129 | @expect("D411: Missing blank line before section ('Returns')") -130 | def no_blank_line_before_section(): # noqa: D416 -131 | """Toggle the gizmo. - | _____^ -132 | | -133 | | The function's description. -134 | | Returns -135 | | ------- -136 | | A value of some sort. -137 | | -138 | | """ - | |_______^ D411 +133 | The function's description. +134 | Returns + | ^^^^^^^ D411 +135 | ------- +136 | A value of some sort. | = help: Add blank line before "Returns" @@ -57,27 +39,13 @@ sections.py:131:5: D411 [*] Missing blank line before section ("Returns") 135 136 | ------- 136 137 | A value of some sort. -sections.py:216:5: D411 [*] Missing blank line before section ("Raises") +sections.py:227:5: D411 [*] Missing blank line before section ("Raises") | -214 | @expect("D407: Missing dashed underline after section ('Raises')") -215 | def multiple_sections(): # noqa: D416 -216 | """Toggle the gizmo. - | _____^ -217 | | -218 | | Short summary -219 | | ------------- -220 | | -221 | | This is the function's description, which will also specify what it -222 | | returns. -223 | | -224 | | Returns -225 | | ------ -226 | | Many many wonderful things. -227 | | Raises: -228 | | My attention. -229 | | -230 | | """ - | |_______^ D411 +225 | ------ +226 | Many many wonderful things. +227 | Raises: + | ^^^^^^ D411 +228 | My attention. | = help: Add blank line before "Raises" @@ -88,6 +56,4 @@ sections.py:216:5: D411 [*] Missing blank line before section ("Raises") 227 |+ 227 228 | Raises: 228 229 | My attention. -229 230 | - - +229 230 | diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D412_sections.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D412_sections.py.snap index a3861a422ee2a..a6461d2987661 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D412_sections.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D412_sections.py.snap @@ -1,27 +1,13 @@ --- source: crates/ruff_linter/src/rules/pydocstyle/mod.rs --- -sections.py:216:5: D412 [*] No blank lines allowed between a section header and its content ("Short summary") +sections.py:218:5: D412 [*] No blank lines allowed between a section header and its content ("Short summary") | -214 | @expect("D407: Missing dashed underline after section ('Raises')") -215 | def multiple_sections(): # noqa: D416 -216 | """Toggle the gizmo. - | _____^ -217 | | -218 | | Short summary -219 | | ------------- -220 | | -221 | | This is the function's description, which will also specify what it -222 | | returns. -223 | | -224 | | Returns -225 | | ------ -226 | | Many many wonderful things. -227 | | Raises: -228 | | My attention. -229 | | -230 | | """ - | |_______^ D412 +216 | """Toggle the gizmo. +217 | +218 | Short summary + | ^^^^^^^^^^^^^ D412 +219 | ------------- | = help: Remove blank line(s) @@ -32,6 +18,4 @@ sections.py:216:5: D412 [*] No blank lines allowed between a section header and 220 |- 221 220 | This is the function's description, which will also specify what it 222 221 | returns. -223 222 | - - +223 222 | diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_D413.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_D413.py.snap index c04db39b79078..a2a831d93e752 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_D413.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_D413.py.snap @@ -1,18 +1,14 @@ --- source: crates/ruff_linter/src/rules/pydocstyle/mod.rs --- -D413.py:1:1: D413 [*] Missing blank line after last section ("Returns") +D413.py:7:1: D413 [*] Missing blank line after last section ("Returns") | -1 | / """Do something. -2 | | -3 | | Args: -4 | | x: the value -5 | | with a hanging indent -6 | | -7 | | Returns: -8 | | the value -9 | | """ - | |___^ D413 +5 | with a hanging indent +6 | +7 | Returns: + | ^^^^^^^ D413 +8 | the value +9 | """ | = help: Add blank line after "Returns" @@ -25,20 +21,14 @@ D413.py:1:1: D413 [*] Missing blank line after last section ("Returns") 10 11 | 11 12 | -D413.py:13:5: D413 [*] Missing blank line after last section ("Returns") +D413.py:19:5: D413 [*] Missing blank line after last section ("Returns") | -12 | def func(): -13 | """Do something. - | _____^ -14 | | -15 | | Args: -16 | | x: the value -17 | | with a hanging indent -18 | | -19 | | Returns: -20 | | the value -21 | | """ - | |_______^ D413 +17 | with a hanging indent +18 | +19 | Returns: + | ^^^^^^^ D413 +20 | the value +21 | """ | = help: Add blank line after "Returns" @@ -51,19 +41,13 @@ D413.py:13:5: D413 [*] Missing blank line after last section ("Returns") 22 23 | 23 24 | -D413.py:52:5: D413 [*] Missing blank line after last section ("Returns") +D413.py:58:5: D413 [*] Missing blank line after last section ("Returns") | -51 | def func(): -52 | """Do something. - | _____^ -53 | | -54 | | Args: -55 | | x: the value -56 | | with a hanging indent -57 | | -58 | | Returns: -59 | | the value""" - | |____________________^ D413 +56 | with a hanging indent +57 | +58 | Returns: + | ^^^^^^^ D413 +59 | the value""" | = help: Add blank line after "Returns" @@ -79,20 +63,14 @@ D413.py:52:5: D413 [*] Missing blank line after last section ("Returns") 61 63 | 62 64 | def func(): -D413.py:63:5: D413 [*] Missing blank line after last section ("Returns") +D413.py:69:5: D413 [*] Missing blank line after last section ("Returns") | -62 | def func(): -63 | """Do something. - | _____^ -64 | | -65 | | Args: -66 | | x: the value -67 | | with a hanging indent -68 | | -69 | | Returns: -70 | | the value -71 | | """ - | |___________^ D413 +67 | with a hanging indent +68 | +69 | Returns: + | ^^^^^^^ D413 +70 | the value +71 | """ | = help: Add blank line after "Returns" diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_sections.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_sections.py.snap index 3618cdd6ca061..223c7c38218af 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_sections.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_sections.py.snap @@ -1,15 +1,12 @@ --- source: crates/ruff_linter/src/rules/pydocstyle/mod.rs --- -sections.py:65:5: D413 [*] Missing blank line after last section ("Returns") +sections.py:67:5: D413 [*] Missing blank line after last section ("Returns") | -63 | @expect("D414: Section has no content ('Returns')") -64 | def no_underline_and_no_newline(): # noqa: D416 -65 | """Toggle the gizmo. - | _____^ -66 | | -67 | | Returns""" - | |______________^ D413 +65 | """Toggle the gizmo. +66 | +67 | Returns""" + | ^^^^^^^ D413 | = help: Add blank line after "Returns" @@ -25,18 +22,14 @@ sections.py:65:5: D413 [*] Missing blank line after last section ("Returns") 69 71 | 70 72 | @expect(_D213) -sections.py:120:5: D413 [*] Missing blank line after last section ("Returns") +sections.py:122:5: D413 [*] Missing blank line after last section ("Returns") | -118 | @expect("D413: Missing blank line after last section ('Returns')") -119 | def no_blank_line_after_last_section(): # noqa: D416 -120 | """Toggle the gizmo. - | _____^ -121 | | -122 | | Returns -123 | | ------- -124 | | A value of some sort. -125 | | """ - | |_______^ D413 +120 | """Toggle the gizmo. +121 | +122 | Returns + | ^^^^^^^ D413 +123 | ------- +124 | A value of some sort. | = help: Add blank line after "Returns" @@ -49,17 +42,14 @@ sections.py:120:5: D413 [*] Missing blank line after last section ("Returns") 126 127 | 127 128 | -sections.py:170:5: D413 [*] Missing blank line after last section ("Returns") +sections.py:172:5: D413 [*] Missing blank line after last section ("Returns") | -168 | @expect("D414: Section has no content ('Returns')") -169 | def section_underline_overindented_and_contentless(): # noqa: D416 -170 | """Toggle the gizmo. - | _____^ -171 | | -172 | | Returns -173 | | ------- -174 | | """ - | |_______^ D413 +170 | """Toggle the gizmo. +171 | +172 | Returns + | ^^^^^^^ D413 +173 | ------- +174 | """ | = help: Add blank line after "Returns" @@ -72,16 +62,14 @@ sections.py:170:5: D413 [*] Missing blank line after last section ("Returns") 175 176 | 176 177 | -sections.py:519:5: D413 [*] Missing blank line after last section ("Parameters") +sections.py:521:5: D413 [*] Missing blank line after last section ("Parameters") | -518 | def replace_equals_with_dash(): -519 | """Equal length equals should be replaced with dashes. - | _____^ -520 | | -521 | | Parameters -522 | | ========== -523 | | """ - | |_______^ D413 +519 | """Equal length equals should be replaced with dashes. +520 | +521 | Parameters + | ^^^^^^^^^^ D413 +522 | ========== +523 | """ | = help: Add blank line after "Parameters" @@ -94,16 +82,14 @@ sections.py:519:5: D413 [*] Missing blank line after last section ("Parameters") 524 525 | 525 526 | -sections.py:527:5: D413 [*] Missing blank line after last section ("Parameters") +sections.py:529:5: D413 [*] Missing blank line after last section ("Parameters") | -526 | def replace_equals_with_dash2(): -527 | """Here, the length of equals is not the same. - | _____^ -528 | | -529 | | Parameters -530 | | =========== -531 | | """ - | |_______^ D413 +527 | """Here, the length of equals is not the same. +528 | +529 | Parameters + | ^^^^^^^^^^ D413 +530 | =========== +531 | """ | = help: Add blank line after "Parameters" @@ -116,18 +102,13 @@ sections.py:527:5: D413 [*] Missing blank line after last section ("Parameters") 532 533 | 533 534 | -sections.py:548:5: D413 [*] Missing blank line after last section ("Args") +sections.py:550:5: D413 [*] Missing blank line after last section ("Args") | -547 | def lowercase_sub_section_header(): -548 | """Below, `returns:` should _not_ be considered a section header. - | _____^ -549 | | -550 | | Args: -551 | | Here's a note. -552 | | -553 | | returns: -554 | | """ - | |_______^ D413 +548 | """Below, `returns:` should _not_ be considered a section header. +549 | +550 | Args: + | ^^^^ D413 +551 | Here's a note. | = help: Add blank line after "Args" @@ -140,18 +121,13 @@ sections.py:548:5: D413 [*] Missing blank line after last section ("Args") 555 556 | 556 557 | -sections.py:558:5: D413 [*] Missing blank line after last section ("Returns") +sections.py:563:9: D413 [*] Missing blank line after last section ("Returns") | -557 | def titlecase_sub_section_header(): -558 | """Below, `Returns:` should be considered a section header. - | _____^ -559 | | -560 | | Args: -561 | | Here's a note. -562 | | -563 | | Returns: -564 | | """ - | |_______^ D413 +561 | Here's a note. +562 | +563 | Returns: + | ^^^^^^^ D413 +564 | """ | = help: Add blank line after "Returns" @@ -164,20 +140,14 @@ sections.py:558:5: D413 [*] Missing blank line after last section ("Returns") 565 566 | 566 567 | -sections.py:588:5: D413 [*] Missing blank line after last section ("Parameters") +sections.py:590:5: D413 [*] Missing blank line after last section ("Parameters") | -587 | def test_lowercase_sub_section_header_should_be_valid(parameters: list[str], value: int): # noqa: D213 -588 | """Test that lower case subsection header is valid even if it has the same name as section kind. - | _____^ -589 | | -590 | | Parameters: -591 | | ---------- -592 | | parameters: -593 | | A list of string parameters -594 | | value: -595 | | Some value -596 | | """ - | |_______^ D413 +588 | """Test that lower case subsection header is valid even if it has the same name as section kind. +589 | +590 | Parameters: + | ^^^^^^^^^^ D413 +591 | ---------- +592 | parameters: | = help: Add blank line after "Parameters" diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D414_sections.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D414_sections.py.snap index 9566fd691a6d4..b4a8317bd9295 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D414_sections.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D414_sections.py.snap @@ -1,114 +1,68 @@ --- source: crates/ruff_linter/src/rules/pydocstyle/mod.rs --- -sections.py:54:5: D414 Section has no content ("Returns") +sections.py:56:5: D414 Section has no content ("Returns") | -52 | @expect("D414: Section has no content ('Returns')") -53 | def no_underline_and_no_description(): # noqa: D416 -54 | """Toggle the gizmo. - | _____^ -55 | | -56 | | Returns -57 | | -58 | | """ - | |_______^ D414 +54 | """Toggle the gizmo. +55 | +56 | Returns + | ^^^^^^^ D414 +57 | +58 | """ | -sections.py:65:5: D414 Section has no content ("Returns") +sections.py:67:5: D414 Section has no content ("Returns") | -63 | @expect("D414: Section has no content ('Returns')") -64 | def no_underline_and_no_newline(): # noqa: D416 -65 | """Toggle the gizmo. - | _____^ -66 | | -67 | | Returns""" - | |______________^ D414 +65 | """Toggle the gizmo. +66 | +67 | Returns""" + | ^^^^^^^ D414 | -sections.py:76:5: D414 Section has no content ("Returns") +sections.py:78:5: D414 Section has no content ("Returns") | -74 | @expect("D414: Section has no content ('Yields')") -75 | def consecutive_sections(): # noqa: D416 -76 | """Toggle the gizmo. - | _____^ -77 | | -78 | | Returns -79 | | ------- -80 | | Yields -81 | | ------ -82 | | -83 | | Raises -84 | | ------ -85 | | Questions. -86 | | -87 | | """ - | |_______^ D414 +76 | """Toggle the gizmo. +77 | +78 | Returns + | ^^^^^^^ D414 +79 | ------- +80 | Yields | -sections.py:76:5: D414 Section has no content ("Yields") +sections.py:80:5: D414 Section has no content ("Yields") | -74 | @expect("D414: Section has no content ('Yields')") -75 | def consecutive_sections(): # noqa: D416 -76 | """Toggle the gizmo. - | _____^ -77 | | -78 | | Returns -79 | | ------- -80 | | Yields -81 | | ------ -82 | | -83 | | Raises -84 | | ------ -85 | | Questions. -86 | | -87 | | """ - | |_______^ D414 +78 | Returns +79 | ------- +80 | Yields + | ^^^^^^ D414 +81 | ------ | -sections.py:170:5: D414 Section has no content ("Returns") +sections.py:172:5: D414 Section has no content ("Returns") | -168 | @expect("D414: Section has no content ('Returns')") -169 | def section_underline_overindented_and_contentless(): # noqa: D416 -170 | """Toggle the gizmo. - | _____^ -171 | | -172 | | Returns -173 | | ------- -174 | | """ - | |_______^ D414 +170 | """Toggle the gizmo. +171 | +172 | Returns + | ^^^^^^^ D414 +173 | ------- +174 | """ | -sections.py:261:5: D414 Section has no content ("Returns") +sections.py:266:5: D414 Section has no content ("Returns") | -259 | @expect("D414: Section has no content ('Returns')") -260 | def valid_google_style_section(): # noqa: D406, D407 -261 | """Toggle the gizmo. - | _____^ -262 | | -263 | | Args: -264 | | note: A random string. -265 | | -266 | | Returns: -267 | | -268 | | Raises: -269 | | RandomError: A random error that occurs randomly. -270 | | -271 | | """ - | |_______^ D414 +264 | note: A random string. +265 | +266 | Returns: + | ^^^^^^^ D414 +267 | +268 | Raises: | -sections.py:558:5: D414 Section has no content ("Returns") +sections.py:563:9: D414 Section has no content ("Returns") | -557 | def titlecase_sub_section_header(): -558 | """Below, `Returns:` should be considered a section header. - | _____^ -559 | | -560 | | Args: -561 | | Here's a note. -562 | | -563 | | Returns: -564 | | """ - | |_______^ D414 +561 | Here's a note. +562 | +563 | Returns: + | ^^^^^^^ D414 +564 | """ | - - diff --git a/crates/ruff_linter/src/rules/pyflakes/mod.rs b/crates/ruff_linter/src/rules/pyflakes/mod.rs index 503690624cac2..6f5673b1ecc39 100644 --- a/crates/ruff_linter/src/rules/pyflakes/mod.rs +++ b/crates/ruff_linter/src/rules/pyflakes/mod.rs @@ -162,6 +162,7 @@ mod tests { #[test_case(Rule::UndefinedExport, Path::new("F822_0.pyi"))] #[test_case(Rule::UndefinedExport, Path::new("F822_1.py"))] #[test_case(Rule::UndefinedExport, Path::new("F822_2.py"))] + #[test_case(Rule::UndefinedExport, Path::new("F822_3.py"))] #[test_case(Rule::UndefinedLocal, Path::new("F823.py"))] #[test_case(Rule::UnusedVariable, Path::new("F841_0.py"))] #[test_case(Rule::UnusedVariable, Path::new("F841_1.py"))] diff --git a/crates/ruff_linter/src/rules/pyflakes/settings.rs b/crates/ruff_linter/src/rules/pyflakes/settings.rs index b87c9aebf2108..2aa404fbe4688 100644 --- a/crates/ruff_linter/src/rules/pyflakes/settings.rs +++ b/crates/ruff_linter/src/rules/pyflakes/settings.rs @@ -4,7 +4,7 @@ use crate::display_settings; use ruff_macros::CacheKey; use std::fmt; -#[derive(Debug, Default, CacheKey)] +#[derive(Debug, Clone, Default, CacheKey)] pub struct Settings { pub extend_generics: Vec, } diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F822_F822_3.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F822_F822_3.py.snap new file mode 100644 index 0000000000000..5d572ea096297 --- /dev/null +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F822_F822_3.py.snap @@ -0,0 +1,11 @@ +--- +source: crates/ruff_linter/src/rules/pyflakes/mod.rs +--- +F822_3.py:12:5: F822 Undefined name `ExponentialFamily` in `__all__` + | +10 | __all__ += [ +11 | "ContinuousBernoulli", # noqa: F822 +12 | "ExponentialFamily", + | ^^^^^^^^^^^^^^^^^^^ F822 +13 | ] + | diff --git a/crates/ruff_linter/src/rules/pylint/mod.rs b/crates/ruff_linter/src/rules/pylint/mod.rs index 86d3d2e897e37..867e6dfc9596b 100644 --- a/crates/ruff_linter/src/rules/pylint/mod.rs +++ b/crates/ruff_linter/src/rules/pylint/mod.rs @@ -190,6 +190,10 @@ mod tests { Path::new("useless_exception_statement.py") )] #[test_case(Rule::NanComparison, Path::new("nan_comparison.py"))] + #[test_case( + Rule::BadStaticmethodArgument, + Path::new("bad_staticmethod_argument.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_linter/src/rules/pylint/rules/bad_staticmethod_argument.rs b/crates/ruff_linter/src/rules/pylint/rules/bad_staticmethod_argument.rs new file mode 100644 index 0000000000000..c99cd52c8cc1b --- /dev/null +++ b/crates/ruff_linter/src/rules/pylint/rules/bad_staticmethod_argument.rs @@ -0,0 +1,103 @@ +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast as ast; +use ruff_python_ast::ParameterWithDefault; +use ruff_python_semantic::analyze::function_type; +use ruff_python_semantic::Scope; +use ruff_text_size::Ranged; + +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for static methods that use `self` or `cls` as their first argument. +/// +/// ## Why is this bad? +/// [PEP 8] recommends the use of `self` and `cls` as the first arguments for +/// instance methods and class methods, respectively. Naming the first argument +/// of a static method as `self` or `cls` can be misleading, as static methods +/// do not receive an instance or class reference as their first argument. +/// +/// ## Example +/// ```python +/// class Wolf: +/// @staticmethod +/// def eat(self): +/// pass +/// ``` +/// +/// Use instead: +/// ```python +/// class Wolf: +/// @staticmethod +/// def eat(sheep): +/// pass +/// ``` +/// +/// [PEP 8]: https://peps.python.org/pep-0008/#function-and-method-arguments +#[violation] +pub struct BadStaticmethodArgument { + argument_name: String, +} + +impl Violation for BadStaticmethodArgument { + #[derive_message_formats] + fn message(&self) -> String { + let Self { argument_name } = self; + format!("First argument of a static method should not be named `{argument_name}`") + } +} + +/// PLW0211 +pub(crate) fn bad_staticmethod_argument( + checker: &Checker, + scope: &Scope, + diagnostics: &mut Vec, +) { + let Some(func) = scope.kind.as_function() else { + return; + }; + + let ast::StmtFunctionDef { + name, + decorator_list, + parameters, + .. + } = func; + + let Some(parent) = &checker.semantic().first_non_type_parent_scope(scope) else { + return; + }; + + let type_ = function_type::classify( + name, + decorator_list, + parent, + checker.semantic(), + &checker.settings.pep8_naming.classmethod_decorators, + &checker.settings.pep8_naming.staticmethod_decorators, + ); + if !matches!(type_, function_type::FunctionType::StaticMethod) { + return; + } + + let Some(ParameterWithDefault { + parameter: self_or_cls, + .. + }) = parameters + .posonlyargs + .first() + .or_else(|| parameters.args.first()) + else { + return; + }; + + if matches!(self_or_cls.name.as_str(), "self" | "cls") { + let diagnostic = Diagnostic::new( + BadStaticmethodArgument { + argument_name: self_or_cls.name.to_string(), + }, + self_or_cls.range(), + ); + diagnostics.push(diagnostic); + } +} diff --git a/crates/ruff_linter/src/rules/pylint/rules/mod.rs b/crates/ruff_linter/src/rules/pylint/rules/mod.rs index 99bbb5063efdb..93f89e1f6cec0 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/mod.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/mod.rs @@ -3,6 +3,7 @@ pub(crate) use assert_on_string_literal::*; pub(crate) use await_outside_async::*; pub(crate) use bad_dunder_method_name::*; pub(crate) use bad_open_mode::*; +pub(crate) use bad_staticmethod_argument::*; pub(crate) use bad_str_strip_call::*; pub(crate) use bad_string_format_character::BadStringFormatCharacter; pub(crate) use bad_string_format_type::*; @@ -97,6 +98,7 @@ mod assert_on_string_literal; mod await_outside_async; mod bad_dunder_method_name; mod bad_open_mode; +mod bad_staticmethod_argument; mod bad_str_strip_call; pub(crate) mod bad_string_format_character; mod bad_string_format_type; diff --git a/crates/ruff_linter/src/rules/pylint/rules/singledispatch_method.rs b/crates/ruff_linter/src/rules/pylint/rules/singledispatch_method.rs index ec732781e3b85..17c24e1a1962f 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/singledispatch_method.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/singledispatch_method.rs @@ -9,13 +9,13 @@ use crate::checkers::ast::Checker; use crate::importer::ImportRequest; /// ## What it does -/// Checks for `@singledispatch` decorators on class and instance methods. +/// Checks for methods decorated with `@singledispatch`. /// /// ## Why is this bad? /// The `@singledispatch` decorator is intended for use with functions, not methods. /// /// Instead, use the `@singledispatchmethod` decorator, or migrate the method to a -/// standalone function or `@staticmethod`. +/// standalone function. /// /// ## Example /// ```python @@ -88,7 +88,9 @@ pub(crate) fn singledispatch_method( ); if !matches!( type_, - function_type::FunctionType::Method | function_type::FunctionType::ClassMethod + function_type::FunctionType::Method + | function_type::FunctionType::ClassMethod + | function_type::FunctionType::StaticMethod ) { return; } diff --git a/crates/ruff_linter/src/rules/pylint/rules/singledispatchmethod_function.rs b/crates/ruff_linter/src/rules/pylint/rules/singledispatchmethod_function.rs index 5a60f4b9cf67c..98a05582eb045 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/singledispatchmethod_function.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/singledispatchmethod_function.rs @@ -9,12 +9,11 @@ use crate::checkers::ast::Checker; use crate::importer::ImportRequest; /// ## What it does -/// Checks for `@singledispatchmethod` decorators on functions or static -/// methods. +/// Checks for non-method functions decorated with `@singledispatchmethod`. /// /// ## Why is this bad? -/// The `@singledispatchmethod` decorator is intended for use with class and -/// instance methods, not functions. +/// The `@singledispatchmethod` decorator is intended for use with methods, not +/// functions. /// /// Instead, use the `@singledispatch` decorator. /// @@ -85,10 +84,7 @@ pub(crate) fn singledispatchmethod_function( &checker.settings.pep8_naming.classmethod_decorators, &checker.settings.pep8_naming.staticmethod_decorators, ); - if !matches!( - type_, - function_type::FunctionType::Function | function_type::FunctionType::StaticMethod - ) { + if !matches!(type_, function_type::FunctionType::Function) { return; } diff --git a/crates/ruff_linter/src/rules/pylint/settings.rs b/crates/ruff_linter/src/rules/pylint/settings.rs index c98698d5a283c..383f5136c8de0 100644 --- a/crates/ruff_linter/src/rules/pylint/settings.rs +++ b/crates/ruff_linter/src/rules/pylint/settings.rs @@ -48,7 +48,7 @@ impl fmt::Display for ConstantType { } } -#[derive(Debug, CacheKey)] +#[derive(Debug, Clone, CacheKey)] pub struct Settings { pub allow_magic_value_types: Vec, pub allow_dunder_method_names: FxHashSet, diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1519_singledispatch_method.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1519_singledispatch_method.py.snap index fc28e7a514995..caa3e35b7c10e 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1519_singledispatch_method.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1519_singledispatch_method.py.snap @@ -40,4 +40,25 @@ singledispatch_method.py:15:5: PLE1519 [*] `@singledispatch` decorator should no 15 |+ @singledispatchmethod # [singledispatch-method] 16 16 | def move(self, position): 17 17 | pass -18 18 | +18 18 | + +singledispatch_method.py:23:5: PLE1519 [*] `@singledispatch` decorator should not be used on methods + | +21 | pass +22 | +23 | @singledispatch # [singledispatch-method] + | ^^^^^^^^^^^^^^^ PLE1519 +24 | @staticmethod +25 | def do(position): + | + = help: Replace with `@singledispatchmethod` + +ℹ Unsafe fix +20 20 | def place(self, position): +21 21 | pass +22 22 | +23 |- @singledispatch # [singledispatch-method] + 23 |+ @singledispatchmethod # [singledispatch-method] +24 24 | @staticmethod +25 25 | def do(position): +26 26 | pass diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1520_singledispatchmethod_function.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1520_singledispatchmethod_function.py.snap index 76b340f38f449..1507083e5817f 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1520_singledispatchmethod_function.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1520_singledispatchmethod_function.py.snap @@ -19,31 +19,4 @@ singledispatchmethod_function.py:4:1: PLE1520 [*] `@singledispatchmethod` decora 4 |+@singledispatch # [singledispatchmethod-function] 5 5 | def convert_position(position): 6 6 | pass -7 7 | - -singledispatchmethod_function.py:20:5: PLE1520 [*] `@singledispatchmethod` decorator should not be used on non-method functions - | -18 | pass -19 | -20 | @singledispatchmethod # [singledispatchmethod-function] - | ^^^^^^^^^^^^^^^^^^^^^ PLE1520 -21 | @staticmethod -22 | def do(position): - | - = help: Replace with `@singledispatch` - -ℹ Unsafe fix -1 |-from functools import singledispatchmethod - 1 |+from functools import singledispatchmethod, singledispatch -2 2 | -3 3 | -4 4 | @singledispatchmethod # [singledispatchmethod-function] --------------------------------------------------------------------------------- -17 17 | def move(self, position): -18 18 | pass -19 19 | -20 |- @singledispatchmethod # [singledispatchmethod-function] - 20 |+ @singledispatch # [singledispatchmethod-function] -21 21 | @staticmethod -22 22 | def do(position): -23 23 | pass +7 7 | diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0211_bad_staticmethod_argument.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0211_bad_staticmethod_argument.py.snap new file mode 100644 index 0000000000000..add63e311b094 --- /dev/null +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0211_bad_staticmethod_argument.py.snap @@ -0,0 +1,28 @@ +--- +source: crates/ruff_linter/src/rules/pylint/mod.rs +--- +bad_staticmethod_argument.py:3:13: PLW0211 First argument of a static method should not be named `self` + | +1 | class Wolf: +2 | @staticmethod +3 | def eat(self): # [bad-staticmethod-argument] + | ^^^^ PLW0211 +4 | pass + | + +bad_staticmethod_argument.py:15:13: PLW0211 First argument of a static method should not be named `cls` + | +13 | class Sheep: +14 | @staticmethod +15 | def eat(cls, x, y, z): # [bad-staticmethod-argument] + | ^^^ PLW0211 +16 | pass + | + +bad_staticmethod_argument.py:19:15: PLW0211 First argument of a static method should not be named `self` + | +18 | @staticmethod +19 | def sleep(self, x, y, z): # [bad-staticmethod-argument] + | ^^^^ PLW0211 +20 | pass + | diff --git a/crates/ruff_linter/src/rules/pyupgrade/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/mod.rs index 10fae94973062..427b448d765b1 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/mod.rs @@ -61,6 +61,7 @@ mod tests { #[test_case(Rule::ReplaceUniversalNewlines, Path::new("UP021.py"))] #[test_case(Rule::SuperCallWithParameters, Path::new("UP008.py"))] #[test_case(Rule::TimeoutErrorAlias, Path::new("UP041.py"))] + #[test_case(Rule::ReplaceStrEnum, Path::new("UP042.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"))] diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/mod.rs index aa6a60732fbed..3b7928f6e9020 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/mod.rs @@ -18,6 +18,7 @@ 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_str_enum::*; pub(crate) use replace_universal_newlines::*; pub(crate) use super_call_with_parameters::*; pub(crate) use timeout_error_alias::*; @@ -58,6 +59,7 @@ mod printf_string_formatting; mod quoted_annotation; mod redundant_open_modes; mod replace_stdout_stderr; +mod replace_str_enum; mod replace_universal_newlines; mod super_call_with_parameters; mod timeout_error_alias; diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/replace_str_enum.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/replace_str_enum.rs new file mode 100644 index 0000000000000..4717ce2a5347f --- /dev/null +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/replace_str_enum.rs @@ -0,0 +1,160 @@ +use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast as ast; +use ruff_python_ast::identifier::Identifier; +use ruff_text_size::Ranged; + +use crate::checkers::ast::Checker; +use crate::importer::ImportRequest; + +/// ## What it does +/// Checks for classes that inherit from both `str` and `enum.Enum`. +/// +/// ## Why is this bad? +/// Python 3.11 introduced `enum.StrEnum`, which is preferred over inheriting +/// from both `str` and `enum.Enum`. +/// +/// ## Example +/// +/// ```python +/// import enum +/// +/// +/// class Foo(str, enum.Enum): +/// ... +/// ``` +/// +/// Use instead: +/// +/// ```python +/// import enum +/// +/// +/// class Foo(enum.StrEnum): +/// ... +/// ``` +/// +/// ## Fix safety +/// +/// Python 3.11 introduced a [breaking change] for enums that inherit from both +/// `str` and `enum.Enum`. Consider the following enum: +/// +/// ```python +/// from enum import Enum +/// +/// +/// class Foo(str, Enum): +/// BAR = "bar" +/// ``` +/// +/// In Python 3.11, the formatted representation of `Foo.BAR` changed as +/// follows: +/// +/// ```python +/// # Python 3.10 +/// f"{Foo.BAR}" # > bar +/// # Python 3.11 +/// f"{Foo.BAR}" # > Foo.BAR +/// ``` +/// +/// Migrating from `str` and `enum.Enum` to `enum.StrEnum` will restore the +/// previous behavior, such that: +/// +/// ```python +/// from enum import StrEnum +/// +/// +/// class Foo(StrEnum): +/// BAR = "bar" +/// +/// +/// f"{Foo.BAR}" # > bar +/// ``` +/// +/// As such, migrating to `enum.StrEnum` will introduce a behavior change for +/// code that relies on the Python 3.11 behavior. +/// +/// ## References +/// - [enum.StrEnum](https://docs.python.org/3/library/enum.html#enum.StrEnum) +/// +/// [breaking change]: https://blog.pecar.me/python-enum + +#[violation] +pub struct ReplaceStrEnum { + name: String, +} + +impl Violation for ReplaceStrEnum { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + + #[derive_message_formats] + fn message(&self) -> String { + let ReplaceStrEnum { name } = self; + format!("Class {name} inherits from both `str` and `enum.Enum`") + } + + fn fix_title(&self) -> Option { + Some("Inherit from `enum.StrEnum`".to_string()) + } +} + +/// UP042 +pub(crate) fn replace_str_enum(checker: &mut Checker, class_def: &ast::StmtClassDef) { + let Some(arguments) = class_def.arguments.as_deref() else { + // class does not inherit anything, exit early + return; + }; + + // Determine whether the class inherits from both `str` and `enum.Enum`. + let mut inherits_str = false; + let mut inherits_enum = false; + for base in arguments.args.iter() { + if let Some(qualified_name) = checker.semantic().resolve_qualified_name(base) { + match qualified_name.segments() { + ["", "str"] => inherits_str = true, + ["enum", "Enum"] => inherits_enum = true, + _ => {} + } + } + + // Short-circuit if both `str` and `enum.Enum` are found. + if inherits_str && inherits_enum { + break; + } + } + + // If the class does not inherit both `str` and `enum.Enum`, exit early. + if !inherits_str || !inherits_enum { + return; + }; + + let mut diagnostic = Diagnostic::new( + ReplaceStrEnum { + name: class_def.name.to_string(), + }, + class_def.identifier(), + ); + + // If the base classes are _exactly_ `str` and `enum.Enum`, apply a fix. + // TODO(charlie): As an alternative, we could remove both arguments, and replace one of the two + // with `StrEnum`. However, `remove_argument` can't be applied multiple times within a single + // fix; doing so leads to a syntax error. + if arguments.len() == 2 { + diagnostic.try_set_fix(|| { + let (import_edit, binding) = checker.importer().get_or_import_symbol( + &ImportRequest::import("enum", "StrEnum"), + class_def.start(), + checker.semantic(), + )?; + Ok(Fix::unsafe_edits( + import_edit, + [Edit::range_replacement( + format!("({binding})"), + arguments.range(), + )], + )) + }); + } + + checker.diagnostics.push(diagnostic); +} diff --git a/crates/ruff_linter/src/rules/pyupgrade/settings.rs b/crates/ruff_linter/src/rules/pyupgrade/settings.rs index 4e228351f3639..72fedbdd339c7 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/settings.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/settings.rs @@ -4,7 +4,7 @@ use crate::display_settings; use ruff_macros::CacheKey; use std::fmt; -#[derive(Debug, Default, CacheKey)] +#[derive(Debug, Clone, Default, CacheKey)] pub struct Settings { pub keep_runtime_typing: bool, } diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP042.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP042.py.snap new file mode 100644 index 0000000000000..e7bff6e26969f --- /dev/null +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP042.py.snap @@ -0,0 +1,55 @@ +--- +source: crates/ruff_linter/src/rules/pyupgrade/mod.rs +--- +UP042.py:4:7: UP042 [*] Class A inherits from both `str` and `enum.Enum` + | +4 | class A(str, Enum): ... + | ^ UP042 + | + = help: Inherit from `enum.StrEnum` + +ℹ Unsafe fix +1 |-from enum import Enum + 1 |+from enum import Enum, StrEnum +2 2 | +3 3 | +4 |-class A(str, Enum): ... + 4 |+class A(StrEnum): ... +5 5 | +6 6 | +7 7 | class B(Enum, str): ... + +UP042.py:7:7: UP042 [*] Class B inherits from both `str` and `enum.Enum` + | +7 | class B(Enum, str): ... + | ^ UP042 + | + = help: Inherit from `enum.StrEnum` + +ℹ Unsafe fix +1 |-from enum import Enum + 1 |+from enum import Enum, StrEnum +2 2 | +3 3 | +4 4 | class A(str, Enum): ... +5 5 | +6 6 | +7 |-class B(Enum, str): ... + 7 |+class B(StrEnum): ... +8 8 | +9 9 | +10 10 | class D(int, str, Enum): ... + +UP042.py:10:7: UP042 Class D inherits from both `str` and `enum.Enum` + | +10 | class D(int, str, Enum): ... + | ^ UP042 + | + = help: Inherit from `enum.StrEnum` + +UP042.py:13:7: UP042 Class E inherits from both `str` and `enum.Enum` + | +13 | class E(str, int, Enum): ... + | ^ UP042 + | + = help: Inherit from `enum.StrEnum` diff --git a/crates/ruff_linter/src/rules/refurb/mod.rs b/crates/ruff_linter/src/rules/refurb/mod.rs index 992c89e948211..16415cc9d4bde 100644 --- a/crates/ruff_linter/src/rules/refurb/mod.rs +++ b/crates/ruff_linter/src/rules/refurb/mod.rs @@ -33,6 +33,7 @@ mod tests { #[test_case(Rule::ImplicitCwd, Path::new("FURB177.py"))] #[test_case(Rule::SingleItemMembershipTest, Path::new("FURB171.py"))] #[test_case(Rule::BitCount, Path::new("FURB161.py"))] + #[test_case(Rule::IntOnSlicedStr, Path::new("FURB166.py"))] #[test_case(Rule::RegexFlagAlias, Path::new("FURB167.py"))] #[test_case(Rule::IsinstanceTypeNone, Path::new("FURB168.py"))] #[test_case(Rule::TypeNoneComparison, Path::new("FURB169.py"))] diff --git a/crates/ruff_linter/src/rules/refurb/rules/int_on_sliced_str.rs b/crates/ruff_linter/src/rules/refurb/rules/int_on_sliced_str.rs new file mode 100644 index 0000000000000..33af2c5ae1d9b --- /dev/null +++ b/crates/ruff_linter/src/rules/refurb/rules/int_on_sliced_str.rs @@ -0,0 +1,134 @@ +use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::{Expr, ExprCall, Identifier}; +use ruff_text_size::Ranged; + +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for uses of `int` with an explicit base in which a string expression +/// is stripped of its leading prefix (i.e., `0b`, `0o`, or `0x`). +/// +/// ## Why is this bad? +/// Given an integer string with a prefix (e.g., `0xABC`), Python can automatically +/// determine the base of the integer by the prefix without needing to specify +/// it explicitly. +/// +/// Instead of `int(num[2:], 16)`, use `int(num, 0)`, which will automatically +/// deduce the base based on the prefix. +/// +/// ## Example +/// ```python +/// num = "0xABC" +/// +/// if num.startswith("0b"): +/// i = int(num[2:], 2) +/// elif num.startswith("0o"): +/// i = int(num[2:], 8) +/// elif num.startswith("0x"): +/// i = int(num[2:], 16) +/// +/// print(i) +/// ``` +/// +/// Use instead: +/// ```python +/// num = "0xABC" +/// +/// i = int(num, 0) +/// +/// print(i) +/// ``` +/// +/// ## Fix safety +/// The rule's fix is marked as unsafe, as Ruff cannot guarantee that the +/// argument to `int` will remain valid when its base is included in the +/// function call. +/// +/// ## References +/// - [Python documentation: `int`](https://docs.python.org/3/library/functions.html#int) +#[violation] +pub struct IntOnSlicedStr { + base: u8, +} + +impl AlwaysFixableViolation for IntOnSlicedStr { + #[derive_message_formats] + fn message(&self) -> String { + let IntOnSlicedStr { base } = self; + format!("Use of `int` with explicit `base={base}` after removing prefix") + } + + fn fix_title(&self) -> String { + format!("Replace with `base=0`") + } +} + +pub(crate) fn int_on_sliced_str(checker: &mut Checker, call: &ExprCall) { + // Verify that the function is `int`. + let Expr::Name(name) = call.func.as_ref() else { + return; + }; + if name.id.as_str() != "int" { + return; + } + if !checker.semantic().is_builtin("int") { + return; + } + + // There must be exactly two arguments (e.g., `int(num[2:], 16)`). + let (expression, base) = match ( + call.arguments.args.as_ref(), + call.arguments.keywords.as_ref(), + ) { + ([expression], [base]) if base.arg.as_ref().map(Identifier::as_str) == Some("base") => { + (expression, &base.value) + } + ([expression, base], []) => (expression, base), + _ => { + return; + } + }; + + // The base must be a number literal with a value of 2, 8, or 16. + let Some(base_u8) = base + .as_number_literal_expr() + .and_then(|base| base.value.as_int()) + .and_then(ruff_python_ast::Int::as_u8) + else { + return; + }; + if !matches!(base_u8, 2 | 8 | 16) { + return; + } + + // Determine whether the expression is a slice of a string (e.g., `num[2:]`). + let Expr::Subscript(expr_subscript) = expression else { + return; + }; + let Expr::Slice(expr_slice) = expr_subscript.slice.as_ref() else { + return; + }; + if expr_slice.upper.is_some() || expr_slice.step.is_some() { + return; + } + if !expr_slice + .lower + .as_ref() + .and_then(|expr| expr.as_number_literal_expr()) + .and_then(|expr| expr.value.as_int()) + .is_some_and(|expr| expr.as_u8() == Some(2)) + { + return; + } + + let mut diagnostic = Diagnostic::new(IntOnSlicedStr { base: base_u8 }, call.range()); + diagnostic.set_fix(Fix::unsafe_edits( + Edit::range_replacement( + checker.locator().slice(&*expr_subscript.value).to_string(), + expression.range(), + ), + [Edit::range_replacement("0".to_string(), base.range())], + )); + checker.diagnostics.push(diagnostic); +} diff --git a/crates/ruff_linter/src/rules/refurb/rules/mod.rs b/crates/ruff_linter/src/rules/refurb/rules/mod.rs index 9211e7083334c..fe1161a8b04b9 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/mod.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/mod.rs @@ -5,6 +5,7 @@ pub(crate) use for_loop_set_mutations::*; pub(crate) use hashlib_digest_hex::*; pub(crate) use if_expr_min_max::*; pub(crate) use implicit_cwd::*; +pub(crate) use int_on_sliced_str::*; pub(crate) use isinstance_type_none::*; pub(crate) use list_reverse_copy::*; pub(crate) use math_constant::*; @@ -32,6 +33,7 @@ mod for_loop_set_mutations; mod hashlib_digest_hex; mod if_expr_min_max; mod implicit_cwd; +mod int_on_sliced_str; mod isinstance_type_none; mod list_reverse_copy; mod math_constant; diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB166_FURB166.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB166_FURB166.py.snap new file mode 100644 index 0000000000000..c78dbdb22cad2 --- /dev/null +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB166_FURB166.py.snap @@ -0,0 +1,141 @@ +--- +source: crates/ruff_linter/src/rules/refurb/mod.rs +--- +FURB166.py:3:5: FURB166 [*] Use of `int` with explicit `base=2` after removing prefix + | +1 | # Errors +2 | +3 | _ = int("0b1010"[2:], 2) + | ^^^^^^^^^^^^^^^^^^^^ FURB166 +4 | _ = int("0o777"[2:], 8) +5 | _ = int("0xFFFF"[2:], 16) + | + = help: Replace with `base=0` + +ℹ Unsafe fix +1 1 | # Errors +2 2 | +3 |-_ = int("0b1010"[2:], 2) + 3 |+_ = int("0b1010", 0) +4 4 | _ = int("0o777"[2:], 8) +5 5 | _ = int("0xFFFF"[2:], 16) +6 6 | + +FURB166.py:4:5: FURB166 [*] Use of `int` with explicit `base=8` after removing prefix + | +3 | _ = int("0b1010"[2:], 2) +4 | _ = int("0o777"[2:], 8) + | ^^^^^^^^^^^^^^^^^^^ FURB166 +5 | _ = int("0xFFFF"[2:], 16) + | + = help: Replace with `base=0` + +ℹ Unsafe fix +1 1 | # Errors +2 2 | +3 3 | _ = int("0b1010"[2:], 2) +4 |-_ = int("0o777"[2:], 8) + 4 |+_ = int("0o777", 0) +5 5 | _ = int("0xFFFF"[2:], 16) +6 6 | +7 7 | b = "0b11" + +FURB166.py:5:5: FURB166 [*] Use of `int` with explicit `base=16` after removing prefix + | +3 | _ = int("0b1010"[2:], 2) +4 | _ = int("0o777"[2:], 8) +5 | _ = int("0xFFFF"[2:], 16) + | ^^^^^^^^^^^^^^^^^^^^^ FURB166 +6 | +7 | b = "0b11" + | + = help: Replace with `base=0` + +ℹ Unsafe fix +2 2 | +3 3 | _ = int("0b1010"[2:], 2) +4 4 | _ = int("0o777"[2:], 8) +5 |-_ = int("0xFFFF"[2:], 16) + 5 |+_ = int("0xFFFF", 0) +6 6 | +7 7 | b = "0b11" +8 8 | _ = int(b[2:], 2) + +FURB166.py:8:5: FURB166 [*] Use of `int` with explicit `base=2` after removing prefix + | + 7 | b = "0b11" + 8 | _ = int(b[2:], 2) + | ^^^^^^^^^^^^^ FURB166 + 9 | +10 | _ = int("0xFFFF"[2:], base=16) + | + = help: Replace with `base=0` + +ℹ Unsafe fix +5 5 | _ = int("0xFFFF"[2:], 16) +6 6 | +7 7 | b = "0b11" +8 |-_ = int(b[2:], 2) + 8 |+_ = int(b, 0) +9 9 | +10 10 | _ = int("0xFFFF"[2:], base=16) +11 11 | + +FURB166.py:10:5: FURB166 [*] Use of `int` with explicit `base=16` after removing prefix + | + 8 | _ = int(b[2:], 2) + 9 | +10 | _ = int("0xFFFF"[2:], base=16) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB166 +11 | +12 | _ = int(b"0xFFFF"[2:], 16) + | + = help: Replace with `base=0` + +ℹ Unsafe fix +7 7 | b = "0b11" +8 8 | _ = int(b[2:], 2) +9 9 | +10 |-_ = int("0xFFFF"[2:], base=16) + 10 |+_ = int("0xFFFF", base=0) +11 11 | +12 12 | _ = int(b"0xFFFF"[2:], 16) +13 13 | + +FURB166.py:12:5: FURB166 [*] Use of `int` with explicit `base=16` after removing prefix + | +10 | _ = int("0xFFFF"[2:], base=16) +11 | +12 | _ = int(b"0xFFFF"[2:], 16) + | ^^^^^^^^^^^^^^^^^^^^^^ FURB166 + | + = help: Replace with `base=0` + +ℹ Unsafe fix +9 9 | +10 10 | _ = int("0xFFFF"[2:], base=16) +11 11 | +12 |-_ = int(b"0xFFFF"[2:], 16) + 12 |+_ = int(b"0xFFFF", 0) +13 13 | +14 14 | +15 15 | def get_str(): + +FURB166.py:19:5: FURB166 [*] Use of `int` with explicit `base=16` after removing prefix + | +19 | _ = int(get_str()[2:], 16) + | ^^^^^^^^^^^^^^^^^^^^^^ FURB166 +20 | +21 | # OK + | + = help: Replace with `base=0` + +ℹ Unsafe fix +16 16 | return "0xFFF" +17 17 | +18 18 | +19 |-_ = int(get_str()[2:], 16) + 19 |+_ = int(get_str(), 0) +20 20 | +21 21 | # OK +22 22 | diff --git a/crates/ruff_linter/src/rules/ruff/rules/asyncio_dangling_task.rs b/crates/ruff_linter/src/rules/ruff/rules/asyncio_dangling_task.rs index 4c04fc0d43af1..8613f44e250bc 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/asyncio_dangling_task.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/asyncio_dangling_task.rs @@ -139,8 +139,7 @@ pub(crate) fn asyncio_dangling_binding( // else: // task = asyncio.create_task(make_request()) // ``` - for binding_id in - std::iter::successors(Some(binding_id), |id| semantic.shadowed_binding(*id)) + for binding_id in std::iter::successors(Some(binding_id), |id| scope.shadowed_binding(*id)) { let binding = semantic.binding(binding_id); if binding.is_used() diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF006_RUF006.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF006_RUF006.py.snap index 46fa193ae9e43..884e920dcb16c 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF006_RUF006.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF006_RUF006.py.snap @@ -41,6 +41,16 @@ RUF006.py:97:5: RUF006 Store a reference to the return value of `loop.create_tas | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF006 | +RUF006.py:152:13: RUF006 Store a reference to the return value of `asyncio.create_task` + | +150 | async def f(x: bool): +151 | if x: +152 | t = asyncio.create_task(asyncio.sleep(1)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF006 +153 | else: +154 | t = None + | + RUF006.py:170:5: RUF006 Store a reference to the return value of `loop.create_task` | 168 | def f(): @@ -60,5 +70,3 @@ RUF006.py:175:5: RUF006 Store a reference to the return value of `loop.create_ta 176 | 177 | # OK | - - diff --git a/crates/ruff_linter/src/settings/fix_safety_table.rs b/crates/ruff_linter/src/settings/fix_safety_table.rs index e3b280be0cddc..8f4382452f3c0 100644 --- a/crates/ruff_linter/src/settings/fix_safety_table.rs +++ b/crates/ruff_linter/src/settings/fix_safety_table.rs @@ -14,7 +14,7 @@ use crate::{ /// A table to keep track of which rules fixes should have /// their safety overridden. -#[derive(Debug, CacheKey, Default)] +#[derive(Debug, Clone, CacheKey, Default)] pub struct FixSafetyTable { forced_safe: RuleSet, forced_unsafe: RuleSet, diff --git a/crates/ruff_linter/src/settings/mod.rs b/crates/ruff_linter/src/settings/mod.rs index 510dc3a512634..42a4ab88b8c19 100644 --- a/crates/ruff_linter/src/settings/mod.rs +++ b/crates/ruff_linter/src/settings/mod.rs @@ -206,7 +206,7 @@ macro_rules! display_settings { }; } -#[derive(Debug, CacheKey)] +#[derive(Debug, Clone, CacheKey)] pub struct LinterSettings { pub exclude: FilePatternSet, pub extension: ExtensionMapping, diff --git a/crates/ruff_linter/src/settings/rule_table.rs b/crates/ruff_linter/src/settings/rule_table.rs index f6b8482afc71c..2e598cbf44d36 100644 --- a/crates/ruff_linter/src/settings/rule_table.rs +++ b/crates/ruff_linter/src/settings/rule_table.rs @@ -6,7 +6,7 @@ use ruff_macros::CacheKey; use crate::registry::{Rule, RuleSet, RuleSetIterator}; /// A table to keep track of which rules are enabled and whether they should be fixed. -#[derive(Debug, CacheKey, Default)] +#[derive(Debug, Clone, CacheKey, Default)] pub struct RuleTable { /// Maps rule codes to a boolean indicating if the rule should be fixed. enabled: RuleSet, diff --git a/crates/ruff_linter/src/settings/types.rs b/crates/ruff_linter/src/settings/types.rs index d89c3844c208d..99aec6740cab2 100644 --- a/crates/ruff_linter/src/settings/types.rs +++ b/crates/ruff_linter/src/settings/types.rs @@ -539,7 +539,7 @@ impl SerializationFormat { pub struct RequiredVersion(VersionSpecifiers); impl TryFrom for RequiredVersion { - type Error = pep440_rs::Pep440Error; + type Error = pep440_rs::VersionSpecifiersParseError; fn try_from(value: String) -> Result { // Treat `0.3.1` as `==0.3.1`, for backwards compatibility. @@ -590,7 +590,7 @@ impl Display for RequiredVersion { /// pattern matching. pub type IdentifierPattern = glob::Pattern; -#[derive(Debug, CacheKey, Default)] +#[derive(Debug, Clone, CacheKey, Default)] pub struct PerFileIgnores { // Ordered as (absolute path matcher, basename matcher, rules) ignores: Vec<(GlobMatcher, GlobMatcher, RuleSet)>, diff --git a/crates/ruff_python_ast/src/all.rs b/crates/ruff_python_ast/src/all.rs index 71348cdc105eb..4a5899ce172e6 100644 --- a/crates/ruff_python_ast/src/all.rs +++ b/crates/ruff_python_ast/src/all.rs @@ -39,6 +39,34 @@ impl Ranged for DunderAllName<'_> { } } +/// Abstraction for a collection of names inside an `__all__` definition, +/// e.g. `["foo", "bar"]` in `__all__ = ["foo", "bar"]` +#[derive(Debug, Clone)] +pub struct DunderAllDefinition<'a> { + /// The range of the `__all__` identifier. + range: TextRange, + /// The names inside the `__all__` definition. + names: Vec>, +} + +impl<'a> DunderAllDefinition<'a> { + /// Initialize a new [`DunderAllDefinition`] instance. + pub fn new(range: TextRange, names: Vec>) -> Self { + Self { range, names } + } + + /// The names inside the `__all__` definition. + pub fn names(&self) -> &[DunderAllName<'a>] { + &self.names + } +} + +impl Ranged for DunderAllDefinition<'_> { + fn range(&self) -> TextRange { + self.range + } +} + /// Extract the names bound to a given __all__ assignment. /// /// Accepts a closure that determines whether a given name (e.g., `"list"`) is a Python builtin. diff --git a/crates/ruff_python_ast/src/nodes.rs b/crates/ruff_python_ast/src/nodes.rs index 5a3c20e4dacc3..09de037850f69 100644 --- a/crates/ruff_python_ast/src/nodes.rs +++ b/crates/ruff_python_ast/src/nodes.rs @@ -2727,73 +2727,6 @@ pub enum ExprContext { Store, Del, } -impl ExprContext { - #[inline] - pub const fn load(&self) -> Option { - match self { - ExprContext::Load => Some(ExprContextLoad), - _ => None, - } - } - - #[inline] - pub const fn store(&self) -> Option { - match self { - ExprContext::Store => Some(ExprContextStore), - _ => None, - } - } - - #[inline] - pub const fn del(&self) -> Option { - match self { - ExprContext::Del => Some(ExprContextDel), - _ => None, - } - } -} - -pub struct ExprContextLoad; -impl From for ExprContext { - fn from(_: ExprContextLoad) -> Self { - ExprContext::Load - } -} - -impl std::cmp::PartialEq for ExprContextLoad { - #[inline] - fn eq(&self, other: &ExprContext) -> bool { - matches!(other, ExprContext::Load) - } -} - -pub struct ExprContextStore; -impl From for ExprContext { - fn from(_: ExprContextStore) -> Self { - ExprContext::Store - } -} - -impl std::cmp::PartialEq for ExprContextStore { - #[inline] - fn eq(&self, other: &ExprContext) -> bool { - matches!(other, ExprContext::Store) - } -} - -pub struct ExprContextDel; -impl From for ExprContext { - fn from(_: ExprContextDel) -> Self { - ExprContext::Del - } -} - -impl std::cmp::PartialEq for ExprContextDel { - #[inline] - fn eq(&self, other: &ExprContext) -> bool { - matches!(other, ExprContext::Del) - } -} /// See also [boolop](https://docs.python.org/3/library/ast.html#ast.BoolOp) #[derive(Clone, Debug, PartialEq, is_macro::Is, Copy, Hash, Eq)] @@ -2801,49 +2734,19 @@ pub enum BoolOp { And, Or, } -impl BoolOp { - #[inline] - pub const fn and(&self) -> Option { - match self { - BoolOp::And => Some(BoolOpAnd), - BoolOp::Or => None, - } - } - #[inline] - pub const fn or(&self) -> Option { +impl BoolOp { + pub const fn as_str(&self) -> &'static str { match self { - BoolOp::Or => Some(BoolOpOr), - BoolOp::And => None, + BoolOp::And => "and", + BoolOp::Or => "or", } } } -pub struct BoolOpAnd; -impl From for BoolOp { - fn from(_: BoolOpAnd) -> Self { - BoolOp::And - } -} - -impl std::cmp::PartialEq for BoolOpAnd { - #[inline] - fn eq(&self, other: &BoolOp) -> bool { - matches!(other, BoolOp::And) - } -} - -pub struct BoolOpOr; -impl From for BoolOp { - fn from(_: BoolOpOr) -> Self { - BoolOp::Or - } -} - -impl std::cmp::PartialEq for BoolOpOr { - #[inline] - fn eq(&self, other: &BoolOp) -> bool { - matches!(other, BoolOp::Or) +impl fmt::Display for BoolOp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) } } @@ -2864,291 +2767,30 @@ pub enum Operator { BitAnd, FloorDiv, } -impl Operator { - #[inline] - pub const fn operator_add(&self) -> Option { - match self { - Operator::Add => Some(OperatorAdd), - _ => None, - } - } - - #[inline] - pub const fn operator_sub(&self) -> Option { - match self { - Operator::Sub => Some(OperatorSub), - _ => None, - } - } - - #[inline] - pub const fn operator_mult(&self) -> Option { - match self { - Operator::Mult => Some(OperatorMult), - _ => None, - } - } - - #[inline] - pub const fn operator_mat_mult(&self) -> Option { - match self { - Operator::MatMult => Some(OperatorMatMult), - _ => None, - } - } - - #[inline] - pub const fn operator_div(&self) -> Option { - match self { - Operator::Div => Some(OperatorDiv), - _ => None, - } - } - - #[inline] - pub const fn operator_mod(&self) -> Option { - match self { - Operator::Mod => Some(OperatorMod), - _ => None, - } - } - - #[inline] - pub const fn operator_pow(&self) -> Option { - match self { - Operator::Pow => Some(OperatorPow), - _ => None, - } - } - - #[inline] - pub const fn operator_l_shift(&self) -> Option { - match self { - Operator::LShift => Some(OperatorLShift), - _ => None, - } - } - - #[inline] - pub const fn operator_r_shift(&self) -> Option { - match self { - Operator::RShift => Some(OperatorRShift), - _ => None, - } - } - #[inline] - pub const fn operator_bit_or(&self) -> Option { - match self { - Operator::BitOr => Some(OperatorBitOr), - _ => None, - } - } - - #[inline] - pub const fn operator_bit_xor(&self) -> Option { +impl Operator { + pub const fn as_str(&self) -> &'static str { match self { - Operator::BitXor => Some(OperatorBitXor), - _ => None, + Operator::Add => "+", + Operator::Sub => "-", + Operator::Mult => "*", + Operator::MatMult => "@", + Operator::Div => "/", + Operator::Mod => "%", + Operator::Pow => "**", + Operator::LShift => "<<", + Operator::RShift => ">>", + Operator::BitOr => "|", + Operator::BitXor => "^", + Operator::BitAnd => "&", + Operator::FloorDiv => "//", } } - - #[inline] - pub const fn operator_bit_and(&self) -> Option { - match self { - Operator::BitAnd => Some(OperatorBitAnd), - _ => None, - } - } - - #[inline] - pub const fn operator_floor_div(&self) -> Option { - match self { - Operator::FloorDiv => Some(OperatorFloorDiv), - _ => None, - } - } -} - -pub struct OperatorAdd; -impl From for Operator { - fn from(_: OperatorAdd) -> Self { - Operator::Add - } -} - -impl std::cmp::PartialEq for OperatorAdd { - #[inline] - fn eq(&self, other: &Operator) -> bool { - matches!(other, Operator::Add) - } -} - -pub struct OperatorSub; -impl From for Operator { - fn from(_: OperatorSub) -> Self { - Operator::Sub - } -} - -impl std::cmp::PartialEq for OperatorSub { - #[inline] - fn eq(&self, other: &Operator) -> bool { - matches!(other, Operator::Sub) - } -} - -pub struct OperatorMult; -impl From for Operator { - fn from(_: OperatorMult) -> Self { - Operator::Mult - } -} - -impl std::cmp::PartialEq for OperatorMult { - #[inline] - fn eq(&self, other: &Operator) -> bool { - matches!(other, Operator::Mult) - } -} - -pub struct OperatorMatMult; -impl From for Operator { - fn from(_: OperatorMatMult) -> Self { - Operator::MatMult - } -} - -impl std::cmp::PartialEq for OperatorMatMult { - #[inline] - fn eq(&self, other: &Operator) -> bool { - matches!(other, Operator::MatMult) - } -} - -pub struct OperatorDiv; -impl From for Operator { - fn from(_: OperatorDiv) -> Self { - Operator::Div - } -} - -impl std::cmp::PartialEq for OperatorDiv { - #[inline] - fn eq(&self, other: &Operator) -> bool { - matches!(other, Operator::Div) - } -} - -pub struct OperatorMod; -impl From for Operator { - fn from(_: OperatorMod) -> Self { - Operator::Mod - } -} - -impl std::cmp::PartialEq for OperatorMod { - #[inline] - fn eq(&self, other: &Operator) -> bool { - matches!(other, Operator::Mod) - } -} - -pub struct OperatorPow; -impl From for Operator { - fn from(_: OperatorPow) -> Self { - Operator::Pow - } -} - -impl std::cmp::PartialEq for OperatorPow { - #[inline] - fn eq(&self, other: &Operator) -> bool { - matches!(other, Operator::Pow) - } -} - -pub struct OperatorLShift; -impl From for Operator { - fn from(_: OperatorLShift) -> Self { - Operator::LShift - } -} - -impl std::cmp::PartialEq for OperatorLShift { - #[inline] - fn eq(&self, other: &Operator) -> bool { - matches!(other, Operator::LShift) - } -} - -pub struct OperatorRShift; -impl From for Operator { - fn from(_: OperatorRShift) -> Self { - Operator::RShift - } -} - -impl std::cmp::PartialEq for OperatorRShift { - #[inline] - fn eq(&self, other: &Operator) -> bool { - matches!(other, Operator::RShift) - } -} - -pub struct OperatorBitOr; -impl From for Operator { - fn from(_: OperatorBitOr) -> Self { - Operator::BitOr - } -} - -impl std::cmp::PartialEq for OperatorBitOr { - #[inline] - fn eq(&self, other: &Operator) -> bool { - matches!(other, Operator::BitOr) - } -} - -pub struct OperatorBitXor; -impl From for Operator { - fn from(_: OperatorBitXor) -> Self { - Operator::BitXor - } -} - -impl std::cmp::PartialEq for OperatorBitXor { - #[inline] - fn eq(&self, other: &Operator) -> bool { - matches!(other, Operator::BitXor) - } -} - -pub struct OperatorBitAnd; -impl From for Operator { - fn from(_: OperatorBitAnd) -> Self { - Operator::BitAnd - } -} - -impl std::cmp::PartialEq for OperatorBitAnd { - #[inline] - fn eq(&self, other: &Operator) -> bool { - matches!(other, Operator::BitAnd) - } -} - -pub struct OperatorFloorDiv; -impl From for Operator { - fn from(_: OperatorFloorDiv) -> Self { - Operator::FloorDiv - } } -impl std::cmp::PartialEq for OperatorFloorDiv { - #[inline] - fn eq(&self, other: &Operator) -> bool { - matches!(other, Operator::FloorDiv) +impl fmt::Display for Operator { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) } } @@ -3160,93 +2802,21 @@ pub enum UnaryOp { UAdd, USub, } -impl UnaryOp { - #[inline] - pub const fn invert(&self) -> Option { - match self { - UnaryOp::Invert => Some(UnaryOpInvert), - _ => None, - } - } - #[inline] - pub const fn not(&self) -> Option { - match self { - UnaryOp::Not => Some(UnaryOpNot), - _ => None, - } - } - - #[inline] - pub const fn u_add(&self) -> Option { - match self { - UnaryOp::UAdd => Some(UnaryOpUAdd), - _ => None, - } - } - - #[inline] - pub const fn u_sub(&self) -> Option { +impl UnaryOp { + pub const fn as_str(&self) -> &'static str { match self { - UnaryOp::USub => Some(UnaryOpUSub), - _ => None, + UnaryOp::Invert => "~", + UnaryOp::Not => "not", + UnaryOp::UAdd => "+", + UnaryOp::USub => "-", } } } -pub struct UnaryOpInvert; -impl From for UnaryOp { - fn from(_: UnaryOpInvert) -> Self { - UnaryOp::Invert - } -} - -impl std::cmp::PartialEq for UnaryOpInvert { - #[inline] - fn eq(&self, other: &UnaryOp) -> bool { - matches!(other, UnaryOp::Invert) - } -} - -pub struct UnaryOpNot; -impl From for UnaryOp { - fn from(_: UnaryOpNot) -> Self { - UnaryOp::Not - } -} - -impl std::cmp::PartialEq for UnaryOpNot { - #[inline] - fn eq(&self, other: &UnaryOp) -> bool { - matches!(other, UnaryOp::Not) - } -} - -pub struct UnaryOpUAdd; -impl From for UnaryOp { - fn from(_: UnaryOpUAdd) -> Self { - UnaryOp::UAdd - } -} - -impl std::cmp::PartialEq for UnaryOpUAdd { - #[inline] - fn eq(&self, other: &UnaryOp) -> bool { - matches!(other, UnaryOp::UAdd) - } -} - -pub struct UnaryOpUSub; -impl From for UnaryOp { - fn from(_: UnaryOpUSub) -> Self { - UnaryOp::USub - } -} - -impl std::cmp::PartialEq for UnaryOpUSub { - #[inline] - fn eq(&self, other: &UnaryOp) -> bool { - matches!(other, UnaryOp::USub) +impl fmt::Display for UnaryOp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) } } @@ -3264,225 +2834,27 @@ pub enum CmpOp { In, NotIn, } -impl CmpOp { - #[inline] - pub const fn cmp_op_eq(&self) -> Option { - match self { - CmpOp::Eq => Some(CmpOpEq), - _ => None, - } - } - - #[inline] - pub const fn cmp_op_not_eq(&self) -> Option { - match self { - CmpOp::NotEq => Some(CmpOpNotEq), - _ => None, - } - } - - #[inline] - pub const fn cmp_op_lt(&self) -> Option { - match self { - CmpOp::Lt => Some(CmpOpLt), - _ => None, - } - } - - #[inline] - pub const fn cmp_op_lt_e(&self) -> Option { - match self { - CmpOp::LtE => Some(CmpOpLtE), - _ => None, - } - } - - #[inline] - pub const fn cmp_op_gt(&self) -> Option { - match self { - CmpOp::Gt => Some(CmpOpGt), - _ => None, - } - } - - #[inline] - pub const fn cmp_op_gt_e(&self) -> Option { - match self { - CmpOp::GtE => Some(CmpOpGtE), - _ => None, - } - } - - #[inline] - pub const fn cmp_op_is(&self) -> Option { - match self { - CmpOp::Is => Some(CmpOpIs), - _ => None, - } - } - - #[inline] - pub const fn cmp_op_is_not(&self) -> Option { - match self { - CmpOp::IsNot => Some(CmpOpIsNot), - _ => None, - } - } - - #[inline] - pub const fn cmp_op_in(&self) -> Option { - match self { - CmpOp::In => Some(CmpOpIn), - _ => None, - } - } - #[inline] - pub const fn cmp_op_not_in(&self) -> Option { +impl CmpOp { + pub const fn as_str(&self) -> &'static str { match self { - CmpOp::NotIn => Some(CmpOpNotIn), - _ => None, + 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", } } } -pub struct CmpOpEq; -impl From for CmpOp { - fn from(_: CmpOpEq) -> Self { - CmpOp::Eq - } -} - -impl std::cmp::PartialEq for CmpOpEq { - #[inline] - fn eq(&self, other: &CmpOp) -> bool { - matches!(other, CmpOp::Eq) - } -} - -pub struct CmpOpNotEq; -impl From for CmpOp { - fn from(_: CmpOpNotEq) -> Self { - CmpOp::NotEq - } -} - -impl std::cmp::PartialEq for CmpOpNotEq { - #[inline] - fn eq(&self, other: &CmpOp) -> bool { - matches!(other, CmpOp::NotEq) - } -} - -pub struct CmpOpLt; -impl From for CmpOp { - fn from(_: CmpOpLt) -> Self { - CmpOp::Lt - } -} - -impl std::cmp::PartialEq for CmpOpLt { - #[inline] - fn eq(&self, other: &CmpOp) -> bool { - matches!(other, CmpOp::Lt) - } -} - -pub struct CmpOpLtE; -impl From for CmpOp { - fn from(_: CmpOpLtE) -> Self { - CmpOp::LtE - } -} - -impl std::cmp::PartialEq for CmpOpLtE { - #[inline] - fn eq(&self, other: &CmpOp) -> bool { - matches!(other, CmpOp::LtE) - } -} - -pub struct CmpOpGt; -impl From for CmpOp { - fn from(_: CmpOpGt) -> Self { - CmpOp::Gt - } -} - -impl std::cmp::PartialEq for CmpOpGt { - #[inline] - fn eq(&self, other: &CmpOp) -> bool { - matches!(other, CmpOp::Gt) - } -} - -pub struct CmpOpGtE; -impl From for CmpOp { - fn from(_: CmpOpGtE) -> Self { - CmpOp::GtE - } -} - -impl std::cmp::PartialEq for CmpOpGtE { - #[inline] - fn eq(&self, other: &CmpOp) -> bool { - matches!(other, CmpOp::GtE) - } -} - -pub struct CmpOpIs; -impl From for CmpOp { - fn from(_: CmpOpIs) -> Self { - CmpOp::Is - } -} - -impl std::cmp::PartialEq for CmpOpIs { - #[inline] - fn eq(&self, other: &CmpOp) -> bool { - matches!(other, CmpOp::Is) - } -} - -pub struct CmpOpIsNot; -impl From for CmpOp { - fn from(_: CmpOpIsNot) -> Self { - CmpOp::IsNot - } -} - -impl std::cmp::PartialEq for CmpOpIsNot { - #[inline] - fn eq(&self, other: &CmpOp) -> bool { - matches!(other, CmpOp::IsNot) - } -} - -pub struct CmpOpIn; -impl From for CmpOp { - fn from(_: CmpOpIn) -> Self { - CmpOp::In - } -} - -impl std::cmp::PartialEq for CmpOpIn { - #[inline] - fn eq(&self, other: &CmpOp) -> bool { - matches!(other, CmpOp::In) - } -} - -pub struct CmpOpNotIn; -impl From for CmpOp { - fn from(_: CmpOpNotIn) -> Self { - CmpOp::NotIn - } -} - -impl std::cmp::PartialEq for CmpOpNotIn { - #[inline] - fn eq(&self, other: &CmpOp) -> bool { - matches!(other, CmpOp::NotIn) +impl fmt::Display for CmpOp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) } } @@ -3991,23 +3363,6 @@ impl Deref for TypeParams { pub type Suite = Vec; -impl CmpOp { - pub fn as_str(&self) -> &'static str { - match self { - 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", - } - } -} - impl Parameters { pub fn empty(range: TextRange) -> Self { Self { diff --git a/crates/ruff_python_parser/Cargo.toml b/crates/ruff_python_parser/Cargo.toml index 2ccf94a8b181f..425537b75e14a 100644 --- a/crates/ruff_python_parser/Cargo.toml +++ b/crates/ruff_python_parser/Cargo.toml @@ -18,7 +18,6 @@ ruff_python_ast = { path = "../ruff_python_ast" } ruff_text_size = { path = "../ruff_text_size" } anyhow = { workspace = true } -bitflags = { workspace = true } bstr = { workspace = true } is-macro = { workspace = true } itertools = { workspace = true } diff --git a/crates/ruff_python_resolver/src/lib.rs b/crates/ruff_python_resolver/src/lib.rs index 52be653d6cdc9..91993cadefe35 100644 --- a/crates/ruff_python_resolver/src/lib.rs +++ b/crates/ruff_python_resolver/src/lib.rs @@ -132,9 +132,7 @@ mod tests { ($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); + insta::assert_snapshot!($value); }}; } diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index 7745426ec9ebc..ac735d97acd29 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -680,7 +680,7 @@ impl<'a> SemanticModel<'a> { /// print(python_version) /// ``` /// - /// ...then `resolve_call_path(${python_version})` will resolve to `sys.version_info`. + /// ...then `resolve_qualified_name(${python_version})` will resolve to `sys.version_info`. pub fn resolve_qualified_name<'name, 'expr: 'name>( &self, value: &'expr Expr, @@ -1525,6 +1525,12 @@ impl<'a> SemanticModel<'a> { self.flags.intersects(SemanticModelFlags::F_STRING) } + /// Return `true` if the model is in an f-string replacement field. + pub const fn in_f_string_replacement_field(&self) -> bool { + self.flags + .intersects(SemanticModelFlags::F_STRING_REPLACEMENT_FIELD) + } + /// Return `true` if the model is in boolean test. pub const fn in_boolean_test(&self) -> bool { self.flags.intersects(SemanticModelFlags::BOOLEAN_TEST) @@ -1960,6 +1966,15 @@ bitflags! { /// ``` const DUNDER_ALL_DEFINITION = 1 << 22; + /// The model is in an f-string replacement field. + /// + /// For example, the model could be visiting `x` or `y` in: + /// + /// ```python + /// f"first {x} second {y}" + /// ``` + const F_STRING_REPLACEMENT_FIELD = 1 << 23; + /// The context is in any type annotation. const ANNOTATION = Self::TYPING_ONLY_ANNOTATION.bits() | Self::RUNTIME_EVALUATED_ANNOTATION.bits() | Self::RUNTIME_REQUIRED_ANNOTATION.bits(); diff --git a/crates/ruff_server/resources/test/fixtures/settings/empty.json b/crates/ruff_server/resources/test/fixtures/settings/empty.json new file mode 100644 index 0000000000000..0967ef424bce6 --- /dev/null +++ b/crates/ruff_server/resources/test/fixtures/settings/empty.json @@ -0,0 +1 @@ +{} diff --git a/crates/ruff_server/resources/test/fixtures/settings/global_only.json b/crates/ruff_server/resources/test/fixtures/settings/global_only.json new file mode 100644 index 0000000000000..674c756a2a575 --- /dev/null +++ b/crates/ruff_server/resources/test/fixtures/settings/global_only.json @@ -0,0 +1,14 @@ +{ + "settings": { + "codeAction": { + "disableRuleComment": { + "enable": false + } + }, + "lint": { + "run": "onSave" + }, + "fixAll": false, + "logLevel": "warn" + } +} diff --git a/crates/ruff_server/resources/test/fixtures/settings/vs_code_initialization_options.json b/crates/ruff_server/resources/test/fixtures/settings/vs_code_initialization_options.json new file mode 100644 index 0000000000000..d7e7c1c7b7efc --- /dev/null +++ b/crates/ruff_server/resources/test/fixtures/settings/vs_code_initialization_options.json @@ -0,0 +1,101 @@ +{ + "settings": [ + { + "experimentalServer": true, + "cwd": "/Users/test/projects/pandas", + "workspace": "file:///Users/test/projects/pandas", + "path": [], + "ignoreStandardLibrary": true, + "interpreter": [ + "/Users/test/projects/pandas/.venv/bin/python" + ], + "importStrategy": "fromEnvironment", + "codeAction": { + "fixViolation": { + "enable": false + }, + "disableRuleComment": { + "enable": false + } + }, + "lint": { + "enable": true, + "run": "onType", + "args": [ + "--preview" + ] + }, + "format": { + "args": [] + }, + "enable": true, + "organizeImports": true, + "fixAll": true, + "showNotifications": "off" + }, + { + "experimentalServer": true, + "cwd": "/Users/test/projects/scipy", + "workspace": "file:///Users/test/projects/scipy", + "path": [], + "ignoreStandardLibrary": true, + "interpreter": [ + "/Users/test/projects/scipy/.venv/bin/python" + ], + "importStrategy": "fromEnvironment", + "codeAction": { + "fixViolation": { + "enable": false + }, + "disableRuleComment": { + "enable": true + } + }, + "lint": { + "enable": true, + "run": "onType", + "args": [ + "--preview" + ] + }, + "format": { + "args": [] + }, + "enable": true, + "organizeImports": true, + "fixAll": true, + "showNotifications": "off" + } + ], + "globalSettings": { + "experimentalServer": true, + "cwd": "/", + "workspace": "/", + "path": [], + "ignoreStandardLibrary": true, + "interpreter": [], + "importStrategy": "fromEnvironment", + "codeAction": { + "fixViolation": { + "enable": false + }, + "disableRuleComment": { + "enable": false + } + }, + "lint": { + "enable": true, + "run": "onType", + "args": [ + "--preview" + ] + }, + "format": { + "args": [] + }, + "enable": true, + "organizeImports": true, + "fixAll": false, + "showNotifications": "off" + } +} diff --git a/crates/ruff_server/src/edit.rs b/crates/ruff_server/src/edit.rs index 0de59793397fe..ec41b22a94479 100644 --- a/crates/ruff_server/src/edit.rs +++ b/crates/ruff_server/src/edit.rs @@ -2,11 +2,17 @@ mod document; mod range; +mod replacement; + +use std::collections::HashMap; pub use document::Document; pub(crate) use document::DocumentVersion; use lsp_types::PositionEncodingKind; pub(crate) use range::{RangeExt, ToRangeExt}; +pub(crate) use replacement::Replacement; + +use crate::session::ResolvedClientCapabilities; /// A convenient enumeration for supported text encodings. Can be converted to [`lsp_types::PositionEncodingKind`]. // Please maintain the order from least to greatest priority for the derived `Ord` impl. @@ -23,6 +29,14 @@ pub enum PositionEncoding { UTF8, } +/// Tracks multi-document edits to eventually merge into a `WorkspaceEdit`. +/// Compatible with clients that don't support `workspace.workspaceEdit.documentChanges`. +#[derive(Debug)] +pub(crate) enum WorkspaceEditTracker { + DocumentChanges(Vec), + Changes(HashMap>), +} + impl From for lsp_types::PositionEncodingKind { fn from(value: PositionEncoding) -> Self { match value { @@ -48,3 +62,70 @@ impl TryFrom<&lsp_types::PositionEncodingKind> for PositionEncoding { }) } } + +impl WorkspaceEditTracker { + pub(crate) fn new(client_capabilities: &ResolvedClientCapabilities) -> Self { + if client_capabilities.document_changes { + Self::DocumentChanges(Vec::default()) + } else { + Self::Changes(HashMap::default()) + } + } + + /// Sets the edits made to a specific document. This should only be called + /// once for each document `uri`, and will fail if this is called for the same `uri` + /// multiple times. + pub(crate) fn set_edits_for_document( + &mut self, + uri: lsp_types::Url, + version: DocumentVersion, + edits: Vec, + ) -> crate::Result<()> { + match self { + Self::DocumentChanges(document_edits) => { + if document_edits + .iter() + .any(|document| document.text_document.uri == uri) + { + return Err(anyhow::anyhow!( + "Attempted to add edits for a document that was already edited" + )); + } + document_edits.push(lsp_types::TextDocumentEdit { + text_document: lsp_types::OptionalVersionedTextDocumentIdentifier { + uri, + version: Some(version), + }, + edits: edits.into_iter().map(lsp_types::OneOf::Left).collect(), + }); + Ok(()) + } + Self::Changes(changes) => { + if changes.get(&uri).is_some() { + return Err(anyhow::anyhow!( + "Attempted to add edits for a document that was already edited" + )); + } + changes.insert(uri, edits); + Ok(()) + } + } + } + + pub(crate) fn is_empty(&self) -> bool { + match self { + Self::DocumentChanges(document_edits) => document_edits.is_empty(), + Self::Changes(changes) => changes.is_empty(), + } + } + + pub(crate) fn into_workspace_edit(self) -> lsp_types::WorkspaceEdit { + match self { + Self::DocumentChanges(document_edits) => lsp_types::WorkspaceEdit { + document_changes: Some(lsp_types::DocumentChanges::Edits(document_edits)), + ..Default::default() + }, + Self::Changes(changes) => lsp_types::WorkspaceEdit::new(changes), + } + } +} diff --git a/crates/ruff_server/src/edit/replacement.rs b/crates/ruff_server/src/edit/replacement.rs new file mode 100644 index 0000000000000..24a58ec3f15d6 --- /dev/null +++ b/crates/ruff_server/src/edit/replacement.rs @@ -0,0 +1,98 @@ +use ruff_text_size::{TextLen, TextRange, TextSize}; + +pub(crate) struct Replacement { + pub(crate) source_range: TextRange, + pub(crate) modified_range: TextRange, +} + +impl Replacement { + /// Creates a [`Replacement`] that describes the `source_range` of `source` to replace + /// with `modified` sliced by `modified_range`. + pub(crate) fn between( + source: &str, + source_line_starts: &[TextSize], + modified: &str, + modified_line_starts: &[TextSize], + ) -> Self { + let mut source_start = TextSize::default(); + let mut replaced_start = TextSize::default(); + let mut source_end = source.text_len(); + let mut replaced_end = modified.text_len(); + let mut line_iter = source_line_starts + .iter() + .copied() + .zip(modified_line_starts.iter().copied()); + for (source_line_start, modified_line_start) in line_iter.by_ref() { + if source_line_start != modified_line_start + || source[TextRange::new(source_start, source_line_start)] + != modified[TextRange::new(replaced_start, modified_line_start)] + { + break; + } + source_start = source_line_start; + replaced_start = modified_line_start; + } + + let mut line_iter = line_iter.rev(); + + for (old_line_start, new_line_start) in line_iter.by_ref() { + if old_line_start <= source_start + || new_line_start <= replaced_start + || source[TextRange::new(old_line_start, source_end)] + != modified[TextRange::new(new_line_start, replaced_end)] + { + break; + } + source_end = old_line_start; + replaced_end = new_line_start; + } + + Replacement { + source_range: TextRange::new(source_start, source_end), + modified_range: TextRange::new(replaced_start, replaced_end), + } + } +} + +#[cfg(test)] +mod tests { + use ruff_source_file::LineIndex; + + use super::Replacement; + + #[test] + fn find_replacement_range_works() { + let original = r#" + aaaa + bbbb + cccc + dddd + eeee + "#; + let original_index = LineIndex::from_source_text(original); + let new = r#" + bb + cccc + dd + "#; + let new_index = LineIndex::from_source_text(new); + let expected = r#" + bb + cccc + dd + "#; + let replacement = Replacement::between( + original, + original_index.line_starts(), + new, + new_index.line_starts(), + ); + let mut test = original.to_string(); + test.replace_range( + replacement.source_range.start().to_usize()..replacement.source_range.end().to_usize(), + &new[replacement.modified_range], + ); + + assert_eq!(expected, &test); + } +} diff --git a/crates/ruff_server/src/fix.rs b/crates/ruff_server/src/fix.rs new file mode 100644 index 0000000000000..fa5607f972228 --- /dev/null +++ b/crates/ruff_server/src/fix.rs @@ -0,0 +1,79 @@ +use ruff_linter::{ + linter::{FixerResult, LinterResult}, + settings::{flags, types::UnsafeFixes, LinterSettings}, + source_kind::SourceKind, +}; +use ruff_python_ast::PySourceType; +use ruff_source_file::LineIndex; +use std::{borrow::Cow, path::Path}; + +use crate::{ + edit::{Replacement, ToRangeExt}, + PositionEncoding, +}; + +pub(crate) fn fix_all( + document: &crate::edit::Document, + linter_settings: &LinterSettings, + encoding: PositionEncoding, +) -> crate::Result> { + let source = document.contents(); + + let source_type = PySourceType::default(); + + // TODO(jane): Support Jupyter Notebooks + let source_kind = SourceKind::Python(source.to_string()); + + // We need to iteratively apply all safe fixes onto a single file and then + // create a diff between the modified file and the original source to use as a single workspace + // edit. + // If we simply generated the diagnostics with `check_path` and then applied fixes individually, + // there's a possibility they could overlap or introduce new problems that need to be fixed, + // which is inconsistent with how `ruff check --fix` works. + let FixerResult { + transformed, + result: LinterResult { error, .. }, + .. + } = ruff_linter::linter::lint_fix( + Path::new(""), + None, + flags::Noqa::Enabled, + UnsafeFixes::Disabled, + linter_settings, + &source_kind, + source_type, + )?; + + if let Some(error) = error { + // abort early if a parsing error occurred + return Err(anyhow::anyhow!( + "A parsing error occurred during `fix_all`: {error}" + )); + } + + // fast path: if `transformed` is still borrowed, no changes were made and we can return early + if let Cow::Borrowed(_) = transformed { + return Ok(vec![]); + } + + let modified = transformed.source_code(); + + let modified_index = LineIndex::from_source_text(modified); + + let source_index = document.index(); + + let Replacement { + source_range, + modified_range, + } = Replacement::between( + source, + source_index.line_starts(), + modified, + modified_index.line_starts(), + ); + + Ok(vec![lsp_types::TextEdit { + range: source_range.to_range(source, source_index, encoding), + new_text: modified[modified_range].to_owned(), + }]) +} diff --git a/crates/ruff_server/src/lib.rs b/crates/ruff_server/src/lib.rs index b4d50d7523344..4814436678f6f 100644 --- a/crates/ruff_server/src/lib.rs +++ b/crates/ruff_server/src/lib.rs @@ -1,9 +1,11 @@ //! ## The Ruff Language Server pub use edit::{Document, PositionEncoding}; +use lsp_types::CodeActionKind; pub use server::Server; mod edit; +mod fix; mod format; mod lint; mod server; @@ -12,6 +14,10 @@ mod session; pub(crate) const SERVER_NAME: &str = "ruff"; pub(crate) const DIAGNOSTIC_NAME: &str = "Ruff"; +pub(crate) const SOURCE_FIX_ALL_RUFF: CodeActionKind = CodeActionKind::new("source.fixAll.ruff"); +pub(crate) const SOURCE_ORGANIZE_IMPORTS_RUFF: CodeActionKind = + CodeActionKind::new("source.organizeImports.ruff"); + /// A common result type used in most cases where a /// result type is needed. pub(crate) type Result = anyhow::Result; diff --git a/crates/ruff_server/src/lint.rs b/crates/ruff_server/src/lint.rs index 347e836c0b8db..355460913f1fb 100644 --- a/crates/ruff_server/src/lint.rs +++ b/crates/ruff_server/src/lint.rs @@ -16,14 +16,26 @@ use ruff_python_index::Indexer; use ruff_python_parser::lexer::LexResult; use ruff_python_parser::AsMode; use ruff_source_file::Locator; +use ruff_text_size::Ranged; use serde::{Deserialize, Serialize}; use crate::{edit::ToRangeExt, PositionEncoding, DIAGNOSTIC_NAME}; -#[derive(Serialize, Deserialize)] -pub(crate) struct DiagnosticFix { +/// This is serialized on the diagnostic `data` field. +#[derive(Serialize, Deserialize, Debug, Clone)] +pub(crate) struct AssociatedDiagnosticData { pub(crate) kind: DiagnosticKind, pub(crate) fix: Fix, + pub(crate) code: String, +} + +/// Describes a fix for `fixed_diagnostic` that applies `document_edits` to the source. +#[derive(Clone, Debug)] +pub(crate) struct DiagnosticFix { + pub(crate) fixed_diagnostic: lsp_types::Diagnostic, + pub(crate) title: String, + pub(crate) code: String, + pub(crate) edits: Vec, } pub(crate) fn check( @@ -78,6 +90,46 @@ pub(crate) fn check( .collect() } +pub(crate) fn fixes_for_diagnostics( + document: &crate::edit::Document, + encoding: PositionEncoding, + diagnostics: Vec, +) -> crate::Result> { + diagnostics + .into_iter() + .map(move |mut diagnostic| { + let Some(data) = diagnostic.data.take() else { + return Ok(None); + }; + let fixed_diagnostic = diagnostic; + let associated_data: crate::lint::AssociatedDiagnosticData = + serde_json::from_value(data).map_err(|err| { + anyhow::anyhow!("failed to deserialize diagnostic data: {err}") + })?; + let edits = associated_data + .fix + .edits() + .iter() + .map(|edit| lsp_types::TextEdit { + range: edit + .range() + .to_range(document.contents(), document.index(), encoding), + new_text: edit.content().unwrap_or_default().to_string(), + }); + Ok(Some(DiagnosticFix { + fixed_diagnostic, + code: associated_data.code, + title: associated_data + .kind + .suggestion + .unwrap_or(associated_data.kind.name), + edits: edits.collect(), + })) + }) + .filter_map(crate::Result::transpose) + .collect() +} + fn to_lsp_diagnostic( diagnostic: Diagnostic, document: &crate::edit::Document, @@ -92,9 +144,10 @@ fn to_lsp_diagnostic( let data = fix.and_then(|fix| { fix.applies(Applicability::Unsafe) .then(|| { - serde_json::to_value(&DiagnosticFix { + serde_json::to_value(&AssociatedDiagnosticData { kind: kind.clone(), fix, + code: rule.noqa_code().to_string(), }) .ok() }) diff --git a/crates/ruff_server/src/server.rs b/crates/ruff_server/src/server.rs index 5bce4bf79498f..dfdea1e252154 100644 --- a/crates/ruff_server/src/server.rs +++ b/crates/ruff_server/src/server.rs @@ -21,6 +21,8 @@ use types::WorkspaceFoldersServerCapabilities; use self::schedule::event_loop_thread; use self::schedule::Scheduler; use self::schedule::Task; +use crate::session::AllSettings; +use crate::session::ClientSettings; use crate::session::Session; use crate::PositionEncoding; @@ -47,15 +49,34 @@ impl Server { let init_params: types::InitializeParams = serde_json::from_value(params)?; let client_capabilities = init_params.capabilities; - let server_capabilities = Self::server_capabilities(&client_capabilities); + let position_encoding = Self::find_best_position_encoding(&client_capabilities); + let server_capabilities = Self::server_capabilities(position_encoding); + + let AllSettings { + global_settings, + mut workspace_settings, + } = AllSettings::from_value(init_params.initialization_options.unwrap_or_default()); + + let mut workspace_for_uri = |uri| { + let Some(workspace_settings) = workspace_settings.as_mut() else { + return (uri, ClientSettings::default()); + }; + let settings = workspace_settings.remove(&uri).unwrap_or_else(|| { + tracing::warn!("No workspace settings found for {uri}"); + ClientSettings::default() + }); + (uri, settings) + }; let workspaces = init_params .workspace_folders - .map(|folders| folders.into_iter().map(|folder| folder.uri).collect()) - .or_else(|| init_params.root_uri.map(|u| vec![u])) + .map(|folders| folders.into_iter().map(|folder| { + workspace_for_uri(folder.uri) + }).collect()) .or_else(|| { - tracing::debug!("No root URI or workspace(s) were provided during initialization. Using the current working directory as a default workspace..."); - Some(vec![types::Url::from_file_path(std::env::current_dir().ok()?).ok()?]) + tracing::debug!("No workspace(s) were provided during initialization. Using the current working directory as a default workspace..."); + let uri = types::Url::from_file_path(std::env::current_dir().ok()?).ok()?; + Some(vec![workspace_for_uri(uri)]) }) .ok_or_else(|| { anyhow::anyhow!("Failed to get the current working directory while creating a default workspace.") @@ -73,10 +94,15 @@ impl Server { Ok(Self { conn, - client_capabilities, threads, worker_threads, - session: Session::new(&server_capabilities, &workspaces)?, + session: Session::new( + &client_capabilities, + position_encoding, + global_settings, + workspaces, + )?, + client_capabilities, }) } @@ -177,8 +203,8 @@ impl Server { } } - fn server_capabilities(client_capabilities: &ClientCapabilities) -> types::ServerCapabilities { - let position_encoding = client_capabilities + fn find_best_position_encoding(client_capabilities: &ClientCapabilities) -> PositionEncoding { + client_capabilities .general .as_ref() .and_then(|general_capabilities| general_capabilities.position_encodings.as_ref()) @@ -188,19 +214,23 @@ impl Server { .filter_map(|encoding| PositionEncoding::try_from(encoding).ok()) .max() // this selects the highest priority position encoding }) - .unwrap_or_default(); + .unwrap_or_default() + } + + fn server_capabilities(position_encoding: PositionEncoding) -> types::ServerCapabilities { types::ServerCapabilities { position_encoding: Some(position_encoding.into()), code_action_provider: Some(types::CodeActionProviderCapability::Options( CodeActionOptions { - code_action_kinds: Some(vec![ - CodeActionKind::QUICKFIX, - CodeActionKind::SOURCE_ORGANIZE_IMPORTS, - ]), + code_action_kinds: Some( + SupportedCodeAction::all() + .flat_map(|action| action.kinds().into_iter()) + .collect(), + ), work_done_progress_options: WorkDoneProgressOptions { work_done_progress: Some(true), }, - resolve_provider: Some(false), + resolve_provider: Some(true), }, )), workspace: Some(types::WorkspaceServerCapabilities { @@ -236,3 +266,56 @@ impl Server { } } } + +/// The code actions we support. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub(crate) enum SupportedCodeAction { + /// Maps to the `quickfix` code action kind. Quick fix code actions are shown under + /// their respective diagnostics. Quick fixes are only created where the fix applicability is + /// at least [`ruff_diagnostics::Applicability::Unsafe`]. + QuickFix, + /// Maps to the `source.fixAll` and `source.fixAll.ruff` code action kinds. + /// This is a source action that applies all safe fixes to the currently open document. + SourceFixAll, + /// Maps to `source.organizeImports` and `source.organizeImports.ruff` code action kinds. + /// This is a source action that applies import sorting fixes to the currently open document. + #[allow(dead_code)] // TODO: remove + SourceOrganizeImports, +} + +impl SupportedCodeAction { + /// Returns the possible LSP code action kind(s) that map to this code action. + fn kinds(self) -> Vec { + match self { + Self::QuickFix => vec![CodeActionKind::QUICKFIX], + Self::SourceFixAll => vec![CodeActionKind::SOURCE_FIX_ALL, crate::SOURCE_FIX_ALL_RUFF], + Self::SourceOrganizeImports => vec![ + CodeActionKind::SOURCE_ORGANIZE_IMPORTS, + crate::SOURCE_ORGANIZE_IMPORTS_RUFF, + ], + } + } + + /// Returns all code actions kinds that the server currently supports. + fn all() -> impl Iterator { + [ + Self::QuickFix, + Self::SourceFixAll, + Self::SourceOrganizeImports, + ] + .into_iter() + } +} + +impl TryFrom for SupportedCodeAction { + type Error = (); + + fn try_from(kind: CodeActionKind) -> std::result::Result { + for supported_kind in Self::all() { + if supported_kind.kinds().contains(&kind) { + return Ok(supported_kind); + } + } + Err(()) + } +} diff --git a/crates/ruff_server/src/server/api.rs b/crates/ruff_server/src/server/api.rs index 21f9a26a42f82..d649379d48317 100644 --- a/crates/ruff_server/src/server/api.rs +++ b/crates/ruff_server/src/server/api.rs @@ -16,8 +16,8 @@ use super::{client::Responder, schedule::BackgroundSchedule, Result}; /// given the parameter type used by the implementer. macro_rules! define_document_url { ($params:ident: &$p:ty) => { - fn document_url($params: &$p) -> &lsp_types::Url { - &$params.text_document.uri + fn document_url($params: &$p) -> std::borrow::Cow { + std::borrow::Cow::Borrowed(&$params.text_document.uri) } }; } @@ -28,16 +28,20 @@ pub(super) fn request<'a>(req: server::Request) -> Task<'a> { let id = req.id.clone(); match req.method.as_str() { - request::CodeAction::METHOD => background_request_task::( + request::CodeActions::METHOD => background_request_task::( req, BackgroundSchedule::LatencySensitive, ), + request::CodeActionResolve::METHOD => { + background_request_task::(req, BackgroundSchedule::Worker) + } request::DocumentDiagnostic::METHOD => { background_request_task::( req, BackgroundSchedule::LatencySensitive, ) } + request::ExecuteCommand::METHOD => local_request_task::(req), request::Format::METHOD => { background_request_task::(req, BackgroundSchedule::Fmt) } @@ -84,13 +88,12 @@ pub(super) fn notification<'a>(notif: server::Notification) -> Task<'a> { }) } -#[allow(dead_code)] fn local_request_task<'a, R: traits::SyncRequestHandler>( req: server::Request, ) -> super::Result> { let (id, params) = cast_request::(req)?; - Ok(Task::local(|session, notifier, responder| { - let result = R::run(session, notifier, params); + Ok(Task::local(|session, notifier, requester, responder| { + let result = R::run(session, notifier, requester, params); respond::(id, result, &responder); })) } @@ -102,7 +105,7 @@ fn background_request_task<'a, R: traits::BackgroundDocumentRequestHandler>( let (id, params) = cast_request::(req)?; Ok(Task::background(schedule, move |session: &Session| { // TODO(jane): we should log an error if we can't take a snapshot. - let Some(snapshot) = session.take_snapshot(R::document_url(¶ms)) else { + let Some(snapshot) = session.take_snapshot(&R::document_url(¶ms)) else { return Box::new(|_, _| {}); }; Box::new(move |notifier, responder| { @@ -116,7 +119,7 @@ fn local_notification_task<'a, N: traits::SyncNotificationHandler>( notif: server::Notification, ) -> super::Result> { let (id, params) = cast_notification::(notif)?; - Ok(Task::local(move |session, notifier, _| { + Ok(Task::local(move |session, notifier, _, _| { if let Err(err) = N::run(session, notifier, params) { tracing::error!("An error occurred while running {id}: {err}"); } @@ -131,7 +134,7 @@ fn background_notification_thread<'a, N: traits::BackgroundDocumentNotificationH let (id, params) = cast_notification::(req)?; Ok(Task::background(schedule, move |session: &Session| { // TODO(jane): we should log an error if we can't take a snapshot. - let Some(snapshot) = session.take_snapshot(N::document_url(¶ms)) else { + let Some(snapshot) = session.take_snapshot(&N::document_url(¶ms)) else { return Box::new(|_, _| {}); }; Box::new(move |notifier, _| { diff --git a/crates/ruff_server/src/server/api/requests.rs b/crates/ruff_server/src/server/api/requests.rs index d29a60a660d49..3713ef536f592 100644 --- a/crates/ruff_server/src/server/api/requests.rs +++ b/crates/ruff_server/src/server/api/requests.rs @@ -1,14 +1,18 @@ mod code_action; +mod code_action_resolve; mod diagnostic; +mod execute_command; mod format; mod format_range; use super::{ define_document_url, - traits::{BackgroundDocumentRequestHandler, RequestHandler}, + traits::{BackgroundDocumentRequestHandler, RequestHandler, SyncRequestHandler}, }; -pub(super) use code_action::CodeAction; +pub(super) use code_action::CodeActions; +pub(super) use code_action_resolve::CodeActionResolve; pub(super) use diagnostic::DocumentDiagnostic; +pub(super) use execute_command::ExecuteCommand; pub(super) use format::Format; pub(super) use format_range::FormatRange; diff --git a/crates/ruff_server/src/server/api/requests/code_action.rs b/crates/ruff_server/src/server/api/requests/code_action.rs index 235b651078f06..0fa462cd2b38b 100644 --- a/crates/ruff_server/src/server/api/requests/code_action.rs +++ b/crates/ruff_server/src/server/api/requests/code_action.rs @@ -1,81 +1,173 @@ -use crate::edit::ToRangeExt; +use crate::edit::WorkspaceEditTracker; +use crate::lint::fixes_for_diagnostics; use crate::server::api::LSPResult; +use crate::server::SupportedCodeAction; use crate::server::{client::Notifier, Result}; use crate::session::DocumentSnapshot; +use crate::DIAGNOSTIC_NAME; +use lsp_server::ErrorCode; use lsp_types::{self as types, request as req}; -use ruff_text_size::Ranged; +use rustc_hash::FxHashSet; +use types::{CodeActionKind, CodeActionOrCommand}; -pub(crate) struct CodeAction; +use super::code_action_resolve::{resolve_edit_for_fix_all, resolve_edit_for_organize_imports}; -impl super::RequestHandler for CodeAction { +pub(crate) struct CodeActions; + +impl super::RequestHandler for CodeActions { type RequestType = req::CodeActionRequest; } -impl super::BackgroundDocumentRequestHandler for CodeAction { +impl super::BackgroundDocumentRequestHandler for CodeActions { super::define_document_url!(params: &types::CodeActionParams); fn run_with_snapshot( snapshot: DocumentSnapshot, _notifier: Notifier, params: types::CodeActionParams, ) -> Result> { - let document = snapshot.document(); - let url = snapshot.url(); - let encoding = snapshot.encoding(); - let version = document.version(); - let actions: Result> = params - .context - .diagnostics - .into_iter() - .map(|diagnostic| { - let Some(data) = diagnostic.data else { - return Ok(None); - }; - let diagnostic_fix: crate::lint::DiagnosticFix = serde_json::from_value(data) - .map_err(|err| anyhow::anyhow!("failed to deserialize diagnostic data: {err}")) - .with_failure_code(lsp_server::ErrorCode::ParseError)?; - let edits = diagnostic_fix - .fix - .edits() + let mut response: types::CodeActionResponse = types::CodeActionResponse::default(); + + let supported_code_actions = supported_code_actions(params.context.only.clone()); + + if supported_code_actions.contains(&SupportedCodeAction::QuickFix) { + response.extend( + quick_fix(&snapshot, params.context.diagnostics.clone()) + .with_failure_code(ErrorCode::InternalError)?, + ); + } + + if supported_code_actions.contains(&SupportedCodeAction::SourceFixAll) { + response.push(fix_all(&snapshot).with_failure_code(ErrorCode::InternalError)?); + } + + if supported_code_actions.contains(&SupportedCodeAction::SourceOrganizeImports) { + response.push(organize_imports(&snapshot).with_failure_code(ErrorCode::InternalError)?); + } + + Ok(Some(response)) + } +} + +fn quick_fix( + snapshot: &DocumentSnapshot, + diagnostics: Vec, +) -> crate::Result> { + let document = snapshot.document(); + + let fixes = fixes_for_diagnostics(document, snapshot.encoding(), diagnostics)?; + + fixes + .into_iter() + .map(|fix| { + let mut tracker = WorkspaceEditTracker::new(snapshot.resolved_client_capabilities()); + + tracker.set_edits_for_document( + snapshot.url().clone(), + document.version(), + fix.edits, + )?; + + Ok(types::CodeActionOrCommand::CodeAction(types::CodeAction { + title: format!("{DIAGNOSTIC_NAME} ({}): {}", fix.code, fix.title), + kind: Some(types::CodeActionKind::QUICKFIX), + edit: Some(tracker.into_workspace_edit()), + diagnostics: Some(vec![fix.fixed_diagnostic.clone()]), + data: Some( + serde_json::to_value(snapshot.url()).expect("document url to serialize"), + ), + ..Default::default() + })) + }) + .collect() +} + +fn fix_all(snapshot: &DocumentSnapshot) -> crate::Result { + let document = snapshot.document(); + + let (edit, data) = if snapshot + .resolved_client_capabilities() + .code_action_deferred_edit_resolution + { + // The editor will request the edit in a `CodeActionsResolve` request + ( + None, + Some(serde_json::to_value(snapshot.url()).expect("document url to serialize")), + ) + } else { + ( + Some(resolve_edit_for_fix_all( + document, + snapshot.resolved_client_capabilities(), + snapshot.url(), + &snapshot.configuration().linter, + snapshot.encoding(), + document.version(), + )?), + None, + ) + }; + let action = types::CodeAction { + title: format!("{DIAGNOSTIC_NAME}: Fix all auto-fixable problems"), + kind: Some(types::CodeActionKind::SOURCE_FIX_ALL), + edit, + data, + ..Default::default() + }; + Ok(types::CodeActionOrCommand::CodeAction(action)) +} + +fn organize_imports(snapshot: &DocumentSnapshot) -> crate::Result { + let document = snapshot.document(); + + let (edit, data) = if snapshot + .resolved_client_capabilities() + .code_action_deferred_edit_resolution + { + // The edit will be resolved later in the `CodeActionsResolve` request + ( + None, + Some(serde_json::to_value(snapshot.url()).expect("document url to serialize")), + ) + } else { + ( + Some(resolve_edit_for_organize_imports( + document, + snapshot.resolved_client_capabilities(), + snapshot.url(), + &snapshot.configuration().linter, + snapshot.encoding(), + document.version(), + )?), + None, + ) + }; + let action = types::CodeAction { + title: format!("{DIAGNOSTIC_NAME}: Organize imports"), + kind: Some(types::CodeActionKind::SOURCE_ORGANIZE_IMPORTS), + edit, + data, + ..Default::default() + }; + Ok(types::CodeActionOrCommand::CodeAction(action)) +} + +/// If `action_filter` is `None`, this returns [`SupportedCodeActionKind::all()`]. Otherwise, +/// the list is filtered. +fn supported_code_actions( + action_filter: Option>, +) -> FxHashSet { + let Some(action_filter) = action_filter else { + return SupportedCodeAction::all().collect(); + }; + + SupportedCodeAction::all() + .filter(move |action| { + action_filter.iter().any(|filter| { + action + .kinds() .iter() - .map(|edit| types::TextEdit { - range: edit.range().to_range( - document.contents(), - document.index(), - encoding, - ), - new_text: edit.content().unwrap_or_default().to_string(), - }); - - let changes = vec![types::TextDocumentEdit { - text_document: types::OptionalVersionedTextDocumentIdentifier::new( - url.clone(), - version, - ), - edits: edits.map(types::OneOf::Left).collect(), - }]; - - let title = diagnostic_fix - .kind - .suggestion - .unwrap_or(diagnostic_fix.kind.name); - Ok(Some(types::CodeAction { - title, - kind: Some(types::CodeActionKind::QUICKFIX), - edit: Some(types::WorkspaceEdit { - document_changes: Some(types::DocumentChanges::Edits(changes)), - ..Default::default() - }), - ..Default::default() - })) + .any(|kind| kind.as_str().starts_with(filter.as_str())) }) - .collect(); - - Ok(Some( - actions? - .into_iter() - .flatten() - .map(types::CodeActionOrCommand::CodeAction) - .collect(), - )) - } + }) + .collect() } diff --git a/crates/ruff_server/src/server/api/requests/code_action_resolve.rs b/crates/ruff_server/src/server/api/requests/code_action_resolve.rs new file mode 100644 index 0000000000000..c752a10827383 --- /dev/null +++ b/crates/ruff_server/src/server/api/requests/code_action_resolve.rs @@ -0,0 +1,133 @@ +use std::borrow::Cow; + +use crate::edit::{DocumentVersion, WorkspaceEditTracker}; +use crate::server::api::LSPResult; +use crate::server::SupportedCodeAction; +use crate::server::{client::Notifier, Result}; +use crate::session::{DocumentSnapshot, ResolvedClientCapabilities}; +use crate::PositionEncoding; +use lsp_server::ErrorCode; +use lsp_types::{self as types, request as req}; +use ruff_linter::codes::Rule; +use ruff_linter::settings::LinterSettings; + +pub(crate) struct CodeActionResolve; + +impl super::RequestHandler for CodeActionResolve { + type RequestType = req::CodeActionResolveRequest; +} + +impl super::BackgroundDocumentRequestHandler for CodeActionResolve { + fn document_url(params: &types::CodeAction) -> Cow { + let uri: lsp_types::Url = serde_json::from_value(params.data.clone().unwrap_or_default()) + .expect("code actions should have a URI in their data fields"); + std::borrow::Cow::Owned(uri) + } + fn run_with_snapshot( + snapshot: DocumentSnapshot, + _notifier: Notifier, + mut action: types::CodeAction, + ) -> Result { + let document = snapshot.document(); + + let action_kind: SupportedCodeAction = action + .kind + .clone() + .ok_or(anyhow::anyhow!("No kind was given for code action")) + .with_failure_code(ErrorCode::InvalidParams)? + .try_into() + .map_err(|()| anyhow::anyhow!("Code action was of an invalid kind")) + .with_failure_code(ErrorCode::InvalidParams)?; + + action.edit = match action_kind { + SupportedCodeAction::SourceFixAll => Some( + resolve_edit_for_fix_all( + document, + snapshot.resolved_client_capabilities(), + snapshot.url(), + &snapshot.configuration().linter, + snapshot.encoding(), + document.version(), + ) + .with_failure_code(ErrorCode::InternalError)?, + ), + SupportedCodeAction::SourceOrganizeImports => Some( + resolve_edit_for_organize_imports( + document, + snapshot.resolved_client_capabilities(), + snapshot.url(), + &snapshot.configuration().linter, + snapshot.encoding(), + document.version(), + ) + .with_failure_code(ErrorCode::InternalError)?, + ), + SupportedCodeAction::QuickFix => { + return Err(anyhow::anyhow!( + "Got a code action that should not need additional resolution: {action_kind:?}" + )) + .with_failure_code(ErrorCode::InvalidParams) + } + }; + + Ok(action) + } +} + +pub(super) fn resolve_edit_for_fix_all( + document: &crate::edit::Document, + client_capabilities: &ResolvedClientCapabilities, + url: &types::Url, + linter_settings: &LinterSettings, + encoding: PositionEncoding, + version: DocumentVersion, +) -> crate::Result { + let mut tracker = WorkspaceEditTracker::new(client_capabilities); + tracker.set_edits_for_document( + url.clone(), + version, + fix_all_edit(document, linter_settings, encoding)?, + )?; + Ok(tracker.into_workspace_edit()) +} + +pub(super) fn fix_all_edit( + document: &crate::edit::Document, + linter_settings: &LinterSettings, + encoding: PositionEncoding, +) -> crate::Result> { + crate::fix::fix_all(document, linter_settings, encoding) +} + +pub(super) fn resolve_edit_for_organize_imports( + document: &crate::edit::Document, + client_capabilities: &ResolvedClientCapabilities, + url: &types::Url, + linter_settings: &ruff_linter::settings::LinterSettings, + encoding: PositionEncoding, + version: DocumentVersion, +) -> crate::Result { + let mut tracker = WorkspaceEditTracker::new(client_capabilities); + tracker.set_edits_for_document( + url.clone(), + version, + organize_imports_edit(document, linter_settings, encoding)?, + )?; + Ok(tracker.into_workspace_edit()) +} + +pub(super) fn organize_imports_edit( + document: &crate::edit::Document, + linter_settings: &LinterSettings, + encoding: PositionEncoding, +) -> crate::Result> { + let mut linter_settings = linter_settings.clone(); + linter_settings.rules = [ + Rule::UnsortedImports, // I001 + Rule::MissingRequiredImport, // I002 + ] + .into_iter() + .collect(); + + crate::fix::fix_all(document, &linter_settings, encoding) +} diff --git a/crates/ruff_server/src/server/api/requests/execute_command.rs b/crates/ruff_server/src/server/api/requests/execute_command.rs new file mode 100644 index 0000000000000..c8cdb7fdec05a --- /dev/null +++ b/crates/ruff_server/src/server/api/requests/execute_command.rs @@ -0,0 +1,153 @@ +use std::str::FromStr; + +use crate::edit::WorkspaceEditTracker; +use crate::server::api::LSPResult; +use crate::server::client; +use crate::server::schedule::Task; +use crate::session::Session; +use crate::DIAGNOSTIC_NAME; +use crate::{edit::DocumentVersion, server}; +use lsp_server::ErrorCode; +use lsp_types::{self as types, request as req}; +use serde::Deserialize; + +#[derive(Debug)] +enum Command { + Format, + FixAll, + OrganizeImports, +} + +pub(crate) struct ExecuteCommand; + +#[derive(Deserialize)] +struct TextDocumentArgument { + uri: types::Url, + version: DocumentVersion, +} + +impl super::RequestHandler for ExecuteCommand { + type RequestType = req::ExecuteCommand; +} + +impl super::SyncRequestHandler for ExecuteCommand { + fn run( + session: &mut Session, + _notifier: client::Notifier, + requester: &mut client::Requester, + params: types::ExecuteCommandParams, + ) -> server::Result> { + let command = + Command::from_str(¶ms.command).with_failure_code(ErrorCode::InvalidParams)?; + + // check if we can apply a workspace edit + if !session.resolved_client_capabilities().apply_edit { + return Err(anyhow::anyhow!("Cannot execute the '{}' command: the client does not support `workspace/applyEdit`", command.label())).with_failure_code(ErrorCode::InternalError); + } + + let mut arguments: Vec = params + .arguments + .into_iter() + .map(|value| serde_json::from_value(value).with_failure_code(ErrorCode::InvalidParams)) + .collect::>()?; + + arguments.sort_by(|a, b| a.uri.cmp(&b.uri)); + arguments.dedup_by(|a, b| a.uri == b.uri); + + let mut edit_tracker = WorkspaceEditTracker::new(session.resolved_client_capabilities()); + for TextDocumentArgument { uri, version } in arguments { + let snapshot = session + .take_snapshot(&uri) + .ok_or(anyhow::anyhow!("Document snapshot not available for {uri}",)) + .with_failure_code(ErrorCode::InternalError)?; + match command { + Command::FixAll => { + let edits = super::code_action_resolve::fix_all_edit( + snapshot.document(), + &snapshot.configuration().linter, + snapshot.encoding(), + ) + .with_failure_code(ErrorCode::InternalError)?; + edit_tracker + .set_edits_for_document(uri, version, edits) + .with_failure_code(ErrorCode::InternalError)?; + } + Command::Format => { + let response = super::format::format_document(&snapshot)?; + if let Some(edits) = response { + edit_tracker + .set_edits_for_document(uri, version, edits) + .with_failure_code(ErrorCode::InternalError)?; + } + } + Command::OrganizeImports => { + let edits = super::code_action_resolve::organize_imports_edit( + snapshot.document(), + &snapshot.configuration().linter, + snapshot.encoding(), + ) + .with_failure_code(ErrorCode::InternalError)?; + edit_tracker + .set_edits_for_document(uri, version, edits) + .with_failure_code(ErrorCode::InternalError)?; + } + } + } + + if !edit_tracker.is_empty() { + apply_edit( + requester, + command.label(), + edit_tracker.into_workspace_edit(), + ) + .with_failure_code(ErrorCode::InternalError)?; + } + + Ok(None) + } +} + +impl Command { + fn label(&self) -> &str { + match self { + Self::FixAll => "Fix all auto-fixable problems", + Self::Format => "Format document", + Self::OrganizeImports => "Format imports", + } + } +} + +impl FromStr for Command { + type Err = anyhow::Error; + + fn from_str(name: &str) -> Result { + Ok(match name { + "ruff.applyAutofix" => Self::FixAll, + "ruff.applyFormat" => Self::Format, + "ruff.applyOrganizeImports" => Self::OrganizeImports, + _ => return Err(anyhow::anyhow!("Invalid command `{name}`")), + }) + } +} + +fn apply_edit( + requester: &mut client::Requester, + label: &str, + edit: types::WorkspaceEdit, +) -> crate::Result<()> { + requester.request::( + types::ApplyWorkspaceEditParams { + label: Some(format!("{DIAGNOSTIC_NAME}: {label}")), + edit, + }, + |response| { + if !response.applied { + let reason = response + .failure_reason + .unwrap_or_else(|| String::from("unspecified reason")); + tracing::error!("Failed to apply workspace edit: {}", reason); + } + Task::nothing() + }, + ) +} diff --git a/crates/ruff_server/src/server/api/requests/format.rs b/crates/ruff_server/src/server/api/requests/format.rs index 384539ad092fa..05a4485f594e7 100644 --- a/crates/ruff_server/src/server/api/requests/format.rs +++ b/crates/ruff_server/src/server/api/requests/format.rs @@ -1,10 +1,9 @@ -use crate::edit::ToRangeExt; +use crate::edit::{Replacement, ToRangeExt}; use crate::server::api::LSPResult; use crate::server::{client::Notifier, Result}; use crate::session::DocumentSnapshot; use lsp_types::{self as types, request as req}; use ruff_source_file::LineIndex; -use ruff_text_size::{TextLen, TextRange, TextSize}; use types::TextEdit; pub(crate) struct Format; @@ -20,128 +19,35 @@ impl super::BackgroundDocumentRequestHandler for Format { _notifier: Notifier, _params: types::DocumentFormattingParams, ) -> Result { - let doc = snapshot.document(); - let source = doc.contents(); - let formatted = crate::format::format(doc, &snapshot.configuration().formatter) - .with_failure_code(lsp_server::ErrorCode::InternalError)?; - // fast path - if the code is the same, return early - if formatted == source { - return Ok(None); - } - let formatted_index: LineIndex = LineIndex::from_source_text(&formatted); - - let unformatted_index = doc.index(); - - let Replacement { - source_range: replace_range, - formatted_range: replacement_text_range, - } = Replacement::between( - source, - unformatted_index.line_starts(), - &formatted, - formatted_index.line_starts(), - ); - - Ok(Some(vec![TextEdit { - range: replace_range.to_range(source, unformatted_index, snapshot.encoding()), - new_text: formatted[replacement_text_range].to_owned(), - }])) - } -} - -struct Replacement { - source_range: TextRange, - formatted_range: TextRange, -} - -impl Replacement { - /// Creates a [`Replacement`] that describes the `replace_range` of `old_text` to replace - /// with `new_text` sliced by `replacement_text_range`. - fn between( - source: &str, - source_line_starts: &[TextSize], - formatted: &str, - formatted_line_starts: &[TextSize], - ) -> Self { - let mut source_start = TextSize::default(); - let mut formatted_start = TextSize::default(); - let mut source_end = source.text_len(); - let mut formatted_end = formatted.text_len(); - let mut line_iter = source_line_starts - .iter() - .copied() - .zip(formatted_line_starts.iter().copied()); - for (source_line_start, formatted_line_start) in line_iter.by_ref() { - if source_line_start != formatted_line_start - || source[TextRange::new(source_start, source_line_start)] - != formatted[TextRange::new(formatted_start, formatted_line_start)] - { - break; - } - source_start = source_line_start; - formatted_start = formatted_line_start; - } - - let mut line_iter = line_iter.rev(); - - for (old_line_start, new_line_start) in line_iter.by_ref() { - if old_line_start <= source_start - || new_line_start <= formatted_start - || source[TextRange::new(old_line_start, source_end)] - != formatted[TextRange::new(new_line_start, formatted_end)] - { - break; - } - source_end = old_line_start; - formatted_end = new_line_start; - } - - Replacement { - source_range: TextRange::new(source_start, source_end), - formatted_range: TextRange::new(formatted_start, formatted_end), - } + format_document(&snapshot) } } -#[cfg(test)] -mod tests { - use ruff_source_file::LineIndex; - - use crate::server::api::requests::format::Replacement; - - #[test] - fn find_replacement_range_works() { - let original = r#" - aaaa - bbbb - cccc - dddd - eeee - "#; - let original_index = LineIndex::from_source_text(original); - let new = r#" - bb - cccc - dd - "#; - let new_index = LineIndex::from_source_text(new); - let expected = r#" - bb - cccc - dd - "#; - let replacement = Replacement::between( - original, - original_index.line_starts(), - new, - new_index.line_starts(), - ); - let mut test = original.to_string(); - test.replace_range( - replacement.source_range.start().to_usize()..replacement.source_range.end().to_usize(), - &new[replacement.formatted_range], - ); - - assert_eq!(expected, &test); +pub(super) fn format_document(snapshot: &DocumentSnapshot) -> Result { + let doc = snapshot.document(); + let source = doc.contents(); + let formatted = crate::format::format(doc, &snapshot.configuration().formatter) + .with_failure_code(lsp_server::ErrorCode::InternalError)?; + // fast path - if the code is the same, return early + if formatted == source { + return Ok(None); } + let formatted_index: LineIndex = LineIndex::from_source_text(&formatted); + + let unformatted_index = doc.index(); + + let Replacement { + source_range, + modified_range: formatted_range, + } = Replacement::between( + source, + unformatted_index.line_starts(), + &formatted, + formatted_index.line_starts(), + ); + + Ok(Some(vec![TextEdit { + range: source_range.to_range(source, unformatted_index, snapshot.encoding()), + new_text: formatted[formatted_range].to_owned(), + }])) } diff --git a/crates/ruff_server/src/server/api/traits.rs b/crates/ruff_server/src/server/api/traits.rs index 54639546dc9fc..59da1624e3924 100644 --- a/crates/ruff_server/src/server/api/traits.rs +++ b/crates/ruff_server/src/server/api/traits.rs @@ -1,6 +1,6 @@ //! A stateful LSP implementation that calls into the Ruff API. -use crate::server::client::Notifier; +use crate::server::client::{Notifier, Requester}; use crate::session::{DocumentSnapshot, Session}; use lsp_types::notification::Notification as LSPNotification; @@ -20,6 +20,7 @@ pub(super) trait SyncRequestHandler: RequestHandler { fn run( session: &mut Session, notifier: Notifier, + requester: &mut Requester, params: <::RequestType as Request>::Params, ) -> super::Result<<::RequestType as Request>::Result>; } @@ -31,7 +32,7 @@ pub(super) trait BackgroundDocumentRequestHandler: RequestHandler { /// implementation. fn document_url( params: &<::RequestType as Request>::Params, - ) -> &lsp_types::Url; + ) -> std::borrow::Cow; fn run_with_snapshot( snapshot: DocumentSnapshot, @@ -66,7 +67,7 @@ pub(super) trait BackgroundDocumentNotificationHandler: NotificationHandler { /// implementation. fn document_url( params: &<::NotificationType as LSPNotification>::Params, - ) -> &lsp_types::Url; + ) -> std::borrow::Cow; fn run_with_snapshot( snapshot: DocumentSnapshot, diff --git a/crates/ruff_server/src/server/schedule.rs b/crates/ruff_server/src/server/schedule.rs index 3e5ecbd35a781..fe8cc5c18c4e0 100644 --- a/crates/ruff_server/src/server/schedule.rs +++ b/crates/ruff_server/src/server/schedule.rs @@ -80,10 +80,13 @@ impl<'s> Scheduler<'s> { pub(super) fn dispatch(&mut self, task: task::Task<'s>) { match task { Task::Sync(SyncTask { func }) => { + let notifier = self.client.notifier(); + let responder = self.client.responder(); func( self.session, - self.client.notifier(), - self.client.responder(), + notifier, + &mut self.client.requester, + responder, ); } Task::Background(BackgroundTaskBuilder { diff --git a/crates/ruff_server/src/server/schedule/task.rs b/crates/ruff_server/src/server/schedule/task.rs index b4de2d8c97b0a..fdba5e3991d9a 100644 --- a/crates/ruff_server/src/server/schedule/task.rs +++ b/crates/ruff_server/src/server/schedule/task.rs @@ -2,11 +2,11 @@ use lsp_server::RequestId; use serde::Serialize; use crate::{ - server::client::{Notifier, Responder}, + server::client::{Notifier, Requester, Responder}, session::Session, }; -type LocalFn<'s> = Box; +type LocalFn<'s> = Box; type BackgroundFn = Box; @@ -68,7 +68,9 @@ impl<'s> Task<'s> { }) } /// Creates a new local task. - pub(crate) fn local(func: impl FnOnce(&mut Session, Notifier, Responder) + 's) -> Self { + pub(crate) fn local( + func: impl FnOnce(&mut Session, Notifier, &mut Requester, Responder) + 's, + ) -> Self { Self::Sync(SyncTask { func: Box::new(func), }) @@ -79,14 +81,15 @@ impl<'s> Task<'s> { where R: Serialize + Send + 'static, { - Self::local(move |_, _, responder| { + Self::local(move |_, _, _, responder| { if let Err(err) = responder.respond(id, result) { tracing::error!("Unable to send immediate response: {err}"); } }) } + /// Creates a local task that does nothing. pub(crate) fn nothing() -> Self { - Self::local(move |_, _, _| {}) + Self::local(move |_, _, _, _| {}) } } diff --git a/crates/ruff_server/src/session.rs b/crates/ruff_server/src/session.rs index aa039b28363d4..bf8bb2efc8e98 100644 --- a/crates/ruff_server/src/session.rs +++ b/crates/ruff_server/src/session.rs @@ -1,34 +1,43 @@ //! Data model, state management, and configuration resolution. -mod types; +mod capabilities; +mod settings; use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use std::{ops::Deref, sync::Arc}; use anyhow::anyhow; -use lsp_types::{ServerCapabilities, Url}; +use lsp_types::{ClientCapabilities, Url}; use ruff_workspace::resolver::{ConfigurationTransformer, Relativity}; use rustc_hash::FxHashMap; use crate::edit::{Document, DocumentVersion}; use crate::PositionEncoding; +pub(crate) use self::capabilities::ResolvedClientCapabilities; +use self::settings::ResolvedClientSettings; +pub(crate) use self::settings::{AllSettings, ClientSettings}; + /// The global state for the LSP pub(crate) struct Session { /// Workspace folders in the current session, which contain the state of all open files. workspaces: Workspaces, /// The global position encoding, negotiated during LSP initialization. position_encoding: PositionEncoding, - /// Extension-specific settings, set by the client, that apply to all workspace folders. - #[allow(dead_code)] - lsp_settings: types::ExtensionSettings, + /// Global settings provided by the client. + global_settings: ClientSettings, + /// Tracks what LSP features the client supports and doesn't support. + resolved_client_capabilities: Arc, } /// An immutable snapshot of `Session` that references /// a specific document. pub(crate) struct DocumentSnapshot { configuration: Arc, + resolved_client_capabilities: Arc, + #[allow(dead_code)] + client_settings: settings::ResolvedClientSettings, document_ref: DocumentRef, position_encoding: PositionEncoding, url: Url, @@ -48,6 +57,7 @@ pub(crate) struct Workspaces(BTreeMap); pub(crate) struct Workspace { open_documents: OpenDocuments, configuration: Arc, + settings: ClientSettings, } #[derive(Default)] @@ -70,16 +80,17 @@ pub(crate) struct DocumentRef { impl Session { pub(crate) fn new( - server_capabilities: &ServerCapabilities, - workspaces: &[Url], + client_capabilities: &ClientCapabilities, + position_encoding: PositionEncoding, + global_settings: ClientSettings, + workspaces: Vec<(Url, ClientSettings)>, ) -> crate::Result { Ok(Self { - position_encoding: server_capabilities - .position_encoding - .as_ref() - .and_then(|encoding| encoding.try_into().ok()) - .unwrap_or_default(), - lsp_settings: types::ExtensionSettings, + position_encoding, + global_settings, + resolved_client_capabilities: Arc::new(ResolvedClientCapabilities::new( + client_capabilities, + )), workspaces: Workspaces::new(workspaces)?, }) } @@ -87,6 +98,8 @@ impl Session { pub(crate) fn take_snapshot(&self, url: &Url) -> Option { Some(DocumentSnapshot { configuration: self.workspaces.configuration(url)?.clone(), + resolved_client_capabilities: self.resolved_client_capabilities.clone(), + client_settings: self.workspaces.client_settings(url, &self.global_settings), document_ref: self.workspaces.snapshot(url)?, position_encoding: self.position_encoding, url: url.clone(), @@ -125,6 +138,10 @@ impl Session { Ok(()) } + pub(crate) fn resolved_client_capabilities(&self) -> &ResolvedClientCapabilities { + &self.resolved_client_capabilities + } + pub(crate) fn encoding(&self) -> PositionEncoding { self.position_encoding } @@ -196,6 +213,10 @@ impl DocumentSnapshot { &self.configuration } + pub(crate) fn resolved_client_capabilities(&self) -> &ResolvedClientCapabilities { + &self.resolved_client_capabilities + } + pub(crate) fn document(&self) -> &DocumentRef { &self.document_ref } @@ -210,16 +231,18 @@ impl DocumentSnapshot { } impl Workspaces { - fn new(urls: &[Url]) -> crate::Result { + fn new(workspaces: Vec<(Url, ClientSettings)>) -> crate::Result { Ok(Self( - urls.iter() - .map(Workspace::new) + workspaces + .into_iter() + .map(|(url, settings)| Workspace::new(&url, settings)) .collect::>()?, )) } fn open_workspace_folder(&mut self, folder_url: &Url) -> crate::Result<()> { - let (path, workspace) = Workspace::new(folder_url)?; + // TODO(jane): find a way to allow for workspace settings to be updated dynamically + let (path, workspace) = Workspace::new(folder_url, ClientSettings::default())?; self.0.insert(path, workspace); Ok(()) } @@ -271,6 +294,24 @@ impl Workspaces { .close(url) } + fn client_settings( + &self, + url: &Url, + global_settings: &ClientSettings, + ) -> ResolvedClientSettings { + self.workspace_for_url(url).map_or_else( + || { + tracing::warn!( + "Workspace not found for {url}. Global settings will be used for this document" + ); + ResolvedClientSettings::global(global_settings) + }, + |workspace| { + ResolvedClientSettings::with_workspace(&workspace.settings, global_settings) + }, + ) + } + fn workspace_for_url(&self, url: &Url) -> Option<&Workspace> { Some(self.entry_for_url(url)?.1) } @@ -297,7 +338,7 @@ impl Workspaces { } impl Workspace { - pub(crate) fn new(root: &Url) -> crate::Result<(PathBuf, Self)> { + pub(crate) fn new(root: &Url, settings: ClientSettings) -> crate::Result<(PathBuf, Self)> { let path = root .to_file_path() .map_err(|()| anyhow!("workspace URL was not a file path!"))?; @@ -309,6 +350,7 @@ impl Workspace { Self { open_documents: OpenDocuments::default(), configuration: Arc::new(configuration), + settings, }, )) } diff --git a/crates/ruff_server/src/session/capabilities.rs b/crates/ruff_server/src/session/capabilities.rs new file mode 100644 index 0000000000000..563737542cf51 --- /dev/null +++ b/crates/ruff_server/src/session/capabilities.rs @@ -0,0 +1,43 @@ +use lsp_types::ClientCapabilities; + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub(crate) struct ResolvedClientCapabilities { + pub(crate) code_action_deferred_edit_resolution: bool, + pub(crate) apply_edit: bool, + pub(crate) document_changes: bool, +} + +impl ResolvedClientCapabilities { + pub(super) fn new(client_capabilities: &ClientCapabilities) -> Self { + let code_action_settings = client_capabilities + .text_document + .as_ref() + .and_then(|doc_settings| doc_settings.code_action.as_ref()); + let code_action_data_support = code_action_settings + .and_then(|code_action_settings| code_action_settings.data_support) + .unwrap_or_default(); + let code_action_edit_resolution = code_action_settings + .and_then(|code_action_settings| code_action_settings.resolve_support.as_ref()) + .is_some_and(|resolve_support| resolve_support.properties.contains(&"edit".into())); + + let apply_edit = client_capabilities + .workspace + .as_ref() + .and_then(|workspace| workspace.apply_edit) + .unwrap_or_default(); + + let document_changes = client_capabilities + .workspace + .as_ref() + .and_then(|workspace| workspace.workspace_edit.as_ref()) + .and_then(|workspace_edit| workspace_edit.document_changes) + .unwrap_or_default(); + + Self { + code_action_deferred_edit_resolution: code_action_data_support + && code_action_edit_resolution, + apply_edit, + document_changes, + } + } +} diff --git a/crates/ruff_server/src/session/settings.rs b/crates/ruff_server/src/session/settings.rs new file mode 100644 index 0000000000000..b6e831b1d05ed --- /dev/null +++ b/crates/ruff_server/src/session/settings.rs @@ -0,0 +1,462 @@ +use std::ops::Deref; + +use lsp_types::Url; +use rustc_hash::FxHashMap; +use serde::Deserialize; + +/// Maps a workspace URI to its associated client settings. Used during server initialization. +pub(crate) type WorkspaceSettingsMap = FxHashMap; + +/// Resolved client settings for a specific document. These settings are meant to be +/// used directly by the server, and are *not* a 1:1 representation with how the client +/// sends them. +#[derive(Debug)] +#[cfg_attr(test, derive(PartialEq, Eq))] +// TODO(jane): Remove dead code warning +#[allow(dead_code, clippy::struct_excessive_bools)] +pub(crate) struct ResolvedClientSettings { + fix_all: bool, + organize_imports: bool, + lint_enable: bool, + disable_rule_comment_enable: bool, + fix_violation_enable: bool, +} + +/// This is a direct representation of the settings schema sent by the client. +#[derive(Debug, Deserialize, Default)] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[serde(rename_all = "camelCase")] +pub(crate) struct ClientSettings { + fix_all: Option, + organize_imports: Option, + lint: Option, + code_action: Option, +} + +/// This is a direct representation of the workspace settings schema, +/// which inherits the schema of [`ClientSettings`] and adds extra fields +/// to describe the workspace it applies to. +#[derive(Debug, Deserialize)] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[serde(rename_all = "camelCase")] +struct WorkspaceSettings { + #[serde(flatten)] + settings: ClientSettings, + workspace: Url, +} + +#[derive(Debug, Deserialize)] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[serde(rename_all = "camelCase")] +struct Lint { + enable: Option, +} + +#[derive(Debug, Default, Deserialize)] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[serde(rename_all = "camelCase")] +struct CodeAction { + disable_rule_comment: Option, + fix_violation: Option, +} + +#[derive(Debug, Deserialize)] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[serde(rename_all = "camelCase")] +struct CodeActionSettings { + enable: Option, +} + +/// This is the exact schema for initialization options sent in by the client +/// during initialization. +#[derive(Debug, Deserialize)] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[serde(untagged)] +enum InitializationOptions { + #[serde(rename_all = "camelCase")] + HasWorkspaces { + global_settings: ClientSettings, + #[serde(rename = "settings")] + workspace_settings: Vec, + }, + GlobalOnly { + settings: Option, + }, +} + +/// Built from the initialization options provided by the client. +pub(crate) struct AllSettings { + pub(crate) global_settings: ClientSettings, + /// If this is `None`, the client only passed in global settings. + pub(crate) workspace_settings: Option, +} + +impl AllSettings { + /// Initializes the controller from the serialized initialization options. + /// This fails if `options` are not valid initialization options. + pub(crate) fn from_value(options: serde_json::Value) -> Self { + Self::from_init_options( + serde_json::from_value(options) + .map_err(|err| { + tracing::error!("Failed to deserialize initialization options: {err}. Falling back to default client settings..."); + }) + .unwrap_or_default(), + ) + } + + fn from_init_options(options: InitializationOptions) -> Self { + let (global_settings, workspace_settings) = match options { + InitializationOptions::GlobalOnly { settings } => (settings.unwrap_or_default(), None), + InitializationOptions::HasWorkspaces { + global_settings, + workspace_settings, + } => (global_settings, Some(workspace_settings)), + }; + + Self { + global_settings, + workspace_settings: workspace_settings.map(|workspace_settings| { + workspace_settings + .into_iter() + .map(|settings| (settings.workspace, settings.settings)) + .collect() + }), + } + } +} + +impl ResolvedClientSettings { + /// Resolves a series of client settings, prioritizing workspace settings over global settings. + /// Any fields not specified by either are set to their defaults. + pub(super) fn with_workspace( + workspace_settings: &ClientSettings, + global_settings: &ClientSettings, + ) -> Self { + Self::new_impl(&[workspace_settings, global_settings]) + } + + /// Resolves global settings only. + pub(super) fn global(global_settings: &ClientSettings) -> Self { + Self::new_impl(&[global_settings]) + } + + fn new_impl(all_settings: &[&ClientSettings]) -> Self { + Self { + fix_all: Self::resolve_or(all_settings, |settings| settings.fix_all, true), + organize_imports: Self::resolve_or( + all_settings, + |settings| settings.organize_imports, + true, + ), + lint_enable: Self::resolve_or( + all_settings, + |settings| settings.lint.as_ref()?.enable, + true, + ), + disable_rule_comment_enable: Self::resolve_or( + all_settings, + |settings| { + settings + .code_action + .as_ref()? + .disable_rule_comment + .as_ref()? + .enable + }, + true, + ), + fix_violation_enable: Self::resolve_or( + all_settings, + |settings| { + settings + .code_action + .as_ref()? + .fix_violation + .as_ref()? + .enable + }, + true, + ), + } + } + + /// Attempts to resolve a setting using a list of available client settings as sources. + /// Client settings that come earlier in the list take priority. `default` will be returned + /// if none of the settings specify the requested setting. + fn resolve_or( + all_settings: &[&ClientSettings], + get: impl Fn(&ClientSettings) -> Option, + default: T, + ) -> T { + all_settings + .iter() + .map(Deref::deref) + .find_map(get) + .unwrap_or(default) + } +} + +impl Default for InitializationOptions { + fn default() -> Self { + Self::GlobalOnly { settings: None } + } +} + +#[cfg(test)] +mod tests { + use insta::assert_debug_snapshot; + use serde::de::DeserializeOwned; + + use super::*; + + const VS_CODE_INIT_OPTIONS_FIXTURE: &str = + include_str!("../../resources/test/fixtures/settings/vs_code_initialization_options.json"); + const GLOBAL_ONLY_INIT_OPTIONS_FIXTURE: &str = + include_str!("../../resources/test/fixtures/settings/global_only.json"); + const EMPTY_INIT_OPTIONS_FIXTURE: &str = + include_str!("../../resources/test/fixtures/settings/empty.json"); + + fn deserialize_fixture(content: &str) -> T { + serde_json::from_str(content).expect("test fixture JSON should deserialize") + } + + #[test] + fn test_vs_code_init_options_deserialize() { + let options: InitializationOptions = deserialize_fixture(VS_CODE_INIT_OPTIONS_FIXTURE); + + assert_debug_snapshot!(options, @r###" + HasWorkspaces { + global_settings: ClientSettings { + fix_all: Some( + false, + ), + organize_imports: Some( + true, + ), + lint: Some( + Lint { + enable: Some( + true, + ), + }, + ), + code_action: Some( + CodeAction { + disable_rule_comment: Some( + CodeActionSettings { + enable: Some( + false, + ), + }, + ), + fix_violation: Some( + CodeActionSettings { + enable: Some( + false, + ), + }, + ), + }, + ), + }, + workspace_settings: [ + WorkspaceSettings { + settings: ClientSettings { + fix_all: Some( + true, + ), + organize_imports: Some( + true, + ), + lint: Some( + Lint { + enable: Some( + true, + ), + }, + ), + code_action: Some( + CodeAction { + disable_rule_comment: Some( + CodeActionSettings { + enable: Some( + false, + ), + }, + ), + fix_violation: Some( + CodeActionSettings { + enable: Some( + false, + ), + }, + ), + }, + ), + }, + workspace: Url { + scheme: "file", + cannot_be_a_base: false, + username: "", + password: None, + host: None, + port: None, + path: "/Users/test/projects/pandas", + query: None, + fragment: None, + }, + }, + WorkspaceSettings { + settings: ClientSettings { + fix_all: Some( + true, + ), + organize_imports: Some( + true, + ), + lint: Some( + Lint { + enable: Some( + true, + ), + }, + ), + code_action: Some( + CodeAction { + disable_rule_comment: Some( + CodeActionSettings { + enable: Some( + true, + ), + }, + ), + fix_violation: Some( + CodeActionSettings { + enable: Some( + false, + ), + }, + ), + }, + ), + }, + workspace: Url { + scheme: "file", + cannot_be_a_base: false, + username: "", + password: None, + host: None, + port: None, + path: "/Users/test/projects/scipy", + query: None, + fragment: None, + }, + }, + ], + } + "###); + } + + #[test] + fn test_vs_code_workspace_settings_resolve() { + let options = deserialize_fixture(VS_CODE_INIT_OPTIONS_FIXTURE); + let AllSettings { + global_settings, + workspace_settings, + } = AllSettings::from_init_options(options); + let url = Url::parse("file:///Users/test/projects/pandas").expect("url should parse"); + let workspace_settings = workspace_settings.expect("workspace settings should exist"); + assert_eq!( + ResolvedClientSettings::with_workspace( + workspace_settings + .get(&url) + .expect("workspace setting should exist"), + &global_settings + ), + ResolvedClientSettings { + fix_all: true, + organize_imports: true, + lint_enable: true, + disable_rule_comment_enable: false, + fix_violation_enable: false, + } + ); + let url = Url::parse("file:///Users/test/projects/scipy").expect("url should parse"); + assert_eq!( + ResolvedClientSettings::with_workspace( + workspace_settings + .get(&url) + .expect("workspace setting should exist"), + &global_settings + ), + ResolvedClientSettings { + fix_all: true, + organize_imports: true, + lint_enable: true, + disable_rule_comment_enable: true, + fix_violation_enable: false, + } + ); + } + + #[test] + fn test_global_only_init_options_deserialize() { + let options: InitializationOptions = deserialize_fixture(GLOBAL_ONLY_INIT_OPTIONS_FIXTURE); + + assert_debug_snapshot!(options, @r###" + GlobalOnly { + settings: Some( + ClientSettings { + fix_all: Some( + false, + ), + organize_imports: None, + lint: Some( + Lint { + enable: None, + }, + ), + code_action: Some( + CodeAction { + disable_rule_comment: Some( + CodeActionSettings { + enable: Some( + false, + ), + }, + ), + fix_violation: None, + }, + ), + }, + ), + } + "###); + } + + #[test] + fn test_global_only_resolves_correctly() { + let options = deserialize_fixture(GLOBAL_ONLY_INIT_OPTIONS_FIXTURE); + + let AllSettings { + global_settings, .. + } = AllSettings::from_init_options(options); + assert_eq!( + ResolvedClientSettings::global(&global_settings), + ResolvedClientSettings { + fix_all: false, + organize_imports: true, + lint_enable: true, + disable_rule_comment_enable: false, + fix_violation_enable: true, + } + ); + } + + #[test] + fn test_empty_init_options_deserialize() { + let options: InitializationOptions = deserialize_fixture(EMPTY_INIT_OPTIONS_FIXTURE); + + assert_eq!(options, InitializationOptions::default()); + } +} diff --git a/crates/ruff_server/src/session/types.rs b/crates/ruff_server/src/session/types.rs deleted file mode 100644 index 1ed23ae69da38..0000000000000 --- a/crates/ruff_server/src/session/types.rs +++ /dev/null @@ -1,3 +0,0 @@ -#[allow(dead_code)] // TODO(jane): get this wired up after the pre-release -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub(crate) struct ExtensionSettings; diff --git a/crates/ruff_shrinking/Cargo.toml b/crates/ruff_shrinking/Cargo.toml index 6a9b51047ec7c..0527b86387c53 100644 --- a/crates/ruff_shrinking/Cargo.toml +++ b/crates/ruff_shrinking/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_shrinking" -version = "0.3.4" +version = "0.3.5" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/docs/faq.md b/docs/faq.md index 6311dddfdccda..f6fff988df650 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -126,8 +126,16 @@ Despite these differences, many users have successfully switched from Pylint to those using Ruff alongside a [type checker](faq.md#how-does-ruff-compare-to-mypy-or-pyright-or-pyre), which can cover some of the functionality that Pylint provides. -Like Flake8, Pylint supports plugins (called "checkers"), while Ruff implements all rules natively. -Unlike Pylint, Ruff is capable of automatically fixing its own lint violations. +Like Flake8, Pylint supports plugins (called "checkers"), while Ruff implements all rules natively +and does not support custom or third-party rules. Unlike Pylint, Ruff is capable of automatically +fixing its own lint violations. + +In some cases, Ruff's rules may yield slightly different results than their Pylint counterparts. For +example, Ruff's [`too-many-branches`](rules/too-many-branches.md) does not count `try` blocks as +their own branches, unlike Pylint's `R0912`. Ruff's `PL` rule group also includes a small number of +rules from Pylint _extensions_ (like [`magic-value-comparison`](rules/magic-value-comparison.md)), +which need to be explicitly activated when using Pylint. By enabling Ruff's `PL` group, you may +see violations for rules that weren't previously enabled through your Pylint configuration. Pylint parity is being tracked in [#970](https://github.com/astral-sh/ruff/issues/970). diff --git a/docs/formatter.md b/docs/formatter.md index 04d04b14eaf3f..c85b8c82c42c5 100644 --- a/docs/formatter.md +++ b/docs/formatter.md @@ -12,7 +12,7 @@ directories, and formats all discovered Python files: ```shell ruff format # Format all files in the current directory. -ruff format path/to/code/ # Lint all files in `path/to/code` (and any subdirectories). +ruff format path/to/code/ # Format all files in `path/to/code` (and any subdirectories). ruff format path/to/file.py # Format a single file. ``` diff --git a/docs/integrations.md b/docs/integrations.md index 2304286ed0522..fe1a45882b10d 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -14,7 +14,7 @@ Ruff can be used as a [pre-commit](https://pre-commit.com) hook via [`ruff-pre-c ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.3.4 + rev: v0.3.5 hooks: # Run the linter. - id: ruff @@ -27,7 +27,7 @@ To enable lint fixes, add the `--fix` argument to the lint hook: ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.3.4 + rev: v0.3.5 hooks: # Run the linter. - id: ruff @@ -41,7 +41,7 @@ To run the hooks over Jupyter Notebooks too, add `jupyter` to the list of allowe ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.3.4 + rev: v0.3.5 hooks: # Run the linter. - id: ruff diff --git a/playground/api/package-lock.json b/playground/api/package-lock.json index ac1395ff80011..293caa2cdfd25 100644 --- a/playground/api/package-lock.json +++ b/playground/api/package-lock.json @@ -16,7 +16,7 @@ "@cloudflare/workers-types": "^4.20230801.0", "miniflare": "^3.20230801.1", "typescript": "^5.1.6", - "wrangler": "3.37.0" + "wrangler": "3.42.0" } }, "node_modules/@cloudflare/kv-asset-handler": { @@ -29,9 +29,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-64": { - "version": "1.20240320.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20240320.1.tgz", - "integrity": "sha512-ioG5k2M17xyiAlK/k3L21NZLMVeSHMjwlmGtZyCyzSLL5/zGINcgZ5yPLV0UuWiysw07/6Jjzm5Sx94hzMVybg==", + "version": "1.20240329.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20240329.0.tgz", + "integrity": "sha512-/raHmsHrYjoC5am84wqyiZIDCRrrYN6YDFb4zchwWQzJ0ZHleUeY6IzNdjujrS/gYey/+Db9oyl2PD1xAZt4gA==", "cpu": [ "x64" ], @@ -45,9 +45,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-arm64": { - "version": "1.20240320.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20240320.1.tgz", - "integrity": "sha512-Ga6RDdnFEIsN4WuWsaP9bLGvK9K7pEIVoSIgmw6vweVlD8UK/a2MPGrsF1ogwdeCTCOMY8wUh9poL/Yu48IPpg==", + "version": "1.20240329.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20240329.0.tgz", + "integrity": "sha512-3wnwVdfFDt+JUhlA6NWW+093ryGNF0HMuBmkOh0PG6j4GMRH8Y+EDsqzqrzT3ZoGGXbI9x1H7k15VKb3LAN/KA==", "cpu": [ "arm64" ], @@ -61,9 +61,9 @@ } }, "node_modules/@cloudflare/workerd-linux-64": { - "version": "1.20240320.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20240320.1.tgz", - "integrity": "sha512-KFof5H8eU0NXv+pUAU7Lk/OLtOmfsioTJqu0v6kPL7QsTGsgzj5sEQNcQ8DONSze549Yflu5W00qpA2cPz9eWQ==", + "version": "1.20240329.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20240329.0.tgz", + "integrity": "sha512-E909ZIXgjdr2iuq5bF/vq02elizDlPQoYRiKojdvODC7w8rbnpwnuptajS4xK5kmKh4XBiU2o9NDhut/W1kfyw==", "cpu": [ "x64" ], @@ -77,9 +77,9 @@ } }, "node_modules/@cloudflare/workerd-linux-arm64": { - "version": "1.20240320.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20240320.1.tgz", - "integrity": "sha512-t+kGc6dGdkKvVMGcHCPhlCsUZF5dj8xbAFvLB7DAJ8T79ys30rmY2Lu/C8vKlhjH9TJhbzgKmPaJ0wC/K4euvw==", + "version": "1.20240329.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20240329.0.tgz", + "integrity": "sha512-PELA3FVW75pKchsSI5o40oiClFY2Uiq+KUx/f/srwz2pIJoM5YWLmFrv+s8feKoEwuabxIGSzHxy7QA++HyprQ==", "cpu": [ "arm64" ], @@ -93,9 +93,9 @@ } }, "node_modules/@cloudflare/workerd-windows-64": { - "version": "1.20240320.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20240320.1.tgz", - "integrity": "sha512-9xDylCOsuzWqGuANkuUByiJ5RHeMqgw37FiI7rn8I6zdGAc/alOB9B4Bh7B73WC2uEpFL+XCEjcHZ6NmsO4NaQ==", + "version": "1.20240329.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20240329.0.tgz", + "integrity": "sha512-/T+AcjVqTuqAeGBQmjAF4TOTm8sv3BSO/NtUPa1ghCvsp1sb03L6/c3wFc9ZonSdRYeBb0XDX7PnenGCvjr/Tw==", "cpu": [ "x64" ], @@ -109,9 +109,9 @@ } }, "node_modules/@cloudflare/workers-types": { - "version": "4.20240320.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20240320.1.tgz", - "integrity": "sha512-CiYtVpQURPgQqtBKkmOAnfPElVZuD7Xyf1IxKtKp2B4aB9gnooapwJhzeY8c4Ls4u17SgMS0MprOkrgYwzZ6xg==", + "version": "4.20240329.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20240329.0.tgz", + "integrity": "sha512-AbzgvSQjG8Nci4xxQEcjTTVjiWXgOQnFIbIHtEZXteHiMGDXMWGegjWBo5JHGsZCq+U5V/SD5EnlypQnUQEoig==", "dev": true }, "node_modules/@cspotcode/source-map-support": { @@ -559,20 +559,6 @@ "node": ">=16.13" } }, - "node_modules/@miniflare/shared": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/@miniflare/shared/-/shared-2.14.0.tgz", - "integrity": "sha512-O0jAEdMkp8BzrdFCfMWZu76h4Cq+tt3/oDtcTFgzum3fRW5vUhIi/5f6bfndu6rkGbSlzxwor8CJWpzityXGug==", - "dependencies": { - "@types/better-sqlite3": "^7.6.0", - "kleur": "^4.1.4", - "npx-import": "^1.1.4", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=16.13" - } - }, "node_modules/@miniflare/storage-memory": { "version": "2.14.2", "resolved": "https://registry.npmjs.org/@miniflare/storage-memory/-/storage-memory-2.14.2.tgz", @@ -905,15 +891,6 @@ "source-map": "^0.6.1" } }, - "node_modules/get-source/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -1090,9 +1067,9 @@ } }, "node_modules/miniflare": { - "version": "3.20240320.0", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-3.20240320.0.tgz", - "integrity": "sha512-4M2QRxs+J5sUsybBzKT++tlbrjjjGZdtWxKmj2sqLsT26dGaKDz7DxjAeF5XIhKa5cADcffygjxx4EvfWocMmw==", + "version": "3.20240329.0", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-3.20240329.0.tgz", + "integrity": "sha512-kdHlMwhV241kck5oh8uyKPIhCusP1BL4+iOSeJZgcJ46EATA6crWtYqlARNU9t/iYXhzKhXOlOPJjjlCJuOgTA==", "dev": true, "dependencies": { "@cspotcode/source-map-support": "0.8.1", @@ -1103,7 +1080,7 @@ "glob-to-regexp": "^0.4.1", "stoppable": "^1.1.0", "undici": "^5.28.2", - "workerd": "1.20240320.1", + "workerd": "1.20240329.0", "ws": "^8.11.0", "youch": "^3.2.2", "zod": "^3.20.6" @@ -1376,6 +1353,15 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/sourcemap-codec": { "version": "1.4.8", "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", @@ -1507,9 +1493,9 @@ } }, "node_modules/workerd": { - "version": "1.20240320.1", - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20240320.1.tgz", - "integrity": "sha512-nuavAGGjh0qqM6RF5zxTHyUwEqdLCHchodbrpbh/xlJpFGnJVY5C1YgSi2S9aLkJJoa0/25Ta/+EzXEbApA/3w==", + "version": "1.20240329.0", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20240329.0.tgz", + "integrity": "sha512-6wWuMOwWsp3K6447XsI/MZYFq0KlpV2zVbbNFEkv3N7UgJJKaHGwL/hilr6RlS4UFLU4co8nrF2lc5uR781HKg==", "dev": true, "hasInstallScript": true, "bin": { @@ -1519,17 +1505,17 @@ "node": ">=16" }, "optionalDependencies": { - "@cloudflare/workerd-darwin-64": "1.20240320.1", - "@cloudflare/workerd-darwin-arm64": "1.20240320.1", - "@cloudflare/workerd-linux-64": "1.20240320.1", - "@cloudflare/workerd-linux-arm64": "1.20240320.1", - "@cloudflare/workerd-windows-64": "1.20240320.1" + "@cloudflare/workerd-darwin-64": "1.20240329.0", + "@cloudflare/workerd-darwin-arm64": "1.20240329.0", + "@cloudflare/workerd-linux-64": "1.20240329.0", + "@cloudflare/workerd-linux-arm64": "1.20240329.0", + "@cloudflare/workerd-windows-64": "1.20240329.0" } }, "node_modules/wrangler": { - "version": "3.37.0", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.37.0.tgz", - "integrity": "sha512-dffPF92EApW77lIYXxz0DMoMm2LPMlFNlgrQ0jNj7g7Mm/AaogtSuY7jXNLSeoniYNHL/57V7wlVBn82aYIqyg==", + "version": "3.42.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.42.0.tgz", + "integrity": "sha512-t/Fq80aG5RrCyN118aV9oG1Tt66QEatz0tarKzpy0cuUMUf3T3yJAuYb6kmYIy6+beQJNoGWSAjjhAQnOn2MCQ==", "dev": true, "dependencies": { "@cloudflare/kv-asset-handler": "0.3.1", @@ -1538,7 +1524,7 @@ "blake3-wasm": "^2.1.5", "chokidar": "^3.5.3", "esbuild": "0.17.19", - "miniflare": "3.20240320.0", + "miniflare": "3.20240329.0", "nanoid": "^3.3.3", "path-to-regexp": "^6.2.0", "resolve": "^1.22.8", @@ -1566,15 +1552,6 @@ } } }, - "node_modules/wrangler/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/ws": { "version": "8.13.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", diff --git a/playground/api/package.json b/playground/api/package.json index af705921be240..107cdc7d91090 100644 --- a/playground/api/package.json +++ b/playground/api/package.json @@ -5,7 +5,7 @@ "@cloudflare/workers-types": "^4.20230801.0", "miniflare": "^3.20230801.1", "typescript": "^5.1.6", - "wrangler": "3.37.0" + "wrangler": "3.42.0" }, "private": true, "scripts": { diff --git a/playground/package-lock.json b/playground/package-lock.json index 238366c40a962..9a79c877c722f 100644 --- a/playground/package-lock.json +++ b/playground/package-lock.json @@ -1046,31 +1046,24 @@ "dev": true }, "node_modules/@types/react": { - "version": "18.2.71", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.71.tgz", - "integrity": "sha512-PxEsB9OjmQeYGffoWnYAd/r5FiJuUw2niFQHPc2v2idwh8wGPkkYzOHuinNJJY6NZqfoTCiOIizDOz38gYNsyw==", + "version": "18.2.73", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.73.tgz", + "integrity": "sha512-XcGdod0Jjv84HOC7N5ziY3x+qL0AfmubvKOZ9hJjJ2yd5EE+KYjWhdOjt387e9HPheHkdggF9atTifMRtyAaRA==", "dev": true, "dependencies": { "@types/prop-types": "*", - "@types/scheduler": "*", "csstype": "^3.0.2" } }, "node_modules/@types/react-dom": { - "version": "18.2.22", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.22.tgz", - "integrity": "sha512-fHkBXPeNtfvri6gdsMYyW+dW7RXFo6Ad09nLFK0VQWR7yGLai/Cyvyj696gbwYvBnhGtevUG9cET0pmUbMtoPQ==", + "version": "18.2.23", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.23.tgz", + "integrity": "sha512-ZQ71wgGOTmDYpnav2knkjr3qXdAFu0vsk8Ci5w3pGAIdj7/kKAyn+VsQDhXsmzzzepAiI9leWMmubXz690AI/A==", "dev": true, "dependencies": { "@types/react": "*" } }, - "node_modules/@types/scheduler": { - "version": "0.16.3", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", - "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", - "dev": true - }, "node_modules/@types/semver": { "version": "7.5.8", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", @@ -3492,9 +3485,9 @@ } }, "node_modules/jiti": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.19.1.tgz", - "integrity": "sha512-oVhqoRDaBXf7sjkll95LHVS6Myyyb1zaunVwk4Z0+WPSW4gjS0pl01zYKHScTuyEhQsFxV5L4DR5r+YqSyqyyg==", + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", + "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", "dev": true, "bin": { "jiti": "bin/jiti.js" @@ -4792,9 +4785,9 @@ } }, "node_modules/tailwindcss": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz", - "integrity": "sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.3.tgz", + "integrity": "sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==", "dev": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -4805,7 +4798,7 @@ "fast-glob": "^3.3.0", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", - "jiti": "^1.19.1", + "jiti": "^1.21.0", "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", @@ -5074,13 +5067,13 @@ "dev": true }, "node_modules/vite": { - "version": "5.2.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.6.tgz", - "integrity": "sha512-FPtnxFlSIKYjZ2eosBQamz4CbyrTizbZ3hnGJlh/wMtCrlp1Hah6AzBLjGI5I2urTfNnpovpHdrL6YRuBOPnCA==", + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.7.tgz", + "integrity": "sha512-k14PWOKLI6pMaSzAuGtT+Cf0YmIx12z9YGon39onaJNy8DLBfBJrzg9FQEmkAM5lpHBZs9wksWAsyF/HkpEwJA==", "dev": true, "dependencies": { "esbuild": "^0.20.1", - "postcss": "^8.4.36", + "postcss": "^8.4.38", "rollup": "^4.13.0" }, "bin": { diff --git a/pyproject.toml b/pyproject.toml index 39471ada77f31..c1db0a858587e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "ruff" -version = "0.3.4" +version = "0.3.5" description = "An extremely fast Python linter and code formatter, written in Rust." authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }] readme = "README.md" diff --git a/ruff.schema.json b/ruff.schema.json index a821bcea599c6..ea85c0ce15c40 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -3064,6 +3064,7 @@ "FURB161", "FURB163", "FURB164", + "FURB166", "FURB167", "FURB168", "FURB169", @@ -3389,6 +3390,8 @@ "PLW0131", "PLW0133", "PLW02", + "PLW021", + "PLW0211", "PLW024", "PLW0245", "PLW04", @@ -3865,6 +3868,7 @@ "UP04", "UP040", "UP041", + "UP042", "W", "W1", "W19", diff --git a/scripts/benchmarks/pyproject.toml b/scripts/benchmarks/pyproject.toml index 14e5c802aa425..c34cf9fa30d9e 100644 --- a/scripts/benchmarks/pyproject.toml +++ b/scripts/benchmarks/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "scripts" -version = "0.3.4" +version = "0.3.5" description = "" authors = ["Charles Marsh "]