Compare commits
1 Commits
v0.4.7
...
dhruv/synt
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
909798ffa2 |
@@ -1,10 +1,3 @@
|
||||
[alias]
|
||||
dev = "run --package ruff_dev --bin ruff_dev"
|
||||
benchmark = "bench -p ruff_benchmark --bench linter --bench formatter --"
|
||||
|
||||
# statically link the C runtime so the executable does not depend on
|
||||
# that shared/dynamic library.
|
||||
#
|
||||
# See: https://github.com/astral-sh/ruff/issues/11503
|
||||
[target.'cfg(all(target_env="msvc", target_os = "windows"))']
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
|
||||
3
.github/CODEOWNERS
vendored
3
.github/CODEOWNERS
vendored
@@ -15,6 +15,3 @@
|
||||
|
||||
# Script for fuzzing the parser
|
||||
/scripts/fuzz-parser/ @AlexWaygood
|
||||
|
||||
# red-knot
|
||||
/crates/red_knot/ @carljm @MichaReiser
|
||||
|
||||
3
.github/workflows/ci.yaml
vendored
3
.github/workflows/ci.yaml
vendored
@@ -167,9 +167,6 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: "Run tests"
|
||||
shell: bash
|
||||
env:
|
||||
# Workaround for <https://github.com/nextest-rs/nextest/issues/1493>.
|
||||
RUSTUP_WINDOWS_PATH_ADD_BIN: 1
|
||||
run: |
|
||||
cargo nextest run --all-features --profile ci
|
||||
cargo test --all-features --doc
|
||||
|
||||
5
.vscode/extensions.json
vendored
5
.vscode/extensions.json
vendored
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"rust-lang.rust-analyzer"
|
||||
]
|
||||
}
|
||||
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"rust-analyzer.check.extraArgs": [
|
||||
"--all-features"
|
||||
],
|
||||
"rust-analyzer.check.command": "clippy",
|
||||
}
|
||||
129
CHANGELOG.md
129
CHANGELOG.md
@@ -1,130 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 0.4.7
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`flake8-pyi`\] Implement `PYI064` ([#11325](https://github.com/astral-sh/ruff/pull/11325))
|
||||
- \[`flake8-pyi`\] Implement `PYI066` ([#11541](https://github.com/astral-sh/ruff/pull/11541))
|
||||
- \[`flake8-pyi`\] Implement `PYI057` ([#11486](https://github.com/astral-sh/ruff/pull/11486))
|
||||
- \[`pyflakes`\] Add option to enable F822 in `__init__.py` files ([#11370](https://github.com/astral-sh/ruff/pull/11370))
|
||||
|
||||
### Formatter
|
||||
|
||||
- Fix incorrect placement of trailing stub function comments ([#11632](https://github.com/astral-sh/ruff/pull/11632))
|
||||
|
||||
### Server
|
||||
|
||||
- Respect file exclusions in `ruff server` ([#11590](https://github.com/astral-sh/ruff/pull/11590))
|
||||
- Add support for documents not exist on disk ([#11588](https://github.com/astral-sh/ruff/pull/11588))
|
||||
- Add Vim and Kate setup guide for `ruff server` ([#11615](https://github.com/astral-sh/ruff/pull/11615))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Avoid removing newlines between docstring headers and rST blocks ([#11609](https://github.com/astral-sh/ruff/pull/11609))
|
||||
- Infer indentation with imports when logical indent is absent ([#11608](https://github.com/astral-sh/ruff/pull/11608))
|
||||
- Use char index rather than position for indent slice ([#11645](https://github.com/astral-sh/ruff/pull/11645))
|
||||
- \[`flake8-comprehension`\] Strip parentheses around generators in `C400` ([#11607](https://github.com/astral-sh/ruff/pull/11607))
|
||||
- Mark `repeated-isinstance-calls` as unsafe on Python 3.10 and later ([#11622](https://github.com/astral-sh/ruff/pull/11622))
|
||||
|
||||
## 0.4.6
|
||||
|
||||
### Breaking changes
|
||||
|
||||
- Use project-relative paths when calculating GitLab fingerprints ([#11532](https://github.com/astral-sh/ruff/pull/11532))
|
||||
- Bump minimum supported Windows version to Windows 10 ([#11613](https://github.com/astral-sh/ruff/pull/11613))
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`flake8-async`\] Sleep with >24 hour interval should usually sleep forever (`ASYNC116`) ([#11498](https://github.com/astral-sh/ruff/pull/11498))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`numpy`\] Add missing functions to NumPy 2.0 migration rule ([#11528](https://github.com/astral-sh/ruff/pull/11528))
|
||||
- \[`mccabe`\] Consider irrefutable pattern similar to `if .. else` for `C901` ([#11565](https://github.com/astral-sh/ruff/pull/11565))
|
||||
- Consider `match`-`case` statements for `C901`, `PLR0912`, and `PLR0915` ([#11521](https://github.com/astral-sh/ruff/pull/11521))
|
||||
- Remove empty strings when converting to f-string (`UP032`) ([#11524](https://github.com/astral-sh/ruff/pull/11524))
|
||||
- \[`flake8-bandit`\] `request-without-timeout` should warn for `requests.request` ([#11548](https://github.com/astral-sh/ruff/pull/11548))
|
||||
- \[`flake8-self`\] Ignore sunder accesses in `flake8-self` rules ([#11546](https://github.com/astral-sh/ruff/pull/11546))
|
||||
- \[`pyupgrade`\] Lint for `TypeAliasType` usages (`UP040`) ([#11530](https://github.com/astral-sh/ruff/pull/11530))
|
||||
|
||||
### Server
|
||||
|
||||
- Respect excludes in `ruff server` configuration discovery ([#11551](https://github.com/astral-sh/ruff/pull/11551))
|
||||
- Use default settings if initialization options is empty or not provided ([#11566](https://github.com/astral-sh/ruff/pull/11566))
|
||||
- `ruff server` correctly treats `.pyi` files as stub files ([#11535](https://github.com/astral-sh/ruff/pull/11535))
|
||||
- `ruff server` searches for configuration in parent directories ([#11537](https://github.com/astral-sh/ruff/pull/11537))
|
||||
- `ruff server`: An empty code action filter no longer returns notebook source actions ([#11526](https://github.com/astral-sh/ruff/pull/11526))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- \[`flake8-logging-format`\] Fix autofix title in `logging-warn` (`G010`) ([#11514](https://github.com/astral-sh/ruff/pull/11514))
|
||||
- \[`refurb`\] Avoid recommending `operator.itemgetter` with dependence on lambda arguments ([#11574](https://github.com/astral-sh/ruff/pull/11574))
|
||||
- \[`flake8-simplify`\] Avoid recommending context manager in `__enter__` implementations ([#11575](https://github.com/astral-sh/ruff/pull/11575))
|
||||
- Create intermediary directories for `--output-file` ([#11550](https://github.com/astral-sh/ruff/pull/11550))
|
||||
- Propagate reads on global variables ([#11584](https://github.com/astral-sh/ruff/pull/11584))
|
||||
- Treat all `singledispatch` arguments as runtime-required ([#11523](https://github.com/astral-sh/ruff/pull/11523))
|
||||
|
||||
## 0.4.5
|
||||
|
||||
### Ruff's language server is now in Beta
|
||||
|
||||
`v0.4.5` marks the official Beta release of `ruff server`, an integrated language server built into Ruff.
|
||||
`ruff server` supports the same feature set as `ruff-lsp`, powering linting, formatting, and
|
||||
code fixes in Ruff's editor integrations -- but with superior performance and
|
||||
no installation required. We'd love your feedback!
|
||||
|
||||
You can enable `ruff server` in the [VS Code extension](https://github.com/astral-sh/ruff-vscode?tab=readme-ov-file#enabling-the-rust-based-language-server) today.
|
||||
|
||||
To read more about this exciting milestone, check out our [blog post](https://astral.sh/blog/ruff-v0.4.5)!
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`flake8-future-annotations`\] Reword `future-rewritable-type-annotation` (`FA100`) message ([#11381](https://github.com/astral-sh/ruff/pull/11381))
|
||||
- \[`pycodestyle`\] Consider soft keywords for `E27` rules ([#11446](https://github.com/astral-sh/ruff/pull/11446))
|
||||
- \[`pyflakes`\] Recommend adding unused import bindings to `__all__` ([#11314](https://github.com/astral-sh/ruff/pull/11314))
|
||||
- \[`pyflakes`\] Update documentation and deprecate `ignore_init_module_imports` ([#11436](https://github.com/astral-sh/ruff/pull/11436))
|
||||
- \[`pyupgrade`\] Mark quotes as unnecessary for non-evaluated annotations ([#11485](https://github.com/astral-sh/ruff/pull/11485))
|
||||
|
||||
### Formatter
|
||||
|
||||
- Avoid multiline quotes warning with `quote-style = preserve` ([#11490](https://github.com/astral-sh/ruff/pull/11490))
|
||||
|
||||
### Server
|
||||
|
||||
- Support Jupyter Notebook files ([#11206](https://github.com/astral-sh/ruff/pull/11206))
|
||||
- Support `noqa` comment code actions ([#11276](https://github.com/astral-sh/ruff/pull/11276))
|
||||
- Fix automatic configuration reloading ([#11492](https://github.com/astral-sh/ruff/pull/11492))
|
||||
- Fix several issues with configuration in Neovim and Helix ([#11497](https://github.com/astral-sh/ruff/pull/11497))
|
||||
|
||||
### CLI
|
||||
|
||||
- Add `--output-format` as a CLI option for `ruff config` ([#11438](https://github.com/astral-sh/ruff/pull/11438))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Avoid `PLE0237` for property with setter ([#11377](https://github.com/astral-sh/ruff/pull/11377))
|
||||
- Avoid `TCH005` for `if` stmt with `elif`/`else` block ([#11376](https://github.com/astral-sh/ruff/pull/11376))
|
||||
- Avoid flagging `__future__` annotations as required for non-evaluated type annotations ([#11414](https://github.com/astral-sh/ruff/pull/11414))
|
||||
- Check for ruff executable in 'bin' directory as installed by 'pip install --target'. ([#11450](https://github.com/astral-sh/ruff/pull/11450))
|
||||
- Sort edits prior to deduplicating in quotation fix ([#11452](https://github.com/astral-sh/ruff/pull/11452))
|
||||
- Treat escaped newline as valid sequence ([#11465](https://github.com/astral-sh/ruff/pull/11465))
|
||||
- \[`flake8-pie`\] Preserve parentheses in `unnecessary-dict-kwargs` ([#11372](https://github.com/astral-sh/ruff/pull/11372))
|
||||
- \[`pylint`\] Ignore `__slots__` with dynamic values ([#11488](https://github.com/astral-sh/ruff/pull/11488))
|
||||
- \[`pylint`\] Remove `try` body from branch counting ([#11487](https://github.com/astral-sh/ruff/pull/11487))
|
||||
- \[`refurb`\] Respect operator precedence in `FURB110` ([#11464](https://github.com/astral-sh/ruff/pull/11464))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add `--preview` to the README ([#11395](https://github.com/astral-sh/ruff/pull/11395))
|
||||
- Add Python 3.13 to list of allowed Python versions ([#11411](https://github.com/astral-sh/ruff/pull/11411))
|
||||
- Simplify Neovim setup documentation ([#11489](https://github.com/astral-sh/ruff/pull/11489))
|
||||
- Update CONTRIBUTING.md to reflect the new parser ([#11434](https://github.com/astral-sh/ruff/pull/11434))
|
||||
- Update server documentation with new migration guide ([#11499](https://github.com/astral-sh/ruff/pull/11499))
|
||||
- \[`pycodestyle`\] Clarify motivation for `E713` and `E714` ([#11483](https://github.com/astral-sh/ruff/pull/11483))
|
||||
- \[`pyflakes`\] Update docs to describe WAI behavior (F541) ([#11362](https://github.com/astral-sh/ruff/pull/11362))
|
||||
- \[`pylint`\] Clearly indicate what is counted as a branch ([#11423](https://github.com/astral-sh/ruff/pull/11423))
|
||||
|
||||
## 0.4.4
|
||||
|
||||
### Preview features
|
||||
@@ -199,10 +74,6 @@ To read more about this exciting milestone, check out our [blog post](https://as
|
||||
- Avoid allocations for isort module names ([#11251](https://github.com/astral-sh/ruff/pull/11251))
|
||||
- Build a separate ARM wheel for macOS ([#11149](https://github.com/astral-sh/ruff/pull/11149))
|
||||
|
||||
### Windows
|
||||
|
||||
- Increase the minimum requirement to Windows 10.
|
||||
|
||||
## 0.4.2
|
||||
|
||||
### Rule changes
|
||||
|
||||
@@ -101,8 +101,6 @@ pre-commit run --all-files --show-diff-on-failure # Rust and Python formatting,
|
||||
These checks will run on GitHub Actions when you open your pull request, but running them locally
|
||||
will save you time and expedite the merge process.
|
||||
|
||||
If you're using VS Code, you can also install the recommended [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) extension to get these checks while editing.
|
||||
|
||||
Note that many code changes also require updating the snapshot tests, which is done interactively
|
||||
after running `cargo test` like so:
|
||||
|
||||
|
||||
105
Cargo.lock
generated
105
Cargo.lock
generated
@@ -129,9 +129,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.86"
|
||||
version = "1.0.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
|
||||
checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3"
|
||||
|
||||
[[package]]
|
||||
name = "argfile"
|
||||
@@ -886,6 +886,12 @@ version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
||||
|
||||
[[package]]
|
||||
name = "hexf-parse"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
|
||||
|
||||
[[package]]
|
||||
name = "home"
|
||||
version = "0.5.9"
|
||||
@@ -1171,10 +1177,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.155"
|
||||
name = "lexical-parse-float"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
|
||||
checksum = "683b3a5ebd0130b8fb52ba0bdc718cc56815b6a097e28ae5a6997d0ad17dc05f"
|
||||
dependencies = [
|
||||
"lexical-parse-integer",
|
||||
"lexical-util",
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lexical-parse-integer"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d0994485ed0c312f6d965766754ea177d07f9c00c9b82a5ee62ed5b47945ee9"
|
||||
dependencies = [
|
||||
"lexical-util",
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lexical-util"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5255b9ff16ff898710eb9eb63cb39248ea8a5bb036bea8085b1a767ff6c4e3fc"
|
||||
dependencies = [
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.154"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346"
|
||||
|
||||
[[package]]
|
||||
name = "libcst"
|
||||
@@ -1203,9 +1239,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libmimalloc-sys"
|
||||
version = "0.1.38"
|
||||
version = "0.1.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e7bb23d733dfcc8af652a78b7bf232f0e967710d044732185e561e47c0336b6"
|
||||
checksum = "81eb4061c0582dedea1cbc7aff2240300dd6982e0239d1c99e65c1dbf4a30ba7"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -1264,7 +1300,8 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "lsp-types"
|
||||
version = "0.95.1"
|
||||
source = "git+https://github.com/astral-sh/lsp-types.git?rev=3512a9f#3512a9f33eadc5402cfab1b8f7340824c8ca1439"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e34d33a8e9b006cd3fc4fe69a921affa097bae4bb65f76271f4644f9a334365"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"serde",
|
||||
@@ -1302,9 +1339,9 @@ checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
|
||||
|
||||
[[package]]
|
||||
name = "mimalloc"
|
||||
version = "0.1.42"
|
||||
version = "0.1.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e9186d86b79b52f4a77af65604b51225e8db1d6ee7e3f41aec1e40829c71a176"
|
||||
checksum = "9f41a2280ded0da56c8cf898babb86e8f10651a34adcfff190ae9a1159c6908d"
|
||||
dependencies = [
|
||||
"libmimalloc-sys",
|
||||
]
|
||||
@@ -1461,9 +1498,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.3"
|
||||
version = "0.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
|
||||
checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
"parking_lot_core",
|
||||
@@ -1670,9 +1707,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.84"
|
||||
version = "1.0.82"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec96c6a92621310b51366f1e28d05ef11489516e93be030060e5fc12024a49d6"
|
||||
checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -1903,7 +1940,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.4.7"
|
||||
version = "0.4.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -2064,7 +2101,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.4.7"
|
||||
version = "0.4.4"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"annotate-snippets 0.9.2",
|
||||
@@ -2241,7 +2278,9 @@ name = "ruff_python_literal"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
"hexf-parse",
|
||||
"itertools 0.12.1",
|
||||
"lexical-parse-float",
|
||||
"ruff_python_ast",
|
||||
"unic-ucd-category",
|
||||
]
|
||||
@@ -2329,7 +2368,6 @@ version = "0.2.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"crossbeam",
|
||||
"globset",
|
||||
"insta",
|
||||
"jod-thread",
|
||||
"libc",
|
||||
@@ -2339,7 +2377,6 @@ dependencies = [
|
||||
"ruff_diagnostics",
|
||||
"ruff_formatter",
|
||||
"ruff_linter",
|
||||
"ruff_notebook",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_codegen",
|
||||
"ruff_python_formatter",
|
||||
@@ -2518,9 +2555,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "0.8.21"
|
||||
version = "0.8.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92"
|
||||
checksum = "fc6e7ed6919cb46507fb01ff1654309219f62b4d603822501b0b80d42f6f21ef"
|
||||
dependencies = [
|
||||
"dyn-clone",
|
||||
"schemars_derive",
|
||||
@@ -2530,9 +2567,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "schemars_derive"
|
||||
version = "0.8.21"
|
||||
version = "0.8.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e"
|
||||
checksum = "185f2b7aa7e02d418e453790dde16890256bbd2bcd04b7dc5348811052b53f49"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2560,9 +2597,9 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.203"
|
||||
version = "1.0.201"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094"
|
||||
checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
@@ -2580,9 +2617,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.203"
|
||||
version = "1.0.201"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
|
||||
checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2707,9 +2744,9 @@ checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
|
||||
|
||||
[[package]]
|
||||
name = "smol_str"
|
||||
version = "0.2.2"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead"
|
||||
checksum = "e6845563ada680337a52d43bb0b29f396f2d911616f6573012645b9e3d048a49"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -2777,9 +2814,9 @@ checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.66"
|
||||
version = "2.0.63"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5"
|
||||
checksum = "bf5be731623ca1a1fb7d8be6f261a3be6d3e2337b8a1f97be944d020c8fcb704"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2867,18 +2904,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.61"
|
||||
version = "1.0.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
|
||||
checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.61"
|
||||
version = "1.0.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
|
||||
checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
@@ -62,6 +62,7 @@ filetime = { version = "0.2.23" }
|
||||
glob = { version = "0.3.1" }
|
||||
globset = { version = "0.4.14" }
|
||||
hashbrown = "0.14.3"
|
||||
hexf-parse = { version = "0.2.1" }
|
||||
ignore = { version = "0.4.22" }
|
||||
imara-diff = { version = "0.1.5" }
|
||||
imperative = { version = "1.0.4" }
|
||||
@@ -75,11 +76,12 @@ is-wsl = { version = "0.4.0" }
|
||||
itertools = { version = "0.12.1" }
|
||||
js-sys = { version = "0.3.69" }
|
||||
jod-thread = { version = "0.1.2" }
|
||||
lexical-parse-float = { version = "0.8.0", features = ["format"] }
|
||||
libc = { version = "0.2.153" }
|
||||
libcst = { version = "1.1.0", default-features = false }
|
||||
log = { version = "0.4.17" }
|
||||
lsp-server = { version = "0.7.6" }
|
||||
lsp-types = { git = "https://github.com/astral-sh/lsp-types.git", rev = "3512a9f", features = ["proposed"] }
|
||||
lsp-types = { version = "0.95.0", features = ["proposed"] }
|
||||
matchit = { version = "0.8.1" }
|
||||
memchr = { version = "2.7.1" }
|
||||
mimalloc = { version = "0.1.39" }
|
||||
|
||||
@@ -152,7 +152,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.4.7
|
||||
rev: v0.4.4
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
@@ -408,7 +408,6 @@ Ruff is used by a number of major open-source projects and companies, including:
|
||||
- [Dagster](https://github.com/dagster-io/dagster)
|
||||
- Databricks ([MLflow](https://github.com/mlflow/mlflow))
|
||||
- [FastAPI](https://github.com/tiangolo/fastapi)
|
||||
- [Godot](https://github.com/godotengine/godot)
|
||||
- [Gradio](https://github.com/gradio-app/gradio)
|
||||
- [Great Expectations](https://github.com/great-expectations/great_expectations)
|
||||
- [HTTPX](https://github.com/encode/httpx)
|
||||
@@ -434,7 +433,6 @@ Ruff is used by a number of major open-source projects and companies, including:
|
||||
- Modern Treasury ([Python SDK](https://github.com/Modern-Treasury/modern-treasury-python))
|
||||
- Mozilla ([Firefox](https://github.com/mozilla/gecko-dev))
|
||||
- [Mypy](https://github.com/python/mypy)
|
||||
- [Nautobot](https://github.com/nautobot/nautobot)
|
||||
- Netflix ([Dispatch](https://github.com/Netflix/dispatch))
|
||||
- [Neon](https://github.com/neondatabase/neon)
|
||||
- [Nokia](https://nokia.com/)
|
||||
|
||||
@@ -275,7 +275,10 @@ pub struct TypedNodeKey<N: AstNode> {
|
||||
|
||||
impl<N: AstNode> TypedNodeKey<N> {
|
||||
pub fn from_node(node: &N) -> Self {
|
||||
let inner = NodeKey::from_node(node.as_any_node_ref());
|
||||
let inner = NodeKey {
|
||||
kind: node.as_any_node_ref().kind(),
|
||||
range: node.range(),
|
||||
};
|
||||
Self {
|
||||
inner,
|
||||
_marker: PhantomData,
|
||||
@@ -349,12 +352,6 @@ pub struct NodeKey {
|
||||
}
|
||||
|
||||
impl NodeKey {
|
||||
pub fn from_node(node: AnyNodeRef) -> Self {
|
||||
NodeKey {
|
||||
kind: node.kind(),
|
||||
range: node.range(),
|
||||
}
|
||||
}
|
||||
pub fn resolve<'a>(&self, root: AnyNodeRef<'a>) -> Option<AnyNodeRef<'a>> {
|
||||
// We need to do a binary search here. Only traverse into a node if the range is withint the node
|
||||
let mut visitor = FindNodeKeyVisitor {
|
||||
|
||||
@@ -12,15 +12,11 @@ use crate::files::FileId;
|
||||
use crate::symbols::Dependency;
|
||||
use crate::FxDashMap;
|
||||
|
||||
/// Representation of a Python module.
|
||||
///
|
||||
/// The inner type wrapped by this struct is a unique identifier for the module
|
||||
/// that is used by the struct's methods to lazily query information about the module.
|
||||
/// ID uniquely identifying a module.
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub struct Module(u32);
|
||||
|
||||
impl Module {
|
||||
/// Return the absolute name of the module (e.g. `foo.bar`)
|
||||
pub fn name(&self, db: &dyn SemanticDb) -> QueryResult<ModuleName> {
|
||||
let jar: &SemanticJar = db.jar()?;
|
||||
let modules = &jar.module_resolver;
|
||||
@@ -28,7 +24,6 @@ impl Module {
|
||||
Ok(modules.modules.get(self).unwrap().name.clone())
|
||||
}
|
||||
|
||||
/// Return the path to the source code that defines this module
|
||||
pub fn path(&self, db: &dyn SemanticDb) -> QueryResult<ModulePath> {
|
||||
let jar: &SemanticJar = db.jar()?;
|
||||
let modules = &jar.module_resolver;
|
||||
@@ -36,7 +31,6 @@ impl Module {
|
||||
Ok(modules.modules.get(self).unwrap().path.clone())
|
||||
}
|
||||
|
||||
/// Determine whether this module is a single-file module or a package
|
||||
pub fn kind(&self, db: &dyn SemanticDb) -> QueryResult<ModuleKind> {
|
||||
let jar: &SemanticJar = db.jar()?;
|
||||
let modules = &jar.module_resolver;
|
||||
@@ -44,16 +38,6 @@ impl Module {
|
||||
Ok(modules.modules.get(self).unwrap().kind)
|
||||
}
|
||||
|
||||
/// Attempt to resolve a dependency of this module to an absolute [`ModuleName`].
|
||||
///
|
||||
/// A dependency could be either absolute (e.g. the `foo` dependency implied by `from foo import bar`)
|
||||
/// or relative to this module (e.g. the `.foo` dependency implied by `from .foo import bar`)
|
||||
///
|
||||
/// - Returns an error if the query failed.
|
||||
/// - Returns `Ok(None)` if the query succeeded,
|
||||
/// but the dependency refers to a module that does not exist.
|
||||
/// - Returns `Ok(Some(ModuleName))` if the query succeeded,
|
||||
/// and the dependency refers to a module that exists.
|
||||
pub fn resolve_dependency(
|
||||
&self,
|
||||
db: &dyn SemanticDb,
|
||||
@@ -103,8 +87,7 @@ impl Module {
|
||||
|
||||
/// A module name, e.g. `foo.bar`.
|
||||
///
|
||||
/// Always normalized to the absolute form
|
||||
/// (never a relative module name, i.e., never `.foo`).
|
||||
/// Always normalized to the absolute form (never a relative module name).
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub struct ModuleName(smol_str::SmolStr);
|
||||
|
||||
@@ -141,13 +124,10 @@ impl ModuleName {
|
||||
Some(Self(name))
|
||||
}
|
||||
|
||||
/// An iterator over the components of the module name:
|
||||
/// `foo.bar.baz` -> `foo`, `bar`, `baz`
|
||||
pub fn components(&self) -> impl DoubleEndedIterator<Item = &str> {
|
||||
self.0.split('.')
|
||||
}
|
||||
|
||||
/// The name of this module's immediate parent, if it has a parent
|
||||
pub fn parent(&self) -> Option<ModuleName> {
|
||||
let (_, parent) = self.0.rsplit_once('.')?;
|
||||
|
||||
@@ -179,10 +159,9 @@ impl std::fmt::Display for ModuleName {
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub enum ModuleKind {
|
||||
/// A single-file module (e.g. `foo.py` or `foo.pyi`)
|
||||
Module,
|
||||
|
||||
/// A python package (`foo/__init__.py` or `foo/__init__.pyi`)
|
||||
/// A python package (a `__init__.py` or `__init__.pyi` file)
|
||||
Package,
|
||||
}
|
||||
|
||||
@@ -202,12 +181,10 @@ impl ModuleSearchPath {
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine whether this is a first-party, third-party or standard-library search path
|
||||
pub fn kind(&self) -> ModuleSearchPathKind {
|
||||
self.inner.kind
|
||||
}
|
||||
|
||||
/// Return the location of the search path on the file system
|
||||
pub fn path(&self) -> &Path {
|
||||
&self.inner.path
|
||||
}
|
||||
@@ -254,11 +231,9 @@ pub struct ModuleData {
|
||||
// Queries
|
||||
//////////////////////////////////////////////////////
|
||||
|
||||
/// Resolves a module name to a module.
|
||||
///
|
||||
/// TODO: This would not work with Salsa because `ModuleName` isn't an ingredient
|
||||
/// and, therefore, cannot be used as part of a query.
|
||||
/// For this to work with salsa, it would be necessary to intern all `ModuleName`s.
|
||||
/// Resolves a module name to a module id
|
||||
/// TODO: This would not work with Salsa because `ModuleName` isn't an ingredient and, therefore, cannot be used as part of a query.
|
||||
/// For this to work with salsa, it would be necessary to intern all `ModuleName`s.
|
||||
#[tracing::instrument(level = "debug", skip(db))]
|
||||
pub fn resolve_module(db: &dyn SemanticDb, name: ModuleName) -> QueryResult<Option<Module>> {
|
||||
let jar: &SemanticJar = db.jar()?;
|
||||
@@ -280,7 +255,7 @@ pub fn resolve_module(db: &dyn SemanticDb, name: ModuleName) -> QueryResult<Opti
|
||||
let file_id = db.file_id(&normalized);
|
||||
let path = ModulePath::new(root_path.clone(), file_id);
|
||||
|
||||
let module = Module(
|
||||
let id = Module(
|
||||
modules
|
||||
.next_module_id
|
||||
.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
|
||||
@@ -288,7 +263,7 @@ pub fn resolve_module(db: &dyn SemanticDb, name: ModuleName) -> QueryResult<Opti
|
||||
|
||||
modules
|
||||
.modules
|
||||
.insert(module, Arc::from(ModuleData { name, path, kind }));
|
||||
.insert(id, Arc::from(ModuleData { name, path, kind }));
|
||||
|
||||
// A path can map to multiple modules because of symlinks:
|
||||
// ```
|
||||
@@ -297,33 +272,33 @@ pub fn resolve_module(db: &dyn SemanticDb, name: ModuleName) -> QueryResult<Opti
|
||||
// ```
|
||||
// Here, both `foo` and `bar` resolve to the same module but through different paths.
|
||||
// That's why we need to insert the absolute path and not the normalized path here.
|
||||
let absolute_file_id = if absolute_path == normalized {
|
||||
let absolute_id = if absolute_path == normalized {
|
||||
file_id
|
||||
} else {
|
||||
db.file_id(&absolute_path)
|
||||
};
|
||||
|
||||
modules.by_file.insert(absolute_file_id, module);
|
||||
modules.by_file.insert(absolute_id, id);
|
||||
|
||||
entry.insert_entry(module);
|
||||
entry.insert_entry(id);
|
||||
|
||||
Ok(Some(module))
|
||||
Ok(Some(id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves the module for the given path.
|
||||
/// Resolves the module id for the given path.
|
||||
///
|
||||
/// Returns `None` if the path is not a module locatable via `sys.path`.
|
||||
/// Returns `None` if the path is not a module in `sys.path`.
|
||||
#[tracing::instrument(level = "debug", skip(db))]
|
||||
pub fn path_to_module(db: &dyn SemanticDb, path: &Path) -> QueryResult<Option<Module>> {
|
||||
let file = db.file_id(path);
|
||||
file_to_module(db, file)
|
||||
}
|
||||
|
||||
/// Resolves the module for the file with the given id.
|
||||
/// Resolves the module id for the file with the given id.
|
||||
///
|
||||
/// Returns `None` if the file is not a module locatable via `sys.path`.
|
||||
/// Returns `None` if the file is not a module in `sys.path`.
|
||||
#[tracing::instrument(level = "debug", skip(db))]
|
||||
pub fn file_to_module(db: &dyn SemanticDb, file: FileId) -> QueryResult<Option<Module>> {
|
||||
let jar: &SemanticJar = db.jar()?;
|
||||
@@ -350,12 +325,12 @@ pub fn file_to_module(db: &dyn SemanticDb, file: FileId) -> QueryResult<Option<M
|
||||
|
||||
// Resolve the module name to see if Python would resolve the name to the same path.
|
||||
// If it doesn't, then that means that multiple modules have the same in different
|
||||
// root paths, but that the module corresponding to the past path is in a lower priority search path,
|
||||
// root paths, but that the module corresponding to the past path is in a lower priority path,
|
||||
// in which case we ignore it.
|
||||
let Some(module) = resolve_module(db, module_name)? else {
|
||||
let Some(module_id) = resolve_module(db, module_name)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
let module_path = module.path(db)?;
|
||||
let module_path = module_id.path(db)?;
|
||||
|
||||
if module_path.root() == &root_path {
|
||||
let Ok(normalized) = path.canonicalize() else {
|
||||
@@ -375,7 +350,7 @@ pub fn file_to_module(db: &dyn SemanticDb, file: FileId) -> QueryResult<Option<M
|
||||
}
|
||||
|
||||
// Path has been inserted by `resolved`
|
||||
Ok(Some(module))
|
||||
Ok(Some(module_id))
|
||||
} else {
|
||||
// This path is for a module with the same name but in a module search path with a lower priority.
|
||||
// Ignore it.
|
||||
@@ -394,22 +369,19 @@ pub fn set_module_search_paths(db: &mut dyn SemanticDb, search_paths: Vec<Module
|
||||
jar.module_resolver = ModuleResolver::new(search_paths);
|
||||
}
|
||||
|
||||
/// Adds a module located at `path` to the resolver.
|
||||
/// Adds a module to the resolver.
|
||||
///
|
||||
/// Returns `None` if the path doesn't resolve to a module.
|
||||
///
|
||||
/// Returns `Some(module, other_modules)`, where `module` is the resolved module
|
||||
/// with file location `path`, and `other_modules` is a `Vec` of `ModuleData` instances.
|
||||
/// Each element in `other_modules` provides information regarding a single module that needs
|
||||
/// re-resolving because it was part of a namespace package and might now resolve differently.
|
||||
///
|
||||
/// Returns `Some` with the id of the module and the ids of the modules that need re-resolving
|
||||
/// because they were part of a namespace package and might now resolve differently.
|
||||
/// Note: This won't work with salsa because `Path` is not an ingredient.
|
||||
pub fn add_module(db: &mut dyn SemanticDb, path: &Path) -> Option<(Module, Vec<Arc<ModuleData>>)> {
|
||||
// No locking is required because we're holding a mutable reference to `modules`.
|
||||
|
||||
// TODO This needs tests
|
||||
|
||||
// Note: Intentionally bypass caching here. Module should not be in the cache yet.
|
||||
// Note: Intentionally by-pass caching here. Module should not be in the cache yet.
|
||||
let module = path_to_module(db, path).ok()??;
|
||||
|
||||
// The code below is to handle the addition of `__init__.py` files.
|
||||
@@ -433,15 +405,15 @@ pub fn add_module(db: &mut dyn SemanticDb, path: &Path) -> Option<(Module, Vec<A
|
||||
let jar: &mut SemanticJar = db.jar_mut();
|
||||
let modules = &mut jar.module_resolver;
|
||||
|
||||
modules.by_file.retain(|_, module| {
|
||||
modules.by_file.retain(|_, id| {
|
||||
if modules
|
||||
.modules
|
||||
.get(module)
|
||||
.get(id)
|
||||
.unwrap()
|
||||
.name
|
||||
.starts_with(&parent_name)
|
||||
{
|
||||
to_remove.push(*module);
|
||||
to_remove.push(*id);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
@@ -450,8 +422,8 @@ pub fn add_module(db: &mut dyn SemanticDb, path: &Path) -> Option<(Module, Vec<A
|
||||
|
||||
// TODO remove need for this vec
|
||||
let mut removed = Vec::with_capacity(to_remove.len());
|
||||
for module in &to_remove {
|
||||
removed.push(modules.remove_module(*module));
|
||||
for id in &to_remove {
|
||||
removed.push(modules.remove_module_by_id(*id));
|
||||
}
|
||||
|
||||
Some((module, removed))
|
||||
@@ -464,10 +436,10 @@ pub struct ModuleResolver {
|
||||
|
||||
// Locking: Locking is done by acquiring a (write) lock on `by_name`. This is because `by_name` is the primary
|
||||
// lookup method. Acquiring locks in any other ordering can result in deadlocks.
|
||||
/// Looks up a module by name
|
||||
/// Resolves a module name to it's module id.
|
||||
by_name: FxDashMap<ModuleName, Module>,
|
||||
|
||||
/// A map of all known modules to data about those modules
|
||||
/// All known modules, indexed by the module id.
|
||||
modules: FxDashMap<Module, Arc<ModuleData>>,
|
||||
|
||||
/// Lookup from absolute path to module.
|
||||
@@ -487,27 +459,24 @@ impl ModuleResolver {
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a module from the inner cache
|
||||
pub(crate) fn remove_module_by_file(&mut self, file_id: FileId) {
|
||||
pub(crate) fn remove_module(&mut self, file_id: FileId) {
|
||||
// No locking is required because we're holding a mutable reference to `self`.
|
||||
let Some((_, module)) = self.by_file.remove(&file_id) else {
|
||||
let Some((_, id)) = self.by_file.remove(&file_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.remove_module(module);
|
||||
self.remove_module_by_id(id);
|
||||
}
|
||||
|
||||
fn remove_module(&mut self, module: Module) -> Arc<ModuleData> {
|
||||
let (_, module_data) = self.modules.remove(&module).unwrap();
|
||||
fn remove_module_by_id(&mut self, id: Module) -> Arc<ModuleData> {
|
||||
let (_, module) = self.modules.remove(&id).unwrap();
|
||||
|
||||
self.by_name.remove(&module_data.name).unwrap();
|
||||
self.by_name.remove(&module.name).unwrap();
|
||||
|
||||
// It's possible that multiple paths map to the same module.
|
||||
// Search all other paths referencing the same module.
|
||||
self.by_file
|
||||
.retain(|_, current_module| *current_module != module);
|
||||
// It's possible that multiple paths map to the same id. Search all other paths referencing the same module id.
|
||||
self.by_file.retain(|_, current_id| *current_id != id);
|
||||
|
||||
module_data
|
||||
module
|
||||
}
|
||||
}
|
||||
|
||||
@@ -536,19 +505,15 @@ impl ModulePath {
|
||||
Self { root, file_id }
|
||||
}
|
||||
|
||||
/// The search path that was used to locate the module
|
||||
pub fn root(&self) -> &ModuleSearchPath {
|
||||
&self.root
|
||||
}
|
||||
|
||||
/// The file containing the source code for the module
|
||||
pub fn file(&self) -> FileId {
|
||||
self.file_id
|
||||
}
|
||||
}
|
||||
|
||||
/// Given a module name and a list of search paths in which to lookup modules,
|
||||
/// attempt to resolve the module name
|
||||
fn resolve_name(
|
||||
name: &ModuleName,
|
||||
search_paths: &[ModuleSearchPath],
|
||||
@@ -670,9 +635,7 @@ enum PackageKind {
|
||||
/// A root package or module. E.g. `foo` in `foo.bar.baz` or just `foo`.
|
||||
Root,
|
||||
|
||||
/// A regular sub-package where the parent contains an `__init__.py`.
|
||||
///
|
||||
/// For example, `bar` in `foo.bar` when the `foo` directory contains an `__init__.py`.
|
||||
/// A regular sub-package where the parent contains an `__init__.py`. For example `bar` in `foo.bar` when the `foo` directory contains an `__init__.py`.
|
||||
Regular,
|
||||
|
||||
/// A sub-package in a namespace package. A namespace package is a package without an `__init__.py`.
|
||||
|
||||
@@ -42,7 +42,7 @@ impl Program {
|
||||
|
||||
let (source, semantic, lint) = self.jars_mut();
|
||||
for change in aggregated_changes.iter() {
|
||||
semantic.module_resolver.remove_module_by_file(change.id);
|
||||
semantic.module_resolver.remove_module(change.id);
|
||||
semantic.symbol_tables.remove(&change.id);
|
||||
source.sources.remove(&change.id);
|
||||
source.parsed.remove(&change.id);
|
||||
|
||||
@@ -236,7 +236,6 @@ pub struct SymbolTable {
|
||||
scopes_by_node: FxHashMap<NodeKey, ScopeId>,
|
||||
/// dependencies of this module
|
||||
dependencies: Vec<Dependency>,
|
||||
flow_graph: FlowGraph,
|
||||
}
|
||||
|
||||
impl SymbolTable {
|
||||
@@ -246,7 +245,6 @@ impl SymbolTable {
|
||||
table: SymbolTable::new(),
|
||||
scopes: vec![root_scope_id],
|
||||
current_definition: None,
|
||||
current_flow_node: FlowGraph::start(),
|
||||
};
|
||||
builder.visit_body(&module.body);
|
||||
builder.table
|
||||
@@ -259,7 +257,6 @@ impl SymbolTable {
|
||||
defs: FxHashMap::default(),
|
||||
scopes_by_node: FxHashMap::default(),
|
||||
dependencies: Vec::new(),
|
||||
flow_graph: FlowGraph::new(),
|
||||
};
|
||||
table.scopes_by_id.push(Scope {
|
||||
name: Name::new("<module>"),
|
||||
@@ -277,23 +274,6 @@ impl SymbolTable {
|
||||
&self.dependencies
|
||||
}
|
||||
|
||||
/// Return an iterator over all definitions of `symbol_id` reachable from `use_expr`. The value
|
||||
/// of `symbol_id` in `use_expr` must originate from one of the iterated definitions (or from
|
||||
/// an external reassignment of the name outside of this scope).
|
||||
pub(crate) fn reachable_definitions(
|
||||
&self,
|
||||
symbol_id: SymbolId,
|
||||
use_expr: &ast::Expr,
|
||||
) -> ReachableDefinitionsIterator {
|
||||
let node_key = NodeKey::from_node(use_expr.into());
|
||||
let flow_node_id = self.flow_graph.ast_to_flow[&node_key];
|
||||
ReachableDefinitionsIterator {
|
||||
table: self,
|
||||
flow_node_id,
|
||||
symbol_id,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn root_scope_id() -> ScopeId {
|
||||
ScopeId::from_usize(0)
|
||||
}
|
||||
@@ -543,72 +523,11 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct ReachableDefinitionsIterator<'a> {
|
||||
table: &'a SymbolTable,
|
||||
flow_node_id: FlowNodeId,
|
||||
symbol_id: SymbolId,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for ReachableDefinitionsIterator<'a> {
|
||||
type Item = Definition;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
loop {
|
||||
match &self.table.flow_graph.flow_nodes_by_id[self.flow_node_id] {
|
||||
FlowNode::Start => return None,
|
||||
FlowNode::Definition(def_node) => {
|
||||
self.flow_node_id = def_node.predecessor;
|
||||
if def_node.symbol_id == self.symbol_id {
|
||||
return Some(def_node.definition.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> FusedIterator for ReachableDefinitionsIterator<'a> {}
|
||||
|
||||
#[newtype_index]
|
||||
struct FlowNodeId;
|
||||
|
||||
#[derive(Debug)]
|
||||
enum FlowNode {
|
||||
Start,
|
||||
Definition(DefinitionFlowNode),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct DefinitionFlowNode {
|
||||
symbol_id: SymbolId,
|
||||
definition: Definition,
|
||||
predecessor: FlowNodeId,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct FlowGraph {
|
||||
flow_nodes_by_id: IndexVec<FlowNodeId, FlowNode>,
|
||||
ast_to_flow: FxHashMap<NodeKey, FlowNodeId>,
|
||||
}
|
||||
|
||||
impl FlowGraph {
|
||||
fn new() -> Self {
|
||||
let mut graph = FlowGraph::default();
|
||||
graph.flow_nodes_by_id.push(FlowNode::Start);
|
||||
graph
|
||||
}
|
||||
|
||||
fn start() -> FlowNodeId {
|
||||
FlowNodeId::from_usize(0)
|
||||
}
|
||||
}
|
||||
|
||||
struct SymbolTableBuilder {
|
||||
table: SymbolTable,
|
||||
scopes: Vec<ScopeId>,
|
||||
/// the definition whose target(s) we are currently walking
|
||||
current_definition: Option<Definition>,
|
||||
current_flow_node: FlowNodeId,
|
||||
}
|
||||
|
||||
impl SymbolTableBuilder {
|
||||
@@ -627,16 +546,7 @@ impl SymbolTableBuilder {
|
||||
.defs
|
||||
.entry(symbol_id)
|
||||
.or_default()
|
||||
.push(definition.clone());
|
||||
self.current_flow_node = self
|
||||
.table
|
||||
.flow_graph
|
||||
.flow_nodes_by_id
|
||||
.push(FlowNode::Definition(DefinitionFlowNode {
|
||||
definition,
|
||||
symbol_id,
|
||||
predecessor: self.current_flow_node,
|
||||
}));
|
||||
.push(definition);
|
||||
symbol_id
|
||||
}
|
||||
|
||||
@@ -651,7 +561,6 @@ impl SymbolTableBuilder {
|
||||
self.table
|
||||
.add_child_scope(self.cur_scope(), name, kind, definition, defining_symbol);
|
||||
self.scopes.push(scope_id);
|
||||
self.current_flow_node = FlowGraph::start();
|
||||
scope_id
|
||||
}
|
||||
|
||||
@@ -715,10 +624,6 @@ impl PreorderVisitor<'_> for SymbolTableBuilder {
|
||||
}
|
||||
}
|
||||
}
|
||||
self.table
|
||||
.flow_graph
|
||||
.ast_to_flow
|
||||
.insert(NodeKey::from_node(expr.into()), self.current_flow_node);
|
||||
ast::visitor::preorder::walk_expr(self, expr);
|
||||
}
|
||||
|
||||
@@ -861,13 +766,15 @@ impl DerefMut for SymbolTablesStorage {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::symbols::{ScopeKind, SymbolFlags, SymbolTable};
|
||||
use textwrap::dedent;
|
||||
|
||||
use crate::parse::Parsed;
|
||||
use crate::symbols::ScopeKind;
|
||||
|
||||
use super::{SymbolFlags, SymbolId, SymbolIterator, SymbolTable};
|
||||
|
||||
mod from_ast {
|
||||
use crate::parse::Parsed;
|
||||
use crate::symbols::{Definition, ScopeKind, SymbolId, SymbolIterator, SymbolTable};
|
||||
use ruff_python_ast as ast;
|
||||
use textwrap::dedent;
|
||||
use super::*;
|
||||
|
||||
fn parse(code: &str) -> Parsed {
|
||||
Parsed::from_text(&dedent(code))
|
||||
@@ -1114,35 +1021,6 @@ mod tests {
|
||||
assert_eq!(func_scope.name(), "C");
|
||||
assert_eq!(names(table.symbols_for_scope(func_scope_id)), vec!["x"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reachability_trivial() {
|
||||
let parsed = parse("x = 1; x");
|
||||
let ast = parsed.ast();
|
||||
let table = SymbolTable::from_ast(ast);
|
||||
let x_sym = table
|
||||
.root_symbol_id_by_name("x")
|
||||
.expect("x symbol should exist");
|
||||
let ast::Stmt::Expr(ast::StmtExpr { value: x_use, .. }) = &ast.body[1] else {
|
||||
panic!("should be an expr")
|
||||
};
|
||||
let x_defs: Vec<_> = table.reachable_definitions(x_sym, x_use).collect();
|
||||
assert_eq!(x_defs.len(), 1);
|
||||
let Definition::Assignment(node_key) = &x_defs[0] else {
|
||||
panic!("def should be an assignment")
|
||||
};
|
||||
let Some(def_node) = node_key.resolve(ast.into()) else {
|
||||
panic!("node key should resolve")
|
||||
};
|
||||
let ast::Expr::NumberLiteral(ast::ExprNumberLiteral {
|
||||
value: ast::Number::Int(num),
|
||||
..
|
||||
}) = &*def_node.value
|
||||
else {
|
||||
panic!("should be a number literal")
|
||||
};
|
||||
assert_eq!(*num, 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -2,10 +2,7 @@
|
||||
use crate::ast_ids::NodeKey;
|
||||
use crate::db::{QueryResult, SemanticDb, SemanticJar};
|
||||
use crate::files::FileId;
|
||||
use crate::module::{Module, ModuleName};
|
||||
use crate::symbols::{
|
||||
resolve_global_symbol, symbol_table, GlobalSymbolId, ScopeId, ScopeKind, SymbolId,
|
||||
};
|
||||
use crate::symbols::{symbol_table, GlobalSymbolId, ScopeId, ScopeKind, SymbolId};
|
||||
use crate::{FxDashMap, FxIndexSet, Name};
|
||||
use ruff_index::{newtype_index, IndexVec};
|
||||
use rustc_hash::FxHashMap;
|
||||
@@ -28,15 +25,12 @@ pub enum Type {
|
||||
Unbound,
|
||||
/// a specific function object
|
||||
Function(FunctionTypeId),
|
||||
/// a specific module object
|
||||
Module(ModuleTypeId),
|
||||
/// a specific class object
|
||||
Class(ClassTypeId),
|
||||
/// the set of Python objects with the given class in their __class__'s method resolution order
|
||||
Instance(ClassTypeId),
|
||||
Union(UnionTypeId),
|
||||
Intersection(IntersectionTypeId),
|
||||
IntLiteral(i64),
|
||||
// TODO protocols, callable types, overloads, generics, type vars
|
||||
}
|
||||
|
||||
@@ -52,39 +46,6 @@ impl Type {
|
||||
pub const fn is_unknown(&self) -> bool {
|
||||
matches!(self, Type::Unknown)
|
||||
}
|
||||
|
||||
pub fn get_member(&self, db: &dyn SemanticDb, name: &Name) -> QueryResult<Option<Type>> {
|
||||
match self {
|
||||
Type::Any => todo!("attribute lookup on Any type"),
|
||||
Type::Never => todo!("attribute lookup on Never type"),
|
||||
Type::Unknown => todo!("attribute lookup on Unknown type"),
|
||||
Type::Unbound => todo!("attribute lookup on Unbound type"),
|
||||
Type::Function(_) => todo!("attribute lookup on Function type"),
|
||||
Type::Module(module_id) => module_id.get_member(db, name),
|
||||
Type::Class(class_id) => class_id.get_class_member(db, name),
|
||||
Type::Instance(_) => {
|
||||
// TODO MRO? get_own_instance_member, get_instance_member
|
||||
todo!("attribute lookup on Instance type")
|
||||
}
|
||||
Type::Union(union_id) => {
|
||||
let jar: &SemanticJar = db.jar()?;
|
||||
let _todo_union_ref = jar.type_store.get_union(*union_id);
|
||||
// TODO perform the get_member on each type in the union
|
||||
// TODO return the union of those results
|
||||
// TODO if any of those results is `None` then include Unknown in the result union
|
||||
todo!("attribute lookup on Union type")
|
||||
}
|
||||
Type::Intersection(_) => {
|
||||
// TODO perform the get_member on each type in the intersection
|
||||
// TODO return the intersection of those results
|
||||
todo!("attribute lookup on Intersection type")
|
||||
}
|
||||
Type::IntLiteral(_) => {
|
||||
// TODO raise error
|
||||
Ok(Some(Type::Unknown))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FunctionTypeId> for Type {
|
||||
@@ -375,31 +336,6 @@ impl FunctionTypeId {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
|
||||
pub struct ModuleTypeId {
|
||||
module: Module,
|
||||
file_id: FileId,
|
||||
}
|
||||
|
||||
impl ModuleTypeId {
|
||||
fn module(self, db: &dyn SemanticDb) -> QueryResult<ModuleStoreRef> {
|
||||
let jar: &SemanticJar = db.jar()?;
|
||||
Ok(jar.type_store.add_or_get_module(self.file_id).downgrade())
|
||||
}
|
||||
|
||||
pub(crate) fn name(self, db: &dyn SemanticDb) -> QueryResult<ModuleName> {
|
||||
self.module.name(db)
|
||||
}
|
||||
|
||||
fn get_member(self, db: &dyn SemanticDb, name: &Name) -> QueryResult<Option<Type>> {
|
||||
if let Some(symbol_id) = resolve_global_symbol(db, self.name(db)?, name)? {
|
||||
Ok(Some(infer_symbol_type(db, symbol_id)?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
|
||||
pub struct ClassTypeId {
|
||||
file_id: FileId,
|
||||
@@ -453,13 +389,7 @@ impl ClassTypeId {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get own class member or fall back to super-class member.
|
||||
fn get_class_member(self, db: &dyn SemanticDb, name: &Name) -> QueryResult<Option<Type>> {
|
||||
self.get_own_class_member(db, name)
|
||||
.or_else(|_| self.get_super_class_member(db, name))
|
||||
}
|
||||
|
||||
// TODO: get_own_instance_member, get_instance_member
|
||||
// TODO: get_own_instance_member, get_class_member, get_instance_member
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
|
||||
@@ -599,10 +529,6 @@ impl std::fmt::Display for DisplayType<'_> {
|
||||
Type::Never => f.write_str("Never"),
|
||||
Type::Unknown => f.write_str("Unknown"),
|
||||
Type::Unbound => f.write_str("Unbound"),
|
||||
Type::Module(module_id) => {
|
||||
// NOTE: something like this?: "<module 'module-name' from 'path-from-fileid'>"
|
||||
todo!("{module_id:?}")
|
||||
}
|
||||
// TODO functions and classes should display using a fully qualified name
|
||||
Type::Class(class_id) => {
|
||||
f.write_str("Literal[")?;
|
||||
@@ -621,7 +547,6 @@ impl std::fmt::Display for DisplayType<'_> {
|
||||
.get_module(int_id.file_id)
|
||||
.get_intersection(int_id.intersection_id)
|
||||
.display(f, self.store),
|
||||
Type::IntLiteral(n) => write!(f, "Literal[{n}]"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,13 @@ use ruff_python_ast::AstNode;
|
||||
|
||||
use crate::db::{QueryResult, SemanticDb, SemanticJar};
|
||||
|
||||
use crate::module::{resolve_module, ModuleName};
|
||||
use crate::module::ModuleName;
|
||||
use crate::parse::parse;
|
||||
use crate::symbols::{
|
||||
resolve_global_symbol, symbol_table, Definition, GlobalSymbolId, ImportDefinition,
|
||||
ImportFromDefinition,
|
||||
resolve_global_symbol, symbol_table, Definition, GlobalSymbolId, ImportFromDefinition,
|
||||
};
|
||||
use crate::types::{ModuleTypeId, Type};
|
||||
use crate::{FileId, Name};
|
||||
use crate::types::Type;
|
||||
use crate::FileId;
|
||||
|
||||
// FIXME: Figure out proper dead-lock free synchronisation now that this takes `&db` instead of `&mut db`.
|
||||
#[tracing::instrument(level = "trace", skip(db))]
|
||||
@@ -47,15 +46,6 @@ pub fn infer_definition_type(
|
||||
let file_id = symbol.file_id;
|
||||
|
||||
match definition {
|
||||
Definition::Import(ImportDefinition {
|
||||
module: module_name,
|
||||
}) => {
|
||||
if let Some(module) = resolve_module(db, module_name.clone())? {
|
||||
Ok(Type::Module(ModuleTypeId { module, file_id }))
|
||||
} else {
|
||||
Ok(Type::Unknown)
|
||||
}
|
||||
}
|
||||
Definition::ImportFrom(ImportFromDefinition {
|
||||
module,
|
||||
name,
|
||||
@@ -124,20 +114,10 @@ pub fn infer_definition_type(
|
||||
let parsed = parse(db.upcast(), file_id)?;
|
||||
let ast = parsed.ast();
|
||||
let node = node_key.resolve_unwrap(ast.as_any_node_ref());
|
||||
// TODO handle unpacking assignment correctly (here and for AnnotatedAssignment case, below)
|
||||
// TODO handle unpacking assignment correctly
|
||||
infer_expr_type(db, file_id, &node.value)
|
||||
}
|
||||
Definition::AnnotatedAssignment(node_key) => {
|
||||
let parsed = parse(db.upcast(), file_id)?;
|
||||
let ast = parsed.ast();
|
||||
let node = node_key.resolve_unwrap(ast.as_any_node_ref());
|
||||
// TODO actually look at the annotation
|
||||
let Some(value) = &node.value else {
|
||||
return Ok(Type::Unknown);
|
||||
};
|
||||
// TODO handle unpacking assignment correctly (here and for Assignment case, above)
|
||||
infer_expr_type(db, file_id, value)
|
||||
}
|
||||
_ => todo!("other kinds of definitions"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,16 +125,6 @@ fn infer_expr_type(db: &dyn SemanticDb, file_id: FileId, expr: &ast::Expr) -> Qu
|
||||
// TODO cache the resolution of the type on the node
|
||||
let symbols = symbol_table(db, file_id)?;
|
||||
match expr {
|
||||
ast::Expr::NumberLiteral(ast::ExprNumberLiteral { value, .. }) => {
|
||||
match value {
|
||||
ast::Number::Int(n) => {
|
||||
// TODO support big int literals
|
||||
Ok(n.as_i64().map(Type::IntLiteral).unwrap_or(Type::Unknown))
|
||||
}
|
||||
// TODO builtins.float or builtins.complex
|
||||
_ => Ok(Type::Unknown),
|
||||
}
|
||||
}
|
||||
ast::Expr::Name(name) => {
|
||||
// TODO look up in the correct scope, don't assume global
|
||||
if let Some(symbol_id) = symbols.root_symbol_id_by_name(&name.id) {
|
||||
@@ -163,13 +133,6 @@ fn infer_expr_type(db: &dyn SemanticDb, file_id: FileId, expr: &ast::Expr) -> Qu
|
||||
Ok(Type::Unknown)
|
||||
}
|
||||
}
|
||||
ast::Expr::Attribute(ast::ExprAttribute { value, attr, .. }) => {
|
||||
let value_type = infer_expr_type(db, file_id, value)?;
|
||||
let attr_name = &Name::new(&attr.id);
|
||||
value_type
|
||||
.get_member(db, attr_name)
|
||||
.map(|ty| ty.unwrap_or(Type::Unknown))
|
||||
}
|
||||
_ => todo!("full expression type resolution"),
|
||||
}
|
||||
}
|
||||
@@ -326,66 +289,4 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_module_member() -> anyhow::Result<()> {
|
||||
let case = create_test()?;
|
||||
let db = &case.db;
|
||||
|
||||
let a_path = case.src.path().join("a.py");
|
||||
let b_path = case.src.path().join("b.py");
|
||||
std::fs::write(a_path, "import b; D = b.C")?;
|
||||
std::fs::write(b_path, "class C: pass")?;
|
||||
let a_file = resolve_module(db, ModuleName::new("a"))?
|
||||
.expect("module should be found")
|
||||
.path(db)?
|
||||
.file();
|
||||
let a_syms = symbol_table(db, a_file)?;
|
||||
let d_sym = a_syms
|
||||
.root_symbol_id_by_name("D")
|
||||
.expect("D symbol should be found");
|
||||
|
||||
let ty = infer_symbol_type(
|
||||
db,
|
||||
GlobalSymbolId {
|
||||
file_id: a_file,
|
||||
symbol_id: d_sym,
|
||||
},
|
||||
)?;
|
||||
|
||||
let jar = HasJar::<SemanticJar>::jar(db)?;
|
||||
assert!(matches!(ty, Type::Class(_)));
|
||||
assert_eq!(format!("{}", ty.display(&jar.type_store)), "Literal[C]");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_literal() -> anyhow::Result<()> {
|
||||
let case = create_test()?;
|
||||
let db = &case.db;
|
||||
|
||||
let path = case.src.path().join("a.py");
|
||||
std::fs::write(path, "x = 1")?;
|
||||
let file = resolve_module(db, ModuleName::new("a"))?
|
||||
.expect("module should be found")
|
||||
.path(db)?
|
||||
.file();
|
||||
let syms = symbol_table(db, file)?;
|
||||
let x_sym = syms
|
||||
.root_symbol_id_by_name("x")
|
||||
.expect("x symbol should be found");
|
||||
|
||||
let ty = infer_symbol_type(
|
||||
db,
|
||||
GlobalSymbolId {
|
||||
file_id: file,
|
||||
symbol_id: x_sym,
|
||||
},
|
||||
)?;
|
||||
|
||||
let jar = HasJar::<SemanticJar>::jar(db)?;
|
||||
assert!(matches!(ty, Type::IntLiteral(_)));
|
||||
assert_eq!(format!("{}", ty.display(&jar.type_store)), "Literal[1]");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.4.7"
|
||||
version = "0.4.4"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -401,7 +401,10 @@ pub(crate) fn format_source(
|
||||
// Format the cell.
|
||||
let formatted =
|
||||
format_module_source(unformatted, options.clone()).map_err(|err| {
|
||||
if let FormatModuleError::ParseError(err) = err {
|
||||
if let FormatModuleError::ParseError(mut err) = err {
|
||||
// Offset the error location by the start offset of the cell to report
|
||||
// the correct cell index.
|
||||
err.location += start;
|
||||
DisplayParseError::from_source_kind(
|
||||
err,
|
||||
path.map(Path::to_path_buf),
|
||||
@@ -857,20 +860,12 @@ pub(super) fn warn_incompatible_formatter_settings(resolver: &Resolver) {
|
||||
|
||||
if setting.linter.rules.enabled(Rule::BadQuotesMultilineString)
|
||||
&& setting.linter.flake8_quotes.multiline_quotes == Quote::Single
|
||||
&& matches!(
|
||||
setting.formatter.quote_style,
|
||||
QuoteStyle::Single | QuoteStyle::Double
|
||||
)
|
||||
{
|
||||
warn_user_once!("The `flake8-quotes.multiline-quotes=\"single\"` option is incompatible with the formatter. We recommend disabling `Q001` when using the formatter, which enforces double quotes for multiline strings. Alternatively, set the `flake8-quotes.multiline-quotes` option to `\"double\"`.`");
|
||||
}
|
||||
|
||||
if setting.linter.rules.enabled(Rule::BadQuotesDocstring)
|
||||
&& setting.linter.flake8_quotes.docstring_quotes == Quote::Single
|
||||
&& matches!(
|
||||
setting.formatter.quote_style,
|
||||
QuoteStyle::Single | QuoteStyle::Double
|
||||
)
|
||||
{
|
||||
warn_user_once!("The `flake8-quotes.multiline-quotes=\"single\"` option is incompatible with the formatter. We recommend disabling `Q002` when using the formatter, which enforces double quotes for docstrings. Alternatively, set the `flake8-quotes.docstring-quotes` option to `\"double\"`.`");
|
||||
}
|
||||
|
||||
@@ -237,9 +237,6 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
|
||||
let mut writer: Box<dyn Write> = match cli.output_file {
|
||||
Some(path) if !cli.watch => {
|
||||
colored::control::set_override(false);
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
let file = File::create(path)?;
|
||||
Box::new(BufWriter::new(file))
|
||||
}
|
||||
|
||||
@@ -1038,48 +1038,6 @@ def say_hy(name: str):
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid_linter_options_preserve() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let ruff_toml = tempdir.path().join("ruff.toml");
|
||||
fs::write(
|
||||
&ruff_toml,
|
||||
r#"
|
||||
[lint]
|
||||
select = ["Q"]
|
||||
|
||||
[lint.flake8-quotes]
|
||||
inline-quotes = "single"
|
||||
docstring-quotes = "single"
|
||||
multiline-quotes = "single"
|
||||
|
||||
[format]
|
||||
quote-style = "preserve"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let test_path = tempdir.path().join("test.py");
|
||||
fs::write(
|
||||
&test_path,
|
||||
r#"
|
||||
def say_hy(name: str):
|
||||
print(f"Hy {name}")"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["format", "--no-cache", "--config"])
|
||||
.arg(&ruff_toml)
|
||||
.arg(test_path), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
1 file reformatted
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_rules_default_options() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
|
||||
@@ -1414,7 +1414,7 @@ fn check_input_from_argfile() -> Result<()> {
|
||||
fs::write(&file_a_path, b"import os")?;
|
||||
fs::write(&file_b_path, b"print('hello, world!')")?;
|
||||
|
||||
// Create the input file for argfile to expand
|
||||
// Create a the input file for argfile to expand
|
||||
let input_file_path = tempdir.path().join("file_paths.txt");
|
||||
fs::write(
|
||||
&input_file_path,
|
||||
|
||||
@@ -34,29 +34,12 @@ marking it as unused, as in:
|
||||
from module import member as member
|
||||
```
|
||||
|
||||
Alternatively, you can use `__all__` to declare a symbol as part of the module's
|
||||
interface, as in:
|
||||
|
||||
```python
|
||||
# __init__.py
|
||||
import some_module
|
||||
|
||||
__all__ = ["some_module"]
|
||||
```
|
||||
|
||||
## Fix safety
|
||||
|
||||
Fixes to remove unused imports are safe, except in `__init__.py` files.
|
||||
|
||||
Applying fixes to `__init__.py` files is currently in preview. The fix offered depends on the
|
||||
type of the unused import. Ruff will suggest a safe fix to export first-party imports with
|
||||
either a redundant alias or, if already present in the file, an `__all__` entry. If multiple
|
||||
`__all__` declarations are present, Ruff will not offer a fix. Ruff will suggest an unsafe fix
|
||||
to remove third-party and standard library imports -- the fix is unsafe because the module's
|
||||
interface changes.
|
||||
When `ignore_init_module_imports` is disabled, fixes can remove for unused imports in `__init__` files.
|
||||
These fixes are considered unsafe because they can change the public interface.
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
import numpy as np # unused import
|
||||
|
||||
@@ -66,14 +49,12 @@ def area(radius):
|
||||
```
|
||||
|
||||
Use instead:
|
||||
|
||||
```python
|
||||
def area(radius):
|
||||
return 3.14 * radius**2
|
||||
```
|
||||
|
||||
To check the availability of a module, use `importlib.util.find_spec`:
|
||||
|
||||
```python
|
||||
from importlib.util import find_spec
|
||||
|
||||
|
||||
@@ -60,25 +60,24 @@ fn benchmark_linter(mut group: BenchmarkGroup, settings: &LinterSettings) {
|
||||
// Parse the source.
|
||||
let ast = parse_program_tokens(tokens.clone(), case.code(), false).unwrap();
|
||||
|
||||
b.iter_batched(
|
||||
|| (ast.clone(), tokens.clone()),
|
||||
|(ast, tokens)| {
|
||||
let path = case.path();
|
||||
let result = lint_only(
|
||||
&path,
|
||||
None,
|
||||
settings,
|
||||
flags::Noqa::Enabled,
|
||||
&SourceKind::Python(case.code().to_string()),
|
||||
PySourceType::from(path.as_path()),
|
||||
ParseSource::Precomputed { tokens, ast },
|
||||
);
|
||||
b.iter(|| {
|
||||
let path = case.path();
|
||||
let result = lint_only(
|
||||
&path,
|
||||
None,
|
||||
settings,
|
||||
flags::Noqa::Enabled,
|
||||
&SourceKind::Python(case.code().to_string()),
|
||||
PySourceType::from(path.as_path()),
|
||||
ParseSource::Precomputed {
|
||||
tokens: &tokens,
|
||||
ast: &ast,
|
||||
},
|
||||
);
|
||||
|
||||
// Assert that file contains no parse errors
|
||||
assert_eq!(result.error, None);
|
||||
},
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
// Assert that file contains no parse errors
|
||||
assert_eq!(result.error, None);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -553,6 +553,11 @@ impl PrintedRange {
|
||||
pub fn source_range(&self) -> TextRange {
|
||||
self.source_range
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_code(self, code: String) -> Self {
|
||||
Self { code, ..self }
|
||||
}
|
||||
}
|
||||
|
||||
/// Public return type of the formatter
|
||||
@@ -775,6 +780,10 @@ where
|
||||
self.item = item;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn into_item(self) -> T {
|
||||
self.item
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, R, C> Format<C> for FormatOwnedWithRule<T, R, C>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.4.7"
|
||||
version = "0.4.4"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
# type: ignore
|
||||
# ASYNCIO_NO_ERROR - no asyncio.sleep_forever, so check intentionally doesn't trigger.
|
||||
import math
|
||||
from math import inf
|
||||
|
||||
|
||||
async def import_trio():
|
||||
import trio
|
||||
|
||||
# These examples are probably not meant to ever wake up:
|
||||
await trio.sleep(100000) # error: 116, "async"
|
||||
|
||||
# 'inf literal' overflow trick
|
||||
await trio.sleep(1e999) # error: 116, "async"
|
||||
|
||||
await trio.sleep(86399)
|
||||
await trio.sleep(86400)
|
||||
await trio.sleep(86400.01) # error: 116, "async"
|
||||
await trio.sleep(86401) # error: 116, "async"
|
||||
|
||||
await trio.sleep(-1) # will raise a runtime error
|
||||
await trio.sleep(0) # handled by different check
|
||||
|
||||
# these ones _definitely_ never wake up (TODO)
|
||||
await trio.sleep(float("inf"))
|
||||
await trio.sleep(math.inf)
|
||||
await trio.sleep(inf)
|
||||
|
||||
# don't require inf to be in math (TODO)
|
||||
await trio.sleep(np.inf)
|
||||
|
||||
# don't evaluate expressions (TODO)
|
||||
one_day = 86401
|
||||
await trio.sleep(86400 + 1)
|
||||
await trio.sleep(60 * 60 * 24 + 1)
|
||||
await trio.sleep(foo())
|
||||
await trio.sleep(one_day)
|
||||
await trio.sleep(86400 + foo())
|
||||
await trio.sleep(86400 + ...)
|
||||
await trio.sleep("hello")
|
||||
await trio.sleep(...)
|
||||
|
||||
|
||||
def not_async_fun():
|
||||
import trio
|
||||
|
||||
# does not require the call to be awaited, nor in an async fun
|
||||
trio.sleep(86401) # error: 116, "async"
|
||||
# also checks that we don't break visit_Call
|
||||
trio.run(trio.sleep(86401)) # error: 116, "async"
|
||||
|
||||
|
||||
async def import_from_trio():
|
||||
from trio import sleep
|
||||
|
||||
# catch from import
|
||||
await sleep(86401) # error: 116, "async"
|
||||
@@ -11,11 +11,6 @@ x = list(
|
||||
x for x in range(3)
|
||||
)
|
||||
|
||||
# Strip parentheses from inner generators.
|
||||
list((2 * x for x in range(3)))
|
||||
list(((2 * x for x in range(3))))
|
||||
list((((2 * x for x in range(3)))))
|
||||
|
||||
# Not built-in list.
|
||||
def list(*args, **kwargs):
|
||||
return None
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
def main() -> None:
|
||||
a_list: list[str] | None = []
|
||||
a_list.append("hello")
|
||||
|
||||
|
||||
def hello(y: "dict[str, int] | None") -> None:
|
||||
del y
|
||||
@@ -14,7 +14,5 @@ if sys.version_info <= (3, 10): ... # Error: PYI006 Use only `<` and `>=` for v
|
||||
if sys.version_info <= (3, 10): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons
|
||||
|
||||
if sys.version_info > (3, 10): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons
|
||||
elif sys.version_info > (3, 11): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons
|
||||
|
||||
if python_version > (3, 10): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons
|
||||
elif python_version == (3, 11): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import typing
|
||||
import collections.abc
|
||||
import foo
|
||||
from typing import ByteString
|
||||
from collections.abc import ByteString
|
||||
from foo import ByteString
|
||||
|
||||
a: typing.ByteString
|
||||
b: collections.abc.ByteString
|
||||
c: foo.ByteString
|
||||
@@ -1,10 +0,0 @@
|
||||
import typing
|
||||
import collections.abc
|
||||
import foo
|
||||
from typing import ByteString
|
||||
from collections.abc import ByteString
|
||||
from foo import ByteString
|
||||
|
||||
a: typing.ByteString
|
||||
b: collections.abc.ByteString
|
||||
c: foo.ByteString
|
||||
@@ -1,21 +0,0 @@
|
||||
from typing import Final, Literal
|
||||
|
||||
x: Final[Literal[True]] = True # PYI064
|
||||
y: Final[Literal[None]] = None # PYI064
|
||||
z: Final[Literal[ # PYI064
|
||||
"this is a really long literal, that won't be rendered in the issue text"
|
||||
]] = "this is a really long literal, that won't be rendered in the issue text"
|
||||
|
||||
# This should be fixable, and marked as safe
|
||||
w1: Final[Literal[123]] # PYI064
|
||||
|
||||
# This should not be fixable
|
||||
w2: Final[Literal[123]] = "random value" # PYI064
|
||||
|
||||
n1: Final[Literal[True, False]] = True # No issue here
|
||||
n2: Literal[True] = True # No issue here
|
||||
|
||||
PlatformName = Literal["linux", "macos", "windows"]
|
||||
PLATFORMS: Final[set[PlatformName]] = {"linux", "macos", "windows"} # No issue here
|
||||
|
||||
foo: Final[{1, 2, 3}] = {1, 2, 3} # No issue here
|
||||
@@ -1,19 +0,0 @@
|
||||
from typing import Final, Literal
|
||||
|
||||
x: Final[Literal[True]] # PYI064
|
||||
y: Final[Literal[None]] = None # PYI064
|
||||
z: Final[Literal["this is a really long literal, that won't be rendered in the issue text"]] # PYI064
|
||||
|
||||
# This should be fixable, and marked as safe
|
||||
w1: Final[Literal[123]] # PYI064
|
||||
|
||||
# This should not be fixable
|
||||
w2: Final[Literal[123]] = "random value" # PYI064
|
||||
|
||||
n1: Final[Literal[True, False]] # No issue here
|
||||
n2: Literal[True] # No issue here
|
||||
|
||||
PlatformName = Literal["linux", "macos", "windows"]
|
||||
PLATFORMS: Final[set[PlatformName]] = {"linux", "macos", "windows"} # No issue here
|
||||
|
||||
foo: Final[{1, 2, 3}] = {1, 2, 3} # No issue here
|
||||
@@ -1,54 +0,0 @@
|
||||
import sys
|
||||
|
||||
if sys.version_info < (3, 10): # Y066 When using if/else with sys.version_info, put the code for new Python versions first, e.g. "if sys.version_info >= (3, 10)"
|
||||
def foo(x): ...
|
||||
else:
|
||||
def foo(x, *, bar=True): ...
|
||||
|
||||
if sys.version_info < (3, 8): # Y066 When using if/else with sys.version_info, put the code for new Python versions first, e.g. "if sys.version_info >= (3, 8)"
|
||||
def bar(x): ...
|
||||
elif sys.version_info < (3, 9): # Y066 When using if/else with sys.version_info, put the code for new Python versions first, e.g. "if sys.version_info >= (3, 9)"
|
||||
def bar(x, *, bar=True): ...
|
||||
elif sys.version_info < (3, 11): # Y066 When using if/else with sys.version_info, put the code for new Python versions first, e.g. "if sys.version_info >= (3, 10)"
|
||||
def bar(x, *, bar=True, baz=False): ...
|
||||
else:
|
||||
def bar(x, *, bar=True, baz=False, qux=1): ...
|
||||
|
||||
|
||||
if sys.version_info >= (3, 5):
|
||||
...
|
||||
elif sys.version_info < (3, 9): # Y066 When using if/else with sys.version_info, put the code for new Python versions first, e.g. "if sys.version_info >= (3, 10)"
|
||||
...
|
||||
else:
|
||||
...
|
||||
|
||||
# Negative cases
|
||||
|
||||
if sys.version_info[0] == 2: ...
|
||||
if sys.version_info[:1] == (2,): ...
|
||||
if sys.version_info[:1] == (True,): ...
|
||||
if sys.version_info < ('3', '0'): ...
|
||||
if sys.version_info >= (3, 4, 3): ...
|
||||
if sys.version_info == (3, 4): ...
|
||||
if sys.version_info < (3, 5): ...
|
||||
if sys.version_info >= (3, 5): ...
|
||||
if (2, 7) <= sys.version_info < (3, 5): ...
|
||||
|
||||
|
||||
if sys.version_info >= (3, 5):
|
||||
...
|
||||
else:
|
||||
...
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
def foo1(x, *, bar=True, baz=False): ...
|
||||
elif sys.version_info >= (3, 9):
|
||||
def foo1(x, *, bar=True): ...
|
||||
else:
|
||||
def foo1(x): ...
|
||||
|
||||
if sys.version_info < (3, 9):
|
||||
def foo2(x): ...
|
||||
elif sys.version_info < (3, 10):
|
||||
def foo2(x, *, bar=True): ...
|
||||
# no else case, no raise
|
||||
@@ -1,54 +0,0 @@
|
||||
import sys
|
||||
|
||||
if sys.version_info < (3, 10): # Y066 When using if/else with sys.version_info, put the code for new Python versions first, e.g. "if sys.version_info >= (3, 10)"
|
||||
def foo(x): ...
|
||||
else:
|
||||
def foo(x, *, bar=True): ...
|
||||
|
||||
if sys.version_info < (3, 8): # Y066 When using if/else with sys.version_info, put the code for new Python versions first, e.g. "if sys.version_info >= (3, 8)"
|
||||
def bar(x): ...
|
||||
elif sys.version_info < (3, 9): # Y066 When using if/else with sys.version_info, put the code for new Python versions first, e.g. "if sys.version_info >= (3, 9)"
|
||||
def bar(x, *, bar=True): ...
|
||||
elif sys.version_info < (3, 11): # Y066 When using if/else with sys.version_info, put the code for new Python versions first, e.g. "if sys.version_info >= (3, 10)"
|
||||
def bar(x, *, bar=True, baz=False): ...
|
||||
else:
|
||||
def bar(x, *, bar=True, baz=False, qux=1): ...
|
||||
|
||||
|
||||
if sys.version_info >= (3, 5):
|
||||
...
|
||||
elif sys.version_info < (3, 9): # Y066 When using if/else with sys.version_info, put the code for new Python versions first, e.g. "if sys.version_info >= (3, 10)"
|
||||
...
|
||||
else:
|
||||
...
|
||||
|
||||
# Negative cases
|
||||
|
||||
if sys.version_info[0] == 2: ...
|
||||
if sys.version_info[:1] == (2,): ...
|
||||
if sys.version_info[:1] == (True,): ...
|
||||
if sys.version_info < ('3', '0'): ...
|
||||
if sys.version_info >= (3, 4, 3): ...
|
||||
if sys.version_info == (3, 4): ...
|
||||
if sys.version_info < (3, 5): ...
|
||||
if sys.version_info >= (3, 5): ...
|
||||
if (2, 7) <= sys.version_info < (3, 5): ...
|
||||
|
||||
|
||||
if sys.version_info >= (3, 5):
|
||||
...
|
||||
else:
|
||||
...
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
def foo1(x, *, bar=True, baz=False): ...
|
||||
elif sys.version_info >= (3, 9):
|
||||
def foo1(x, *, bar=True): ...
|
||||
else:
|
||||
def foo1(x): ...
|
||||
|
||||
if sys.version_info < (3, 9):
|
||||
def foo2(x): ...
|
||||
elif sys.version_info < (3, 10):
|
||||
def foo2(x, *, bar=True): ...
|
||||
# no else case, no raise
|
||||
@@ -77,8 +77,3 @@ print(foo._asdict())
|
||||
import os
|
||||
|
||||
os._exit()
|
||||
|
||||
|
||||
from enum import Enum
|
||||
|
||||
Enum._missing_(1) # OK
|
||||
|
||||
@@ -46,15 +46,3 @@ with contextlib.ExitStack() as exit_stack:
|
||||
# OK (quick one-liner to clear file contents)
|
||||
open("filename", "w").close()
|
||||
pathlib.Path("filename").open("w").close()
|
||||
|
||||
|
||||
# OK (custom context manager)
|
||||
class MyFile:
|
||||
def __init__(self, filename: str):
|
||||
self.filename = filename
|
||||
|
||||
def __enter__(self):
|
||||
self.file = open(self.filename)
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.file.close()
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import singledispatch
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from numpy import asarray
|
||||
@@ -33,24 +32,3 @@ def _(a: spmatrix) -> spmatrix:
|
||||
|
||||
def _(a: DataFrame) -> DataFrame:
|
||||
return a
|
||||
|
||||
|
||||
@singledispatch
|
||||
def process_path(a: int | str, p: Path) -> int:
|
||||
"""Convert arg to array or leaves it as sparse matrix."""
|
||||
msg = f"Unhandled type {type(a)}"
|
||||
raise NotImplementedError(msg)
|
||||
|
||||
|
||||
@process_path.register
|
||||
def _(a: int, p: Path) -> int:
|
||||
return asarray(a)
|
||||
|
||||
|
||||
@process_path.register
|
||||
def _(a: str, p: Path) -> int:
|
||||
return a
|
||||
|
||||
|
||||
def _(a: DataFrame, p: Path) -> DataFrame:
|
||||
return a
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
# If the file doesn't contain a logical indent token, we should still detect two-space indentation on imports.
|
||||
from math import (
|
||||
sin,
|
||||
tan,
|
||||
cos,
|
||||
nan,
|
||||
pi,
|
||||
)
|
||||
|
||||
del sin, cos, tan, pi, nan
|
||||
@@ -106,11 +106,3 @@ def func():
|
||||
np.who()
|
||||
|
||||
np.row_stack(([1,2], [3,4]))
|
||||
|
||||
np.alltrue([True, True])
|
||||
|
||||
np.anytrue([True, False])
|
||||
|
||||
np.cumproduct([1, 2, 3])
|
||||
|
||||
np.product([1, 2, 3])
|
||||
|
||||
@@ -63,16 +63,3 @@ if (a and
|
||||
#: Okay
|
||||
def f():
|
||||
return 1
|
||||
|
||||
# Soft keywords
|
||||
|
||||
#: E271
|
||||
type Number = int
|
||||
|
||||
#: E273
|
||||
type Number = int
|
||||
|
||||
#: E275
|
||||
match(foo):
|
||||
case(1):
|
||||
pass
|
||||
|
||||
@@ -46,15 +46,3 @@ regex = '\\\_'
|
||||
|
||||
#: W605:1:7
|
||||
u'foo\ bar'
|
||||
|
||||
#: W605:1:13
|
||||
(
|
||||
"foo \
|
||||
bar \. baz"
|
||||
)
|
||||
|
||||
#: W605:1:6
|
||||
"foo \. bar \t"
|
||||
|
||||
#: W605:1:13
|
||||
"foo \t bar \."
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
def func():
|
||||
"""
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import foo
|
||||
"""
|
||||
|
||||
|
||||
def func():
|
||||
"""
|
||||
Example:
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import foo
|
||||
"""
|
||||
|
||||
|
||||
def func():
|
||||
"""
|
||||
Example:
|
||||
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import foo
|
||||
"""
|
||||
|
||||
|
||||
def func():
|
||||
"""
|
||||
Example
|
||||
-------
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import foo
|
||||
"""
|
||||
|
||||
|
||||
def func():
|
||||
"""
|
||||
Example
|
||||
-------
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import foo
|
||||
"""
|
||||
|
||||
|
||||
def func():
|
||||
"""
|
||||
Example
|
||||
-------
|
||||
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import foo
|
||||
"""
|
||||
@@ -82,16 +82,3 @@ class Foo:
|
||||
@qux.setter
|
||||
def qux(self, value):
|
||||
self.bar = value / 2
|
||||
|
||||
|
||||
class StudentG:
|
||||
names = ("surname",)
|
||||
__slots__ = (*names, "a")
|
||||
|
||||
def __init__(self, name, surname):
|
||||
self.name = name
|
||||
self.surname = surname # [assigning-non-slot]
|
||||
self.setup()
|
||||
|
||||
def setup(self):
|
||||
pass
|
||||
|
||||
@@ -21,8 +21,6 @@ def wrong(): # [too-many-branches]
|
||||
pass
|
||||
try:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
pass
|
||||
if 2:
|
||||
@@ -58,8 +56,6 @@ def good():
|
||||
pass
|
||||
try:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
pass
|
||||
if 1:
|
||||
@@ -94,8 +90,6 @@ def with_statement_wrong():
|
||||
pass
|
||||
try:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
pass
|
||||
if 2:
|
||||
|
||||
@@ -29,11 +29,6 @@ print("%#o" % (123,))
|
||||
|
||||
print("brace {} %s" % (1,))
|
||||
|
||||
print((
|
||||
"foo %s "
|
||||
"bar %s" % (x, y)
|
||||
))
|
||||
|
||||
print(
|
||||
"%s" % (
|
||||
"trailing comma",
|
||||
@@ -57,6 +52,10 @@ print("%(ab)s" % {"a" "b": 1})
|
||||
|
||||
print("%(a)s" % {"a" : 1})
|
||||
|
||||
print((
|
||||
"foo %s "
|
||||
"bar %s" % (x, y)
|
||||
))
|
||||
|
||||
print(
|
||||
"foo %(foo)s "
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Tuple
|
||||
|
||||
|
||||
def foo():
|
||||
# UP037
|
||||
x: "Tuple[int, int]" = (0, 0)
|
||||
print(x)
|
||||
|
||||
|
||||
# OK
|
||||
X: "Tuple[int, int]" = (0, 0)
|
||||
@@ -51,37 +51,3 @@ x: int = 1
|
||||
# type alias.
|
||||
T = typing.TypeVar["T"]
|
||||
Decorator: TypeAlias = typing.Callable[[T], T]
|
||||
|
||||
|
||||
from typing import TypeVar, Annotated, TypeAliasType
|
||||
|
||||
from annotated_types import Gt, SupportGt
|
||||
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/11422
|
||||
T = TypeVar("T")
|
||||
PositiveList = TypeAliasType(
|
||||
"PositiveList", list[Annotated[T, Gt(0)]], type_params=(T,)
|
||||
)
|
||||
|
||||
# Bound
|
||||
T = TypeVar("T", bound=SupportGt)
|
||||
PositiveList = TypeAliasType(
|
||||
"PositiveList", list[Annotated[T, Gt(0)]], type_params=(T,)
|
||||
)
|
||||
|
||||
# Multiple bounds
|
||||
T1 = TypeVar("T1", bound=SupportGt)
|
||||
T2 = TypeVar("T2")
|
||||
T3 = TypeVar("T3")
|
||||
Tuple3 = TypeAliasType("Tuple3", tuple[T1, T2, T3], type_params=(T1, T2, T3))
|
||||
|
||||
# No type_params
|
||||
PositiveInt = TypeAliasType("PositiveInt", Annotated[int, Gt(0)])
|
||||
PositiveInt = TypeAliasType("PositiveInt", Annotated[int, Gt(0)], type_params=())
|
||||
|
||||
# OK: Other name
|
||||
T = TypeVar("T", bound=SupportGt)
|
||||
PositiveList = TypeAliasType(
|
||||
"PositiveList2", list[Annotated[T, Gt(0)]], type_params=(T,)
|
||||
)
|
||||
|
||||
@@ -38,12 +38,3 @@ z = (
|
||||
else
|
||||
y
|
||||
)
|
||||
|
||||
# FURB110
|
||||
z = (
|
||||
x
|
||||
if x
|
||||
else y
|
||||
if y > 0
|
||||
else None
|
||||
)
|
||||
|
||||
@@ -60,7 +60,6 @@ op_itemgetter = lambda x, y: (x[0], y[0])
|
||||
op_itemgetter = lambda x: ()
|
||||
op_itemgetter = lambda x: (*x[0], x[1])
|
||||
op_itemgetter = lambda x: (x[0],)
|
||||
op_itemgetter = lambda x: x[x]
|
||||
|
||||
|
||||
def op_neg3(x, y):
|
||||
|
||||
@@ -62,8 +62,6 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
if !checker.semantic.future_annotations_or_stub()
|
||||
&& checker.settings.target_version < PythonVersion::Py39
|
||||
&& checker.semantic.in_annotation()
|
||||
&& checker.semantic.in_runtime_evaluated_annotation()
|
||||
&& !checker.semantic.in_string_type_definition()
|
||||
&& typing::is_pep585_generic(value, &checker.semantic)
|
||||
{
|
||||
flake8_future_annotations::rules::future_required_type_annotation(
|
||||
@@ -346,9 +344,6 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::PandasUseOfDotValues) {
|
||||
pandas_vet::rules::attr(checker, attribute);
|
||||
}
|
||||
if checker.enabled(Rule::ByteStringUsage) {
|
||||
flake8_pyi::rules::bytestring_attribute(checker, expr);
|
||||
}
|
||||
}
|
||||
Expr::Call(
|
||||
call @ ast::ExprCall {
|
||||
@@ -511,9 +506,6 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::BlockingOsCallInAsyncFunction) {
|
||||
flake8_async::rules::blocking_os_call(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::SleepForeverCall) {
|
||||
flake8_async::rules::sleep_forever_call(checker, call);
|
||||
}
|
||||
if checker.any_enabled(&[Rule::Print, Rule::PPrint]) {
|
||||
flake8_print::rules::print_call(checker, call);
|
||||
}
|
||||
@@ -1203,8 +1195,6 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
if !checker.semantic.future_annotations_or_stub()
|
||||
&& checker.settings.target_version < PythonVersion::Py310
|
||||
&& checker.semantic.in_annotation()
|
||||
&& checker.semantic.in_runtime_evaluated_annotation()
|
||||
&& !checker.semantic.in_string_type_definition()
|
||||
{
|
||||
flake8_future_annotations::rules::future_required_type_annotation(
|
||||
checker,
|
||||
|
||||
@@ -1027,9 +1027,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
if checker.enabled(Rule::ByteStringUsage) {
|
||||
flake8_pyi::rules::bytestring_import(checker, import_from);
|
||||
}
|
||||
}
|
||||
Stmt::Raise(raise @ ast::StmtRaise { exc, .. }) => {
|
||||
if checker.enabled(Rule::RaiseNotImplemented) {
|
||||
@@ -1095,13 +1092,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
ruff::rules::sort_dunder_all_aug_assign(checker, aug_assign);
|
||||
}
|
||||
}
|
||||
Stmt::If(
|
||||
if_ @ ast::StmtIf {
|
||||
test,
|
||||
elif_else_clauses,
|
||||
..
|
||||
},
|
||||
) => {
|
||||
Stmt::If(if_ @ ast::StmtIf { test, .. }) => {
|
||||
if checker.enabled(Rule::TooManyNestedBlocks) {
|
||||
pylint::rules::too_many_nested_blocks(checker, stmt);
|
||||
}
|
||||
@@ -1181,38 +1172,13 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
flake8_pyi::rules::unrecognized_platform(checker, test);
|
||||
}
|
||||
}
|
||||
if checker.any_enabled(&[Rule::BadVersionInfoComparison, Rule::BadVersionInfoOrder])
|
||||
{
|
||||
fn bad_version_info_comparison(
|
||||
checker: &mut Checker,
|
||||
test: &Expr,
|
||||
has_else_clause: bool,
|
||||
) {
|
||||
if let Expr::BoolOp(ast::ExprBoolOp { values, .. }) = test {
|
||||
for value in values {
|
||||
flake8_pyi::rules::bad_version_info_comparison(
|
||||
checker,
|
||||
value,
|
||||
has_else_clause,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
flake8_pyi::rules::bad_version_info_comparison(
|
||||
checker,
|
||||
test,
|
||||
has_else_clause,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let has_else_clause =
|
||||
elif_else_clauses.iter().any(|clause| clause.test.is_none());
|
||||
|
||||
bad_version_info_comparison(checker, test.as_ref(), has_else_clause);
|
||||
for clause in elif_else_clauses {
|
||||
if let Some(test) = clause.test.as_ref() {
|
||||
bad_version_info_comparison(checker, test, has_else_clause);
|
||||
if checker.enabled(Rule::BadVersionInfoComparison) {
|
||||
if let Expr::BoolOp(ast::ExprBoolOp { values, .. }) = test.as_ref() {
|
||||
for value in values {
|
||||
flake8_pyi::rules::bad_version_info_comparison(checker, value);
|
||||
}
|
||||
} else {
|
||||
flake8_pyi::rules::bad_version_info_comparison(checker, test);
|
||||
}
|
||||
}
|
||||
if checker.enabled(Rule::ComplexIfStatementInStub) {
|
||||
@@ -1592,9 +1558,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::ListReverseCopy) {
|
||||
refurb::rules::list_assign_reversed(checker, assign);
|
||||
}
|
||||
if checker.enabled(Rule::NonPEP695TypeAlias) {
|
||||
pyupgrade::rules::non_pep695_type_alias_type(checker, assign);
|
||||
}
|
||||
}
|
||||
Stmt::AnnAssign(
|
||||
assign_stmt @ ast::StmtAnnAssign {
|
||||
@@ -1667,13 +1630,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::TSuffixedTypeAlias) {
|
||||
flake8_pyi::rules::t_suffixed_type_alias(checker, target);
|
||||
}
|
||||
} else if checker
|
||||
.semantic
|
||||
.match_typing_expr(helpers::map_subscript(annotation), "Final")
|
||||
{
|
||||
if checker.enabled(Rule::RedundantFinalLiteral) {
|
||||
flake8_pyi::rules::redundant_final_literal(checker, assign_stmt);
|
||||
}
|
||||
}
|
||||
}
|
||||
Stmt::TypeAlias(ast::StmtTypeAlias { name, .. }) => {
|
||||
|
||||
@@ -588,10 +588,8 @@ impl<'a> Visitor<'a> for Checker<'a> {
|
||||
Stmt::Global(ast::StmtGlobal { names, range: _ }) => {
|
||||
if !self.semantic.scope_id.is_global() {
|
||||
for name in names {
|
||||
let binding_id = self.semantic.global_scope().get(name);
|
||||
|
||||
// Mark the binding in the global scope as "rebound" in the current scope.
|
||||
if let Some(binding_id) = binding_id {
|
||||
if let Some(binding_id) = self.semantic.global_scope().get(name) {
|
||||
// Mark the binding in the global scope as "rebound" in the current scope.
|
||||
self.semantic
|
||||
.add_rebinding_scope(binding_id, self.semantic.scope_id);
|
||||
}
|
||||
@@ -599,7 +597,7 @@ impl<'a> Visitor<'a> for Checker<'a> {
|
||||
// Add a binding to the current scope.
|
||||
let binding_id = self.semantic.push_binding(
|
||||
name.range(),
|
||||
BindingKind::Global(binding_id),
|
||||
BindingKind::Global,
|
||||
BindingFlags::GLOBAL,
|
||||
);
|
||||
let scope = self.semantic.current_scope_mut();
|
||||
@@ -611,8 +609,7 @@ impl<'a> Visitor<'a> for Checker<'a> {
|
||||
if !self.semantic.scope_id.is_global() {
|
||||
for name in names {
|
||||
if let Some((scope_id, binding_id)) = self.semantic.nonlocal(name) {
|
||||
// Mark the binding as "used", since the `nonlocal` requires an existing
|
||||
// binding.
|
||||
// Mark the binding as "used".
|
||||
self.semantic.add_local_reference(
|
||||
binding_id,
|
||||
ExprContext::Load,
|
||||
@@ -627,7 +624,7 @@ impl<'a> Visitor<'a> for Checker<'a> {
|
||||
// Add a binding to the current scope.
|
||||
let binding_id = self.semantic.push_binding(
|
||||
name.range(),
|
||||
BindingKind::Nonlocal(binding_id, scope_id),
|
||||
BindingKind::Nonlocal(scope_id),
|
||||
BindingFlags::NONLOCAL,
|
||||
);
|
||||
let scope = self.semantic.current_scope_mut();
|
||||
@@ -664,7 +661,7 @@ impl<'a> Visitor<'a> for Checker<'a> {
|
||||
AnnotationContext::from_function(function_def, &self.semantic, self.settings);
|
||||
|
||||
// The first parameter may be a single dispatch.
|
||||
let singledispatch =
|
||||
let mut singledispatch =
|
||||
flake8_type_checking::helpers::is_singledispatch_implementation(
|
||||
function_def,
|
||||
self.semantic(),
|
||||
@@ -680,6 +677,7 @@ impl<'a> Visitor<'a> for Checker<'a> {
|
||||
if let Some(expr) = parameter.annotation() {
|
||||
if singledispatch && !parameter.is_variadic() {
|
||||
self.visit_runtime_required_annotation(expr);
|
||||
singledispatch = false;
|
||||
} else {
|
||||
match annotation {
|
||||
AnnotationContext::RuntimeRequired => {
|
||||
@@ -2154,7 +2152,7 @@ impl<'a> Checker<'a> {
|
||||
|
||||
self.semantic.restore(snapshot);
|
||||
|
||||
if self.semantic.in_annotation() && self.semantic.in_typing_only_annotation() {
|
||||
if self.semantic.in_annotation() && self.semantic.future_annotations_or_stub() {
|
||||
if self.enabled(Rule::QuotedAnnotation) {
|
||||
pyupgrade::rules::quoted_annotation(self, value, range);
|
||||
}
|
||||
@@ -2301,9 +2299,7 @@ impl<'a> Checker<'a> {
|
||||
}
|
||||
} else {
|
||||
if self.enabled(Rule::UndefinedExport) {
|
||||
if self.settings.preview.is_enabled()
|
||||
|| !self.path.ends_with("__init__.py")
|
||||
{
|
||||
if !self.path.ends_with("__init__.py") {
|
||||
self.diagnostics.push(
|
||||
Diagnostic::new(
|
||||
pyflakes::rules::UndefinedExport {
|
||||
|
||||
@@ -334,7 +334,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8Async, "100") => (RuleGroup::Stable, rules::flake8_async::rules::BlockingHttpCallInAsyncFunction),
|
||||
(Flake8Async, "101") => (RuleGroup::Stable, rules::flake8_async::rules::OpenSleepOrSubprocessInAsyncFunction),
|
||||
(Flake8Async, "102") => (RuleGroup::Stable, rules::flake8_async::rules::BlockingOsCallInAsyncFunction),
|
||||
(Flake8Async, "116") => (RuleGroup::Preview, rules::flake8_async::rules::SleepForeverCall),
|
||||
|
||||
// flake8-trio
|
||||
(Flake8Trio, "100") => (RuleGroup::Stable, rules::flake8_trio::rules::TrioTimeoutWithoutAwait),
|
||||
@@ -809,11 +808,8 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8Pyi, "055") => (RuleGroup::Stable, rules::flake8_pyi::rules::UnnecessaryTypeUnion),
|
||||
(Flake8Pyi, "056") => (RuleGroup::Stable, rules::flake8_pyi::rules::UnsupportedMethodCallOnAll),
|
||||
(Flake8Pyi, "058") => (RuleGroup::Stable, rules::flake8_pyi::rules::GeneratorReturnFromIterMethod),
|
||||
(Flake8Pyi, "057") => (RuleGroup::Preview, rules::flake8_pyi::rules::ByteStringUsage),
|
||||
(Flake8Pyi, "059") => (RuleGroup::Preview, rules::flake8_pyi::rules::GenericNotLastBaseClass),
|
||||
(Flake8Pyi, "062") => (RuleGroup::Preview, rules::flake8_pyi::rules::DuplicateLiteralMember),
|
||||
(Flake8Pyi, "064") => (RuleGroup::Preview, rules::flake8_pyi::rules::RedundantFinalLiteral),
|
||||
(Flake8Pyi, "066") => (RuleGroup::Preview, rules::flake8_pyi::rules::BadVersionInfoOrder),
|
||||
|
||||
// flake8-pytest-style
|
||||
(Flake8PytestStyle, "001") => (RuleGroup::Stable, rules::flake8_pytest_style::rules::PytestFixtureIncorrectParenthesesStyle),
|
||||
|
||||
@@ -4,7 +4,6 @@ use std::iter::Peekable;
|
||||
use std::str::FromStr;
|
||||
|
||||
use bitflags::bitflags;
|
||||
use ruff_python_ast::StringFlags;
|
||||
use ruff_python_parser::lexer::LexResult;
|
||||
use ruff_python_parser::Tok;
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
|
||||
@@ -46,6 +45,22 @@ pub struct IsortDirectives {
|
||||
pub skip_file: bool,
|
||||
}
|
||||
|
||||
impl IsortDirectives {
|
||||
pub fn is_excluded(&self, offset: TextSize) -> bool {
|
||||
for range in &self.exclusions {
|
||||
if range.contains(offset) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if range.start() > offset {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Directives {
|
||||
pub noqa_line_for: NoqaMapping,
|
||||
pub isort: IsortDirectives,
|
||||
|
||||
@@ -146,7 +146,7 @@ pub fn check_path(
|
||||
.any(|rule_code| rule_code.lint_source().is_imports());
|
||||
if use_ast || use_imports || use_doc_lines {
|
||||
// Parse, if the AST wasn't pre-provided provided.
|
||||
match tokens.into_ast(source_kind, source_type) {
|
||||
match tokens.into_ast_source(source_kind, source_type) {
|
||||
Ok(python_ast) => {
|
||||
let cell_offsets = source_kind.as_ipy_notebook().map(Notebook::cell_offsets);
|
||||
let notebook_index = source_kind.as_ipy_notebook().map(Notebook::index);
|
||||
@@ -684,16 +684,23 @@ This indicates a bug in Ruff. If you could open an issue at:
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ParseSource {
|
||||
pub enum ParseSource<'a> {
|
||||
/// Extract the tokens and AST from the given source code.
|
||||
None,
|
||||
/// Use the precomputed tokens and AST.
|
||||
Precomputed { tokens: Tokens, ast: Suite },
|
||||
Precomputed {
|
||||
tokens: &'a [LexResult],
|
||||
ast: &'a Suite,
|
||||
},
|
||||
}
|
||||
|
||||
impl ParseSource {
|
||||
impl<'a> ParseSource<'a> {
|
||||
/// Convert to a [`TokenSource`], tokenizing if necessary.
|
||||
fn into_token_source(self, source_kind: &SourceKind, source_type: PySourceType) -> TokenSource {
|
||||
fn into_token_source(
|
||||
self,
|
||||
source_kind: &SourceKind,
|
||||
source_type: PySourceType,
|
||||
) -> TokenSource<'a> {
|
||||
match self {
|
||||
Self::None => TokenSource::Tokens(ruff_python_parser::tokenize(
|
||||
source_kind.source_code(),
|
||||
@@ -705,14 +712,17 @@ impl ParseSource {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum TokenSource {
|
||||
pub enum TokenSource<'a> {
|
||||
/// Use the precomputed tokens to generate the AST.
|
||||
Tokens(Tokens),
|
||||
/// Use the precomputed tokens and AST.
|
||||
Precomputed { tokens: Tokens, ast: Suite },
|
||||
Precomputed {
|
||||
tokens: &'a [LexResult],
|
||||
ast: &'a Suite,
|
||||
},
|
||||
}
|
||||
|
||||
impl TokenSource {
|
||||
impl TokenSource<'_> {
|
||||
/// Returns an iterator over the [`TokenKind`] and the corresponding range.
|
||||
///
|
||||
/// [`TokenKind`]: ruff_python_parser::TokenKind
|
||||
@@ -724,7 +734,7 @@ impl TokenSource {
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for TokenSource {
|
||||
impl Deref for TokenSource<'_> {
|
||||
type Target = [LexResult];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
@@ -735,20 +745,39 @@ impl Deref for TokenSource {
|
||||
}
|
||||
}
|
||||
|
||||
impl TokenSource {
|
||||
impl<'a> TokenSource<'a> {
|
||||
/// Convert to an [`AstSource`], parsing if necessary.
|
||||
fn into_ast(
|
||||
fn into_ast_source(
|
||||
self,
|
||||
source_kind: &SourceKind,
|
||||
source_type: PySourceType,
|
||||
) -> Result<Suite, ParseError> {
|
||||
) -> Result<AstSource<'a>, ParseError> {
|
||||
match self {
|
||||
Self::Tokens(tokens) => Ok(ruff_python_parser::parse_program_tokens(
|
||||
Self::Tokens(tokens) => Ok(AstSource::Ast(ruff_python_parser::parse_program_tokens(
|
||||
tokens,
|
||||
source_kind.source_code(),
|
||||
source_type.is_ipynb(),
|
||||
)?),
|
||||
Self::Precomputed { ast, .. } => Ok(ast),
|
||||
)?)),
|
||||
Self::Precomputed { ast, .. } => Ok(AstSource::Precomputed(ast)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AstSource<'a> {
|
||||
/// Extract the AST from the given source code.
|
||||
Ast(Suite),
|
||||
/// Use the precomputed AST.
|
||||
Precomputed(&'a Suite),
|
||||
}
|
||||
|
||||
impl Deref for AstSource<'_> {
|
||||
type Target = Suite;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
match self {
|
||||
Self::Ast(ast) => ast,
|
||||
Self::Precomputed(ast) => ast,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,7 +253,7 @@ impl Display for DisplayParseError {
|
||||
ErrorLocation::Cell(cell, location) => {
|
||||
write!(
|
||||
f,
|
||||
"{cell}{colon}{row}{colon}{column}{colon} {inner}",
|
||||
"cell {cell}{colon}{row}{colon}{column}{colon} {inner}",
|
||||
cell = cell,
|
||||
row = location.row,
|
||||
column = location.column,
|
||||
|
||||
@@ -82,12 +82,12 @@ impl Serialize for SerializedMessages<'_> {
|
||||
|project_dir| relativize_path_to(message.filename(), project_dir),
|
||||
);
|
||||
|
||||
let mut message_fingerprint = fingerprint(message, &path, 0);
|
||||
let mut message_fingerprint = fingerprint(message, 0);
|
||||
|
||||
// Make sure that we do not get a fingerprint that is already in use
|
||||
// by adding in the previously generated one.
|
||||
while fingerprints.contains(&message_fingerprint) {
|
||||
message_fingerprint = fingerprint(message, &path, message_fingerprint);
|
||||
message_fingerprint = fingerprint(message, message_fingerprint);
|
||||
}
|
||||
fingerprints.insert(message_fingerprint);
|
||||
|
||||
@@ -109,12 +109,12 @@ impl Serialize for SerializedMessages<'_> {
|
||||
}
|
||||
|
||||
/// Generate a unique fingerprint to identify a violation.
|
||||
fn fingerprint(message: &Message, project_path: &str, salt: u64) -> u64 {
|
||||
fn fingerprint(message: &Message, salt: u64) -> u64 {
|
||||
let Message {
|
||||
kind,
|
||||
range: _,
|
||||
fix: _fix,
|
||||
file: _,
|
||||
file,
|
||||
noqa_offset: _,
|
||||
} = message;
|
||||
|
||||
@@ -122,7 +122,7 @@ fn fingerprint(message: &Message, project_path: &str, salt: u64) -> u64 {
|
||||
|
||||
salt.hash(&mut hasher);
|
||||
kind.name.hash(&mut hasher);
|
||||
project_path.hash(&mut hasher);
|
||||
file.name().hash(&mut hasher);
|
||||
|
||||
hasher.finish()
|
||||
}
|
||||
|
||||
@@ -125,8 +125,8 @@ impl Renamer {
|
||||
let scope_id = scope.get_all(name).find_map(|binding_id| {
|
||||
let binding = semantic.binding(binding_id);
|
||||
match binding.kind {
|
||||
BindingKind::Global(_) => Some(ScopeId::global()),
|
||||
BindingKind::Nonlocal(_, scope_id) => Some(scope_id),
|
||||
BindingKind::Global => Some(ScopeId::global()),
|
||||
BindingKind::Nonlocal(symbol_id) => Some(symbol_id),
|
||||
_ => None,
|
||||
}
|
||||
});
|
||||
@@ -266,8 +266,8 @@ impl Renamer {
|
||||
| BindingKind::LoopVar
|
||||
| BindingKind::ComprehensionVar
|
||||
| BindingKind::WithItemVar
|
||||
| BindingKind::Global(_)
|
||||
| BindingKind::Nonlocal(_, _)
|
||||
| BindingKind::Global
|
||||
| BindingKind::Nonlocal(_)
|
||||
| BindingKind::ClassDefinition(_)
|
||||
| BindingKind::FunctionDefinition(_)
|
||||
| BindingKind::Deletion
|
||||
|
||||
@@ -93,7 +93,7 @@ impl Violation for SysVersion2 {
|
||||
/// ## Why is this bad?
|
||||
/// If the current major or minor version consists of multiple digits,
|
||||
/// `sys.version[0]` will select the first digit of the major version number
|
||||
/// only (e.g., `"10.2"` would evaluate to `"1"`). This is likely unintended,
|
||||
/// only (e.g., `"3.10"` would evaluate to `"1"`). This is likely unintended,
|
||||
/// and can lead to subtle bugs if the version string is used to test against a
|
||||
/// major version number.
|
||||
///
|
||||
|
||||
@@ -16,7 +16,6 @@ mod tests {
|
||||
#[test_case(Rule::BlockingHttpCallInAsyncFunction, Path::new("ASYNC100.py"))]
|
||||
#[test_case(Rule::OpenSleepOrSubprocessInAsyncFunction, Path::new("ASYNC101.py"))]
|
||||
#[test_case(Rule::BlockingOsCallInAsyncFunction, Path::new("ASYNC102.py"))]
|
||||
#[test_case(Rule::SleepForeverCall, Path::new("ASYNC116.py"))]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
pub(crate) use blocking_http_call::*;
|
||||
pub(crate) use blocking_os_call::*;
|
||||
pub(crate) use open_sleep_or_subprocess_call::*;
|
||||
pub(crate) use sleep_forever_call::*;
|
||||
|
||||
mod blocking_http_call;
|
||||
mod blocking_os_call;
|
||||
mod open_sleep_or_subprocess_call;
|
||||
mod sleep_forever_call;
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{Expr, ExprCall, ExprNumberLiteral, Number};
|
||||
use ruff_python_semantic::Modules;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::{checkers::ast::Checker, importer::ImportRequest};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `trio.sleep()` with an interval greater than 24 hours.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// `trio.sleep()` with an interval greater than 24 hours is usually intended
|
||||
/// to sleep indefinitely. Instead of using a large interval,
|
||||
/// `trio.sleep_forever()` better conveys the intent.
|
||||
///
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import trio
|
||||
///
|
||||
///
|
||||
/// async def func():
|
||||
/// await trio.sleep(86401)
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// import trio
|
||||
///
|
||||
///
|
||||
/// async def func():
|
||||
/// await trio.sleep_forever()
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct SleepForeverCall;
|
||||
|
||||
impl Violation for SleepForeverCall {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("`trio.sleep()` with >24 hour interval should usually be `trio.sleep_forever()`")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
Some(format!("Replace with `trio.sleep_forever()`"))
|
||||
}
|
||||
}
|
||||
|
||||
/// ASYNC116
|
||||
pub(crate) fn sleep_forever_call(checker: &mut Checker, call: &ExprCall) {
|
||||
if !checker.semantic().seen_module(Modules::TRIO) {
|
||||
return;
|
||||
}
|
||||
|
||||
if call.arguments.len() != 1 {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(arg) = call.arguments.find_argument("seconds", 0) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if !checker
|
||||
.semantic()
|
||||
.resolve_qualified_name(call.func.as_ref())
|
||||
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["trio", "sleep"]))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let Expr::NumberLiteral(ExprNumberLiteral { value, .. }) = arg else {
|
||||
return;
|
||||
};
|
||||
|
||||
// TODO(ekohilas): Replace with Duration::from_days(1).as_secs(); when available.
|
||||
let one_day_in_secs = 60 * 60 * 24;
|
||||
match value {
|
||||
Number::Int(int_value) => {
|
||||
let Some(int_value) = int_value.as_u64() else {
|
||||
return;
|
||||
};
|
||||
if int_value <= one_day_in_secs {
|
||||
return;
|
||||
}
|
||||
}
|
||||
Number::Float(float_value) =>
|
||||
{
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
if *float_value <= one_day_in_secs as f64 {
|
||||
return;
|
||||
}
|
||||
}
|
||||
Number::Complex { .. } => return,
|
||||
}
|
||||
|
||||
let mut diagnostic = Diagnostic::new(SleepForeverCall, call.range());
|
||||
let replacement_function = "sleep_forever";
|
||||
diagnostic.try_set_fix(|| {
|
||||
let (import_edit, binding) = checker.importer().get_or_import_symbol(
|
||||
&ImportRequest::import_from("trio", replacement_function),
|
||||
call.func.start(),
|
||||
checker.semantic(),
|
||||
)?;
|
||||
let reference_edit = Edit::range_replacement(binding, call.func.range());
|
||||
let arg_edit = Edit::range_replacement("()".to_string(), call.arguments.range());
|
||||
Ok(Fix::unsafe_edits(import_edit, [reference_edit, arg_edit]))
|
||||
});
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_async/mod.rs
|
||||
---
|
||||
ASYNC116.py:11:11: ASYNC116 [*] `trio.sleep()` with >24 hour interval should usually be `trio.sleep_forever()`
|
||||
|
|
||||
10 | # These examples are probably not meant to ever wake up:
|
||||
11 | await trio.sleep(100000) # error: 116, "async"
|
||||
| ^^^^^^^^^^^^^^^^^^ ASYNC116
|
||||
12 |
|
||||
13 | # 'inf literal' overflow trick
|
||||
|
|
||||
= help: Replace with `trio.sleep_forever()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
8 8 | import trio
|
||||
9 9 |
|
||||
10 10 | # These examples are probably not meant to ever wake up:
|
||||
11 |- await trio.sleep(100000) # error: 116, "async"
|
||||
11 |+ await trio.sleep_forever() # error: 116, "async"
|
||||
12 12 |
|
||||
13 13 | # 'inf literal' overflow trick
|
||||
14 14 | await trio.sleep(1e999) # error: 116, "async"
|
||||
|
||||
ASYNC116.py:14:11: ASYNC116 [*] `trio.sleep()` with >24 hour interval should usually be `trio.sleep_forever()`
|
||||
|
|
||||
13 | # 'inf literal' overflow trick
|
||||
14 | await trio.sleep(1e999) # error: 116, "async"
|
||||
| ^^^^^^^^^^^^^^^^^ ASYNC116
|
||||
15 |
|
||||
16 | await trio.sleep(86399)
|
||||
|
|
||||
= help: Replace with `trio.sleep_forever()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
11 11 | await trio.sleep(100000) # error: 116, "async"
|
||||
12 12 |
|
||||
13 13 | # 'inf literal' overflow trick
|
||||
14 |- await trio.sleep(1e999) # error: 116, "async"
|
||||
14 |+ await trio.sleep_forever() # error: 116, "async"
|
||||
15 15 |
|
||||
16 16 | await trio.sleep(86399)
|
||||
17 17 | await trio.sleep(86400)
|
||||
|
||||
ASYNC116.py:18:11: ASYNC116 [*] `trio.sleep()` with >24 hour interval should usually be `trio.sleep_forever()`
|
||||
|
|
||||
16 | await trio.sleep(86399)
|
||||
17 | await trio.sleep(86400)
|
||||
18 | await trio.sleep(86400.01) # error: 116, "async"
|
||||
| ^^^^^^^^^^^^^^^^^^^^ ASYNC116
|
||||
19 | await trio.sleep(86401) # error: 116, "async"
|
||||
|
|
||||
= help: Replace with `trio.sleep_forever()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
15 15 |
|
||||
16 16 | await trio.sleep(86399)
|
||||
17 17 | await trio.sleep(86400)
|
||||
18 |- await trio.sleep(86400.01) # error: 116, "async"
|
||||
18 |+ await trio.sleep_forever() # error: 116, "async"
|
||||
19 19 | await trio.sleep(86401) # error: 116, "async"
|
||||
20 20 |
|
||||
21 21 | await trio.sleep(-1) # will raise a runtime error
|
||||
|
||||
ASYNC116.py:19:11: ASYNC116 [*] `trio.sleep()` with >24 hour interval should usually be `trio.sleep_forever()`
|
||||
|
|
||||
17 | await trio.sleep(86400)
|
||||
18 | await trio.sleep(86400.01) # error: 116, "async"
|
||||
19 | await trio.sleep(86401) # error: 116, "async"
|
||||
| ^^^^^^^^^^^^^^^^^ ASYNC116
|
||||
20 |
|
||||
21 | await trio.sleep(-1) # will raise a runtime error
|
||||
|
|
||||
= help: Replace with `trio.sleep_forever()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
16 16 | await trio.sleep(86399)
|
||||
17 17 | await trio.sleep(86400)
|
||||
18 18 | await trio.sleep(86400.01) # error: 116, "async"
|
||||
19 |- await trio.sleep(86401) # error: 116, "async"
|
||||
19 |+ await trio.sleep_forever() # error: 116, "async"
|
||||
20 20 |
|
||||
21 21 | await trio.sleep(-1) # will raise a runtime error
|
||||
22 22 | await trio.sleep(0) # handled by different check
|
||||
|
||||
ASYNC116.py:48:5: ASYNC116 [*] `trio.sleep()` with >24 hour interval should usually be `trio.sleep_forever()`
|
||||
|
|
||||
47 | # does not require the call to be awaited, nor in an async fun
|
||||
48 | trio.sleep(86401) # error: 116, "async"
|
||||
| ^^^^^^^^^^^^^^^^^ ASYNC116
|
||||
49 | # also checks that we don't break visit_Call
|
||||
50 | trio.run(trio.sleep(86401)) # error: 116, "async"
|
||||
|
|
||||
= help: Replace with `trio.sleep_forever()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
45 45 | import trio
|
||||
46 46 |
|
||||
47 47 | # does not require the call to be awaited, nor in an async fun
|
||||
48 |- trio.sleep(86401) # error: 116, "async"
|
||||
48 |+ trio.sleep_forever() # error: 116, "async"
|
||||
49 49 | # also checks that we don't break visit_Call
|
||||
50 50 | trio.run(trio.sleep(86401)) # error: 116, "async"
|
||||
51 51 |
|
||||
|
||||
ASYNC116.py:50:14: ASYNC116 [*] `trio.sleep()` with >24 hour interval should usually be `trio.sleep_forever()`
|
||||
|
|
||||
48 | trio.sleep(86401) # error: 116, "async"
|
||||
49 | # also checks that we don't break visit_Call
|
||||
50 | trio.run(trio.sleep(86401)) # error: 116, "async"
|
||||
| ^^^^^^^^^^^^^^^^^ ASYNC116
|
||||
|
|
||||
= help: Replace with `trio.sleep_forever()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
47 47 | # does not require the call to be awaited, nor in an async fun
|
||||
48 48 | trio.sleep(86401) # error: 116, "async"
|
||||
49 49 | # also checks that we don't break visit_Call
|
||||
50 |- trio.run(trio.sleep(86401)) # error: 116, "async"
|
||||
50 |+ trio.run(trio.sleep_forever()) # error: 116, "async"
|
||||
51 51 |
|
||||
52 52 |
|
||||
53 53 | async def import_from_trio():
|
||||
|
||||
ASYNC116.py:57:11: ASYNC116 [*] `trio.sleep()` with >24 hour interval should usually be `trio.sleep_forever()`
|
||||
|
|
||||
56 | # catch from import
|
||||
57 | await sleep(86401) # error: 116, "async"
|
||||
| ^^^^^^^^^^^^ ASYNC116
|
||||
|
|
||||
= help: Replace with `trio.sleep_forever()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
2 2 | # ASYNCIO_NO_ERROR - no asyncio.sleep_forever, so check intentionally doesn't trigger.
|
||||
3 3 | import math
|
||||
4 4 | from math import inf
|
||||
5 |+from trio import sleep_forever
|
||||
5 6 |
|
||||
6 7 |
|
||||
7 8 | async def import_trio():
|
||||
--------------------------------------------------------------------------------
|
||||
54 55 | from trio import sleep
|
||||
55 56 |
|
||||
56 57 | # catch from import
|
||||
57 |- await sleep(86401) # error: 116, "async"
|
||||
58 |+ await sleep_forever() # error: 116, "async"
|
||||
@@ -58,7 +58,7 @@ pub(crate) fn request_without_timeout(checker: &mut Checker, call: &ast::ExprCal
|
||||
qualified_name.segments(),
|
||||
[
|
||||
"requests",
|
||||
"get" | "options" | "head" | "post" | "put" | "patch" | "delete" | "request"
|
||||
"get" | "options" | "head" | "post" | "put" | "patch" | "delete"
|
||||
]
|
||||
)
|
||||
})
|
||||
|
||||
@@ -2,7 +2,6 @@ use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::comparable::ComparableExpr;
|
||||
use ruff_python_ast::parenthesize::parenthesized_range;
|
||||
use ruff_python_ast::ExprGenerator;
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
|
||||
@@ -27,14 +26,12 @@ use super::helpers;
|
||||
/// ```python
|
||||
/// list(f(x) for x in foo)
|
||||
/// list(x for x in foo)
|
||||
/// list((x for x in foo))
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// [f(x) for x in foo]
|
||||
/// list(foo)
|
||||
/// list(foo)
|
||||
/// ```
|
||||
///
|
||||
/// ## Fix safety
|
||||
@@ -79,10 +76,7 @@ pub(crate) fn unnecessary_generator_list(checker: &mut Checker, call: &ast::Expr
|
||||
}
|
||||
|
||||
let Some(ExprGenerator {
|
||||
elt,
|
||||
generators,
|
||||
parenthesized,
|
||||
..
|
||||
elt, generators, ..
|
||||
}) = argument.as_generator_expr()
|
||||
else {
|
||||
return;
|
||||
@@ -131,28 +125,7 @@ pub(crate) fn unnecessary_generator_list(checker: &mut Checker, call: &ast::Expr
|
||||
call.end(),
|
||||
);
|
||||
|
||||
// Remove the inner parentheses, if the expression is a generator. The easiest way to do
|
||||
// this reliably is to use the printer.
|
||||
if *parenthesized {
|
||||
// The generator's range will include the innermost parentheses, but it could be
|
||||
// surrounded by additional parentheses.
|
||||
let range = parenthesized_range(
|
||||
argument.into(),
|
||||
(&call.arguments).into(),
|
||||
checker.indexer().comment_ranges(),
|
||||
checker.locator().contents(),
|
||||
)
|
||||
.unwrap_or(argument.range());
|
||||
|
||||
// The generator always parenthesizes the expression; trim the parentheses.
|
||||
let generator = checker.generator().expr(argument);
|
||||
let generator = generator[1..generator.len() - 1].to_string();
|
||||
|
||||
let replacement = Edit::range_replacement(generator, range);
|
||||
Fix::unsafe_edits(call_start, [call_end, replacement])
|
||||
} else {
|
||||
Fix::unsafe_edits(call_start, [call_end])
|
||||
}
|
||||
Fix::unsafe_edits(call_start, [call_end])
|
||||
});
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
|
||||
@@ -73,7 +73,7 @@ C400.py:10:5: C400 [*] Unnecessary generator (rewrite using `list()`)
|
||||
12 | | )
|
||||
| |_^ C400
|
||||
13 |
|
||||
14 | # Strip parentheses from inner generators.
|
||||
14 | # Not built-in list.
|
||||
|
|
||||
= help: Rewrite using `list()`
|
||||
|
||||
@@ -86,66 +86,5 @@ C400.py:10:5: C400 [*] Unnecessary generator (rewrite using `list()`)
|
||||
12 |-)
|
||||
10 |+x = list(range(3))
|
||||
13 11 |
|
||||
14 12 | # Strip parentheses from inner generators.
|
||||
15 13 | list((2 * x for x in range(3)))
|
||||
|
||||
C400.py:15:1: C400 [*] Unnecessary generator (rewrite as a `list` comprehension)
|
||||
|
|
||||
14 | # Strip parentheses from inner generators.
|
||||
15 | list((2 * x for x in range(3)))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C400
|
||||
16 | list(((2 * x for x in range(3))))
|
||||
17 | list((((2 * x for x in range(3)))))
|
||||
|
|
||||
= help: Rewrite as a `list` comprehension
|
||||
|
||||
ℹ Unsafe fix
|
||||
12 12 | )
|
||||
13 13 |
|
||||
14 14 | # Strip parentheses from inner generators.
|
||||
15 |-list((2 * x for x in range(3)))
|
||||
15 |+[2 * x for x in range(3)]
|
||||
16 16 | list(((2 * x for x in range(3))))
|
||||
17 17 | list((((2 * x for x in range(3)))))
|
||||
18 18 |
|
||||
|
||||
C400.py:16:1: C400 [*] Unnecessary generator (rewrite as a `list` comprehension)
|
||||
|
|
||||
14 | # Strip parentheses from inner generators.
|
||||
15 | list((2 * x for x in range(3)))
|
||||
16 | list(((2 * x for x in range(3))))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C400
|
||||
17 | list((((2 * x for x in range(3)))))
|
||||
|
|
||||
= help: Rewrite as a `list` comprehension
|
||||
|
||||
ℹ Unsafe fix
|
||||
13 13 |
|
||||
14 14 | # Strip parentheses from inner generators.
|
||||
15 15 | list((2 * x for x in range(3)))
|
||||
16 |-list(((2 * x for x in range(3))))
|
||||
16 |+[2 * x for x in range(3)]
|
||||
17 17 | list((((2 * x for x in range(3)))))
|
||||
18 18 |
|
||||
19 19 | # Not built-in list.
|
||||
|
||||
C400.py:17:1: C400 [*] Unnecessary generator (rewrite as a `list` comprehension)
|
||||
|
|
||||
15 | list((2 * x for x in range(3)))
|
||||
16 | list(((2 * x for x in range(3))))
|
||||
17 | list((((2 * x for x in range(3)))))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C400
|
||||
18 |
|
||||
19 | # Not built-in list.
|
||||
|
|
||||
= help: Rewrite as a `list` comprehension
|
||||
|
||||
ℹ Unsafe fix
|
||||
14 14 | # Strip parentheses from inner generators.
|
||||
15 15 | list((2 * x for x in range(3)))
|
||||
16 16 | list(((2 * x for x in range(3))))
|
||||
17 |-list((((2 * x for x in range(3)))))
|
||||
17 |+[2 * x for x in range(3)]
|
||||
18 18 |
|
||||
19 19 | # Not built-in list.
|
||||
20 20 | def list(*args, **kwargs):
|
||||
14 12 | # Not built-in list.
|
||||
15 13 | def list(*args, **kwargs):
|
||||
|
||||
@@ -43,7 +43,6 @@ mod tests {
|
||||
#[test_case(Path::new("no_future_import_uses_union_inner.py"))]
|
||||
#[test_case(Path::new("ok_no_types.py"))]
|
||||
#[test_case(Path::new("ok_uses_future.py"))]
|
||||
#[test_case(Path::new("ok_quoted_type.py"))]
|
||||
fn fa102(path: &Path) -> Result<()> {
|
||||
let snapshot = format!("fa102_{}", path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
|
||||
@@ -7,6 +7,7 @@ use ruff_python_ast::Expr;
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::importer::Importer;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of PEP 585- and PEP 604-style type annotations in Python
|
||||
@@ -86,11 +87,13 @@ impl AlwaysFixableViolation for FutureRequiredTypeAnnotation {
|
||||
/// FA102
|
||||
pub(crate) fn future_required_type_annotation(checker: &mut Checker, expr: &Expr, reason: Reason) {
|
||||
let mut diagnostic = Diagnostic::new(FutureRequiredTypeAnnotation { reason }, expr.range());
|
||||
let required_import = AnyImport::ImportFrom(ImportFrom::member("__future__", "annotations"));
|
||||
diagnostic.set_fix(Fix::unsafe_edit(
|
||||
checker
|
||||
.importer()
|
||||
.add_import(&required_import, TextSize::default()),
|
||||
));
|
||||
if let Some(python_ast) = checker.semantic().definitions.python_ast() {
|
||||
let required_import =
|
||||
AnyImport::ImportFrom(ImportFrom::member("__future__", "annotations"));
|
||||
diagnostic.set_fix(Fix::unsafe_edit(
|
||||
Importer::new(python_ast, checker.locator(), checker.stylist())
|
||||
.add_import(&required_import, TextSize::default()),
|
||||
));
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs
|
||||
---
|
||||
no_future_import_uses_lowercase.py:2:13: FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
|
|
||||
1 | def main() -> None:
|
||||
2 | a_list: list[str] = []
|
||||
| ^^^^^^^^^ FA102
|
||||
3 | a_list.append("hello")
|
||||
|
|
||||
= help: Add `from __future__ import annotations`
|
||||
|
||||
ℹ Unsafe fix
|
||||
1 |+from __future__ import annotations
|
||||
1 2 | def main() -> None:
|
||||
2 3 | a_list: list[str] = []
|
||||
3 4 | a_list.append("hello")
|
||||
|
||||
no_future_import_uses_lowercase.py:6:14: FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
|
|
||||
6 | def hello(y: dict[str, int]) -> None:
|
||||
@@ -14,3 +29,5 @@ no_future_import_uses_lowercase.py:6:14: FA102 [*] Missing `from __future__ impo
|
||||
1 2 | def main() -> None:
|
||||
2 3 | a_list: list[str] = []
|
||||
3 4 | a_list.append("hello")
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,36 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs
|
||||
---
|
||||
no_future_import_uses_union.py:2:13: FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
|
|
||||
1 | def main() -> None:
|
||||
2 | a_list: list[str] | None = []
|
||||
| ^^^^^^^^^ FA102
|
||||
3 | a_list.append("hello")
|
||||
|
|
||||
= help: Add `from __future__ import annotations`
|
||||
|
||||
ℹ Unsafe fix
|
||||
1 |+from __future__ import annotations
|
||||
1 2 | def main() -> None:
|
||||
2 3 | a_list: list[str] | None = []
|
||||
3 4 | a_list.append("hello")
|
||||
|
||||
no_future_import_uses_union.py:2:13: FA102 [*] Missing `from __future__ import annotations`, but uses PEP 604 union
|
||||
|
|
||||
1 | def main() -> None:
|
||||
2 | a_list: list[str] | None = []
|
||||
| ^^^^^^^^^^^^^^^^ FA102
|
||||
3 | a_list.append("hello")
|
||||
|
|
||||
= help: Add `from __future__ import annotations`
|
||||
|
||||
ℹ Unsafe fix
|
||||
1 |+from __future__ import annotations
|
||||
1 2 | def main() -> None:
|
||||
2 3 | a_list: list[str] | None = []
|
||||
3 4 | a_list.append("hello")
|
||||
|
||||
no_future_import_uses_union.py:6:14: FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
|
|
||||
6 | def hello(y: dict[str, int] | None) -> None:
|
||||
@@ -28,3 +58,5 @@ no_future_import_uses_union.py:6:14: FA102 [*] Missing `from __future__ import a
|
||||
1 2 | def main() -> None:
|
||||
2 3 | a_list: list[str] | None = []
|
||||
3 4 | a_list.append("hello")
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,36 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs
|
||||
---
|
||||
no_future_import_uses_union_inner.py:2:13: FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
|
|
||||
1 | def main() -> None:
|
||||
2 | a_list: list[str | None] = []
|
||||
| ^^^^^^^^^^^^^^^^ FA102
|
||||
3 | a_list.append("hello")
|
||||
|
|
||||
= help: Add `from __future__ import annotations`
|
||||
|
||||
ℹ Unsafe fix
|
||||
1 |+from __future__ import annotations
|
||||
1 2 | def main() -> None:
|
||||
2 3 | a_list: list[str | None] = []
|
||||
3 4 | a_list.append("hello")
|
||||
|
||||
no_future_import_uses_union_inner.py:2:18: FA102 [*] Missing `from __future__ import annotations`, but uses PEP 604 union
|
||||
|
|
||||
1 | def main() -> None:
|
||||
2 | a_list: list[str | None] = []
|
||||
| ^^^^^^^^^^ FA102
|
||||
3 | a_list.append("hello")
|
||||
|
|
||||
= help: Add `from __future__ import annotations`
|
||||
|
||||
ℹ Unsafe fix
|
||||
1 |+from __future__ import annotations
|
||||
1 2 | def main() -> None:
|
||||
2 3 | a_list: list[str | None] = []
|
||||
3 4 | a_list.append("hello")
|
||||
|
||||
no_future_import_uses_union_inner.py:6:14: FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
|
|
||||
6 | def hello(y: dict[str | None, int]) -> None:
|
||||
@@ -30,3 +60,35 @@ no_future_import_uses_union_inner.py:6:19: FA102 [*] Missing `from __future__ im
|
||||
1 2 | def main() -> None:
|
||||
2 3 | a_list: list[str | None] = []
|
||||
3 4 | a_list.append("hello")
|
||||
|
||||
no_future_import_uses_union_inner.py:7:8: FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
|
|
||||
6 | def hello(y: dict[str | None, int]) -> None:
|
||||
7 | z: tuple[str, str | None, str] = tuple(y)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ FA102
|
||||
8 | del z
|
||||
|
|
||||
= help: Add `from __future__ import annotations`
|
||||
|
||||
ℹ Unsafe fix
|
||||
1 |+from __future__ import annotations
|
||||
1 2 | def main() -> None:
|
||||
2 3 | a_list: list[str | None] = []
|
||||
3 4 | a_list.append("hello")
|
||||
|
||||
no_future_import_uses_union_inner.py:7:19: FA102 [*] Missing `from __future__ import annotations`, but uses PEP 604 union
|
||||
|
|
||||
6 | def hello(y: dict[str | None, int]) -> None:
|
||||
7 | z: tuple[str, str | None, str] = tuple(y)
|
||||
| ^^^^^^^^^^ FA102
|
||||
8 | del z
|
||||
|
|
||||
= help: Add `from __future__ import annotations`
|
||||
|
||||
ℹ Unsafe fix
|
||||
1 |+from __future__ import annotations
|
||||
1 2 | def main() -> None:
|
||||
2 3 | a_list: list[str | None] = []
|
||||
3 4 | a_list.append("hello")
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs
|
||||
---
|
||||
|
||||
@@ -10,7 +10,7 @@ G010.py:6:9: G010 [*] Logging statement uses `warn` instead of `warning`
|
||||
7 | log.warn("Hello world!") # This shouldn't be considered as a logger candidate
|
||||
8 | logger.warn("Hello world!")
|
||||
|
|
||||
= help: Convert to `warning`
|
||||
= help: Convert to `warn`
|
||||
|
||||
ℹ Safe fix
|
||||
3 3 |
|
||||
@@ -31,7 +31,7 @@ G010.py:8:8: G010 [*] Logging statement uses `warn` instead of `warning`
|
||||
9 |
|
||||
10 | logging . warn("Hello World!")
|
||||
|
|
||||
= help: Convert to `warning`
|
||||
= help: Convert to `warn`
|
||||
|
||||
ℹ Safe fix
|
||||
5 5 |
|
||||
@@ -52,7 +52,7 @@ G010.py:10:11: G010 [*] Logging statement uses `warn` instead of `warning`
|
||||
11 |
|
||||
12 | from logging import warn, warning, exception
|
||||
|
|
||||
= help: Convert to `warning`
|
||||
= help: Convert to `warn`
|
||||
|
||||
ℹ Safe fix
|
||||
7 7 | log.warn("Hello world!") # This shouldn't be considered as a logger candidate
|
||||
@@ -72,7 +72,7 @@ G010.py:13:1: G010 [*] Logging statement uses `warn` instead of `warning`
|
||||
14 | warning("foo")
|
||||
15 | exception("foo")
|
||||
|
|
||||
= help: Convert to `warning`
|
||||
= help: Convert to `warn`
|
||||
|
||||
ℹ Safe fix
|
||||
10 10 | logging . warn("Hello World!")
|
||||
|
||||
@@ -383,7 +383,7 @@ impl AlwaysFixableViolation for LoggingWarn {
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> String {
|
||||
"Convert to `warning`".to_string()
|
||||
"Convert to `warn`".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,10 +23,6 @@ mod tests {
|
||||
#[test_case(Rule::BadExitAnnotation, Path::new("PYI036.pyi"))]
|
||||
#[test_case(Rule::BadVersionInfoComparison, Path::new("PYI006.py"))]
|
||||
#[test_case(Rule::BadVersionInfoComparison, Path::new("PYI006.pyi"))]
|
||||
#[test_case(Rule::BadVersionInfoOrder, Path::new("PYI066.py"))]
|
||||
#[test_case(Rule::BadVersionInfoOrder, Path::new("PYI066.pyi"))]
|
||||
#[test_case(Rule::ByteStringUsage, Path::new("PYI057.py"))]
|
||||
#[test_case(Rule::ByteStringUsage, Path::new("PYI057.pyi"))]
|
||||
#[test_case(Rule::CollectionsNamedTuple, Path::new("PYI024.py"))]
|
||||
#[test_case(Rule::CollectionsNamedTuple, Path::new("PYI024.pyi"))]
|
||||
#[test_case(Rule::ComplexAssignmentInStub, Path::new("PYI017.py"))]
|
||||
@@ -67,8 +63,6 @@ mod tests {
|
||||
#[test_case(Rule::PatchVersionComparison, Path::new("PYI004.pyi"))]
|
||||
#[test_case(Rule::QuotedAnnotationInStub, Path::new("PYI020.py"))]
|
||||
#[test_case(Rule::QuotedAnnotationInStub, Path::new("PYI020.pyi"))]
|
||||
#[test_case(Rule::RedundantFinalLiteral, Path::new("PYI064.py"))]
|
||||
#[test_case(Rule::RedundantFinalLiteral, Path::new("PYI064.pyi"))]
|
||||
#[test_case(Rule::RedundantLiteralUnion, Path::new("PYI051.py"))]
|
||||
#[test_case(Rule::RedundantLiteralUnion, Path::new("PYI051.pyi"))]
|
||||
#[test_case(Rule::RedundantNumericUnion, Path::new("PYI041.py"))]
|
||||
|
||||
@@ -5,7 +5,6 @@ use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::Rule;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of comparators other than `<` and `>=` for
|
||||
@@ -58,61 +57,8 @@ impl Violation for BadVersionInfoComparison {
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for if-else statements with `sys.version_info` comparisons that use
|
||||
/// `<` comparators.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// As a convention, branches that correspond to newer Python versions should
|
||||
/// come first when using `sys.version_info` comparisons. This makes it easier
|
||||
/// to understand the desired behavior, which typically corresponds to the
|
||||
/// latest Python versions.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// import sys
|
||||
///
|
||||
/// if sys.version_info < (3, 10):
|
||||
///
|
||||
/// def read_data(x, *, preserve_order=True):
|
||||
/// ...
|
||||
///
|
||||
/// else:
|
||||
///
|
||||
/// def read_data(x):
|
||||
/// ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// if sys.version_info >= (3, 10):
|
||||
///
|
||||
/// def read_data(x):
|
||||
/// ...
|
||||
///
|
||||
/// else:
|
||||
///
|
||||
/// def read_data(x, *, preserve_order=True):
|
||||
/// ...
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct BadVersionInfoOrder;
|
||||
|
||||
impl Violation for BadVersionInfoOrder {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Use `>=` when using `if`-`else` with `sys.version_info` comparisons")
|
||||
}
|
||||
}
|
||||
|
||||
/// PYI006, PYI066
|
||||
pub(crate) fn bad_version_info_comparison(
|
||||
checker: &mut Checker,
|
||||
test: &Expr,
|
||||
has_else_clause: bool,
|
||||
) {
|
||||
/// PYI006
|
||||
pub(crate) fn bad_version_info_comparison(checker: &mut Checker, test: &Expr) {
|
||||
let Expr::Compare(ast::ExprCompare {
|
||||
left,
|
||||
ops,
|
||||
@@ -135,24 +81,11 @@ pub(crate) fn bad_version_info_comparison(
|
||||
return;
|
||||
}
|
||||
|
||||
if matches!(op, CmpOp::GtE) {
|
||||
// No issue to be raised, early exit.
|
||||
if matches!(op, CmpOp::Lt | CmpOp::GtE) {
|
||||
return;
|
||||
}
|
||||
|
||||
if matches!(op, CmpOp::Lt) {
|
||||
if checker.enabled(Rule::BadVersionInfoOrder) {
|
||||
if has_else_clause {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(BadVersionInfoOrder, test.range()));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if checker.enabled(Rule::BadVersionInfoComparison) {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(BadVersionInfoComparison, test.range()));
|
||||
};
|
||||
}
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(BadVersionInfoComparison, test.range()));
|
||||
}
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
use ruff_diagnostics::{Diagnostic, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
use ruff_python_semantic::Modules;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `typing.ByteString` or `collections.abc.ByteString`.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// `ByteString` has been deprecated since Python 3.9 and will be removed in
|
||||
/// Python 3.14. The Python documentation recommends using either
|
||||
/// `collections.abc.Buffer` (or the `typing_extensions` backport
|
||||
/// on Python <3.12) or a union like `bytes | bytearray | memoryview` instead.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// from typing import ByteString
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// from collections.abc import Buffer
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: The `ByteString` type](https://docs.python.org/3/library/typing.html#typing.ByteString)
|
||||
#[violation]
|
||||
pub struct ByteStringUsage {
|
||||
origin: ByteStringOrigin,
|
||||
}
|
||||
|
||||
impl Violation for ByteStringUsage {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::None;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let ByteStringUsage { origin } = self;
|
||||
format!("Do not use `{origin}.ByteString`, which has unclear semantics and is deprecated")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
enum ByteStringOrigin {
|
||||
Typing,
|
||||
CollectionsAbc,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ByteStringOrigin {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(match self {
|
||||
Self::Typing => "typing",
|
||||
Self::CollectionsAbc => "collections.abc",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// PYI057
|
||||
pub(crate) fn bytestring_attribute(checker: &mut Checker, attribute: &Expr) {
|
||||
let semantic = checker.semantic();
|
||||
if !semantic
|
||||
.seen
|
||||
.intersects(Modules::TYPING | Modules::COLLECTIONS)
|
||||
{
|
||||
return;
|
||||
}
|
||||
let Some(qualified_name) = semantic.resolve_qualified_name(attribute) else {
|
||||
return;
|
||||
};
|
||||
let origin = match qualified_name.segments() {
|
||||
["typing", "ByteString"] => ByteStringOrigin::Typing,
|
||||
["collections", "abc", "ByteString"] => ByteStringOrigin::CollectionsAbc,
|
||||
_ => return,
|
||||
};
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
ByteStringUsage { origin },
|
||||
attribute.range(),
|
||||
));
|
||||
}
|
||||
|
||||
/// PYI057
|
||||
pub(crate) fn bytestring_import(checker: &mut Checker, import_from: &ast::StmtImportFrom) {
|
||||
let ast::StmtImportFrom { names, module, .. } = import_from;
|
||||
|
||||
let module_id = match module {
|
||||
Some(module) => module.id.as_str(),
|
||||
None => return,
|
||||
};
|
||||
|
||||
let origin = match module_id {
|
||||
"typing" => ByteStringOrigin::Typing,
|
||||
"collections.abc" => ByteStringOrigin::CollectionsAbc,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
for name in names {
|
||||
if name.name.as_str() == "ByteString" {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(ByteStringUsage { origin }, name.range()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
pub(crate) use any_eq_ne_annotation::*;
|
||||
pub(crate) use bad_generator_return_type::*;
|
||||
pub(crate) use bad_version_info_comparison::*;
|
||||
pub(crate) use bytestring_usage::*;
|
||||
pub(crate) use collections_named_tuple::*;
|
||||
pub(crate) use complex_assignment_in_stub::*;
|
||||
pub(crate) use complex_if_statement_in_stub::*;
|
||||
@@ -22,7 +21,6 @@ pub(crate) use pass_in_class_body::*;
|
||||
pub(crate) use pass_statement_stub_body::*;
|
||||
pub(crate) use prefix_type_params::*;
|
||||
pub(crate) use quoted_annotation_in_stub::*;
|
||||
pub(crate) use redundant_final_literal::*;
|
||||
pub(crate) use redundant_literal_union::*;
|
||||
pub(crate) use redundant_numeric_union::*;
|
||||
pub(crate) use simple_defaults::*;
|
||||
@@ -43,7 +41,6 @@ pub(crate) use unused_private_type_definition::*;
|
||||
mod any_eq_ne_annotation;
|
||||
mod bad_generator_return_type;
|
||||
mod bad_version_info_comparison;
|
||||
mod bytestring_usage;
|
||||
mod collections_named_tuple;
|
||||
mod complex_assignment_in_stub;
|
||||
mod complex_if_statement_in_stub;
|
||||
@@ -64,7 +61,6 @@ mod pass_in_class_body;
|
||||
mod pass_statement_stub_body;
|
||||
mod prefix_type_params;
|
||||
mod quoted_annotation_in_stub;
|
||||
mod redundant_final_literal;
|
||||
mod redundant_literal_union;
|
||||
mod redundant_numeric_union;
|
||||
mod simple_defaults;
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, comparable::ComparableExpr};
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::fix::snippet::SourceCodeSnippet;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for redundant `Final[Literal[...]]` annotations.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// A `Final[Literal[...]]` annotation can be replaced with `Final`; the literal
|
||||
/// use is unnecessary.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// x: Final[Literal[42]]
|
||||
/// y: Final[Literal[42]] = 42
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// x: Final = 42
|
||||
/// y: Final = 42
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct RedundantFinalLiteral {
|
||||
literal: SourceCodeSnippet,
|
||||
}
|
||||
|
||||
impl Violation for RedundantFinalLiteral {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let RedundantFinalLiteral { literal } = self;
|
||||
format!(
|
||||
"`Final[Literal[{literal}]]` can be replaced with a bare `Final`",
|
||||
literal = literal.truncated_display()
|
||||
)
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
Some("Replace with `Final`".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// PYI064
|
||||
pub(crate) fn redundant_final_literal(checker: &mut Checker, ann_assign: &ast::StmtAnnAssign) {
|
||||
if !checker.semantic().seen_typing() {
|
||||
return;
|
||||
}
|
||||
|
||||
let ast::StmtAnnAssign {
|
||||
value: assign_value,
|
||||
annotation,
|
||||
..
|
||||
} = ann_assign;
|
||||
|
||||
let ast::Expr::Subscript(annotation) = &**annotation else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Ensure it is `Final[Literal[...]]`.
|
||||
let ast::Expr::Subscript(ast::ExprSubscript {
|
||||
value,
|
||||
slice: literal,
|
||||
..
|
||||
}) = &*annotation.slice
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if !checker.semantic().match_typing_expr(value, "Literal") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Discards tuples like `Literal[1, 2, 3]` and complex literals like `Literal[{1, 2}]`.
|
||||
if !matches!(
|
||||
&**literal,
|
||||
ast::Expr::StringLiteral(_)
|
||||
| ast::Expr::BytesLiteral(_)
|
||||
| ast::Expr::NumberLiteral(_)
|
||||
| ast::Expr::BooleanLiteral(_)
|
||||
| ast::Expr::NoneLiteral(_)
|
||||
| ast::Expr::EllipsisLiteral(_)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
RedundantFinalLiteral {
|
||||
literal: SourceCodeSnippet::from_str(checker.locator().slice(literal.range())),
|
||||
},
|
||||
ann_assign.range(),
|
||||
);
|
||||
|
||||
// The literal value and the assignment value being different doesn't make sense, so we skip
|
||||
// fixing in that case.
|
||||
if let Some(assign_value) = assign_value.as_ref() {
|
||||
if ComparableExpr::from(assign_value) == ComparableExpr::from(literal) {
|
||||
diagnostic.set_fix(generate_fix(annotation, None, checker.locator()));
|
||||
}
|
||||
} else {
|
||||
diagnostic.set_fix(generate_fix(annotation, Some(literal), checker.locator()));
|
||||
}
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
/// Generate a fix to convert a `Final[Literal[...]]` annotation to a `Final` annotation.
|
||||
fn generate_fix(
|
||||
annotation: &ast::ExprSubscript,
|
||||
literal: Option<&ast::Expr>,
|
||||
locator: &Locator,
|
||||
) -> Fix {
|
||||
// Remove the `Literal[...]` part from `Final[Literal[...]]`.
|
||||
let deletion = Edit::range_deletion(
|
||||
annotation
|
||||
.slice
|
||||
.range()
|
||||
.sub_start(TextSize::new(1))
|
||||
.add_end(TextSize::new(1)),
|
||||
);
|
||||
|
||||
// If a literal was provided, insert an assignment.
|
||||
//
|
||||
// For example, change `x: Final[Literal[42]]` to `x: Final = 42`.
|
||||
if let Some(literal) = literal {
|
||||
let assignment = Edit::insertion(
|
||||
format!(
|
||||
" = {literal_source}",
|
||||
literal_source = locator.slice(literal)
|
||||
),
|
||||
annotation.end(),
|
||||
);
|
||||
Fix::safe_edits(deletion, std::iter::once(assignment))
|
||||
} else {
|
||||
Fix::safe_edit(deletion)
|
||||
}
|
||||
}
|
||||
@@ -47,30 +47,16 @@ PYI006.pyi:16:4: PYI006 Use `<` or `>=` for `sys.version_info` comparisons
|
||||
15 |
|
||||
16 | if sys.version_info > (3, 10): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI006
|
||||
17 | elif sys.version_info > (3, 11): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons
|
||||
17 |
|
||||
18 | if python_version > (3, 10): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons
|
||||
|
|
||||
|
||||
PYI006.pyi:17:6: PYI006 Use `<` or `>=` for `sys.version_info` comparisons
|
||||
PYI006.pyi:18:4: PYI006 Use `<` or `>=` for `sys.version_info` comparisons
|
||||
|
|
||||
16 | if sys.version_info > (3, 10): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons
|
||||
17 | elif sys.version_info > (3, 11): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI006
|
||||
18 |
|
||||
19 | if python_version > (3, 10): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons
|
||||
|
|
||||
|
||||
PYI006.pyi:19:4: PYI006 Use `<` or `>=` for `sys.version_info` comparisons
|
||||
|
|
||||
17 | elif sys.version_info > (3, 11): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons
|
||||
18 |
|
||||
19 | if python_version > (3, 10): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons
|
||||
17 |
|
||||
18 | if python_version > (3, 10): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ PYI006
|
||||
20 | elif python_version == (3, 11): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons
|
||||
|
|
||||
|
||||
PYI006.pyi:20:6: PYI006 Use `<` or `>=` for `sys.version_info` comparisons
|
||||
|
|
||||
19 | if python_version > (3, 10): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons
|
||||
20 | elif python_version == (3, 11): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^ PYI006
|
||||
|
|
||||
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs
|
||||
---
|
||||
PYI057.py:4:20: PYI057 Do not use `typing.ByteString`, which has unclear semantics and is deprecated
|
||||
|
|
||||
2 | import collections.abc
|
||||
3 | import foo
|
||||
4 | from typing import ByteString
|
||||
| ^^^^^^^^^^ PYI057
|
||||
5 | from collections.abc import ByteString
|
||||
6 | from foo import ByteString
|
||||
|
|
||||
|
||||
PYI057.py:5:29: PYI057 Do not use `collections.abc.ByteString`, which has unclear semantics and is deprecated
|
||||
|
|
||||
3 | import foo
|
||||
4 | from typing import ByteString
|
||||
5 | from collections.abc import ByteString
|
||||
| ^^^^^^^^^^ PYI057
|
||||
6 | from foo import ByteString
|
||||
|
|
||||
|
||||
PYI057.py:8:4: PYI057 Do not use `typing.ByteString`, which has unclear semantics and is deprecated
|
||||
|
|
||||
6 | from foo import ByteString
|
||||
7 |
|
||||
8 | a: typing.ByteString
|
||||
| ^^^^^^^^^^^^^^^^^ PYI057
|
||||
9 | b: collections.abc.ByteString
|
||||
10 | c: foo.ByteString
|
||||
|
|
||||
|
||||
PYI057.py:9:4: PYI057 Do not use `collections.abc.ByteString`, which has unclear semantics and is deprecated
|
||||
|
|
||||
8 | a: typing.ByteString
|
||||
9 | b: collections.abc.ByteString
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI057
|
||||
10 | c: foo.ByteString
|
||||
|
|
||||
@@ -1,39 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs
|
||||
---
|
||||
PYI057.pyi:4:20: PYI057 Do not use `typing.ByteString`, which has unclear semantics and is deprecated
|
||||
|
|
||||
2 | import collections.abc
|
||||
3 | import foo
|
||||
4 | from typing import ByteString
|
||||
| ^^^^^^^^^^ PYI057
|
||||
5 | from collections.abc import ByteString
|
||||
6 | from foo import ByteString
|
||||
|
|
||||
|
||||
PYI057.pyi:5:29: PYI057 Do not use `collections.abc.ByteString`, which has unclear semantics and is deprecated
|
||||
|
|
||||
3 | import foo
|
||||
4 | from typing import ByteString
|
||||
5 | from collections.abc import ByteString
|
||||
| ^^^^^^^^^^ PYI057
|
||||
6 | from foo import ByteString
|
||||
|
|
||||
|
||||
PYI057.pyi:8:4: PYI057 Do not use `typing.ByteString`, which has unclear semantics and is deprecated
|
||||
|
|
||||
6 | from foo import ByteString
|
||||
7 |
|
||||
8 | a: typing.ByteString
|
||||
| ^^^^^^^^^^^^^^^^^ PYI057
|
||||
9 | b: collections.abc.ByteString
|
||||
10 | c: foo.ByteString
|
||||
|
|
||||
|
||||
PYI057.pyi:9:4: PYI057 Do not use `collections.abc.ByteString`, which has unclear semantics and is deprecated
|
||||
|
|
||||
8 | a: typing.ByteString
|
||||
9 | b: collections.abc.ByteString
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI057
|
||||
10 | c: foo.ByteString
|
||||
|
|
||||
@@ -1,97 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs
|
||||
---
|
||||
PYI064.py:3:1: PYI064 [*] `Final[Literal[True]]` can be replaced with a bare `Final`
|
||||
|
|
||||
1 | from typing import Final, Literal
|
||||
2 |
|
||||
3 | x: Final[Literal[True]] = True # PYI064
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI064
|
||||
4 | y: Final[Literal[None]] = None # PYI064
|
||||
5 | z: Final[Literal[ # PYI064
|
||||
|
|
||||
= help: Replace with `Final`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | from typing import Final, Literal
|
||||
2 2 |
|
||||
3 |-x: Final[Literal[True]] = True # PYI064
|
||||
3 |+x: Final = True # PYI064
|
||||
4 4 | y: Final[Literal[None]] = None # PYI064
|
||||
5 5 | z: Final[Literal[ # PYI064
|
||||
6 6 | "this is a really long literal, that won't be rendered in the issue text"
|
||||
|
||||
PYI064.py:4:1: PYI064 [*] `Final[Literal[None]]` can be replaced with a bare `Final`
|
||||
|
|
||||
3 | x: Final[Literal[True]] = True # PYI064
|
||||
4 | y: Final[Literal[None]] = None # PYI064
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI064
|
||||
5 | z: Final[Literal[ # PYI064
|
||||
6 | "this is a really long literal, that won't be rendered in the issue text"
|
||||
|
|
||||
= help: Replace with `Final`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | from typing import Final, Literal
|
||||
2 2 |
|
||||
3 3 | x: Final[Literal[True]] = True # PYI064
|
||||
4 |-y: Final[Literal[None]] = None # PYI064
|
||||
4 |+y: Final = None # PYI064
|
||||
5 5 | z: Final[Literal[ # PYI064
|
||||
6 6 | "this is a really long literal, that won't be rendered in the issue text"
|
||||
7 7 | ]] = "this is a really long literal, that won't be rendered in the issue text"
|
||||
|
||||
PYI064.py:5:1: PYI064 [*] `Final[Literal[...]]` can be replaced with a bare `Final`
|
||||
|
|
||||
3 | x: Final[Literal[True]] = True # PYI064
|
||||
4 | y: Final[Literal[None]] = None # PYI064
|
||||
5 | / z: Final[Literal[ # PYI064
|
||||
6 | | "this is a really long literal, that won't be rendered in the issue text"
|
||||
7 | | ]] = "this is a really long literal, that won't be rendered in the issue text"
|
||||
| |______________________________________________________________________________^ PYI064
|
||||
8 |
|
||||
9 | # This should be fixable, and marked as safe
|
||||
|
|
||||
= help: Replace with `Final`
|
||||
|
||||
ℹ Safe fix
|
||||
2 2 |
|
||||
3 3 | x: Final[Literal[True]] = True # PYI064
|
||||
4 4 | y: Final[Literal[None]] = None # PYI064
|
||||
5 |-z: Final[Literal[ # PYI064
|
||||
6 |- "this is a really long literal, that won't be rendered in the issue text"
|
||||
7 |-]] = "this is a really long literal, that won't be rendered in the issue text"
|
||||
5 |+z: Final = "this is a really long literal, that won't be rendered in the issue text"
|
||||
8 6 |
|
||||
9 7 | # This should be fixable, and marked as safe
|
||||
10 8 | w1: Final[Literal[123]] # PYI064
|
||||
|
||||
PYI064.py:10:1: PYI064 [*] `Final[Literal[123]]` can be replaced with a bare `Final`
|
||||
|
|
||||
9 | # This should be fixable, and marked as safe
|
||||
10 | w1: Final[Literal[123]] # PYI064
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^ PYI064
|
||||
11 |
|
||||
12 | # This should not be fixable
|
||||
|
|
||||
= help: Replace with `Final`
|
||||
|
||||
ℹ Safe fix
|
||||
7 7 | ]] = "this is a really long literal, that won't be rendered in the issue text"
|
||||
8 8 |
|
||||
9 9 | # This should be fixable, and marked as safe
|
||||
10 |-w1: Final[Literal[123]] # PYI064
|
||||
10 |+w1: Final = 123 # PYI064
|
||||
11 11 |
|
||||
12 12 | # This should not be fixable
|
||||
13 13 | w2: Final[Literal[123]] = "random value" # PYI064
|
||||
|
||||
PYI064.py:13:1: PYI064 `Final[Literal[123]]` can be replaced with a bare `Final`
|
||||
|
|
||||
12 | # This should not be fixable
|
||||
13 | w2: Final[Literal[123]] = "random value" # PYI064
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI064
|
||||
14 |
|
||||
15 | n1: Final[Literal[True, False]] = True # No issue here
|
||||
|
|
||||
= help: Replace with `Final`
|
||||
@@ -1,92 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs
|
||||
---
|
||||
PYI064.pyi:3:1: PYI064 [*] `Final[Literal[True]]` can be replaced with a bare `Final`
|
||||
|
|
||||
1 | from typing import Final, Literal
|
||||
2 |
|
||||
3 | x: Final[Literal[True]] # PYI064
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^ PYI064
|
||||
4 | y: Final[Literal[None]] = None # PYI064
|
||||
5 | z: Final[Literal["this is a really long literal, that won't be rendered in the issue text"]] # PYI064
|
||||
|
|
||||
= help: Replace with `Final`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | from typing import Final, Literal
|
||||
2 2 |
|
||||
3 |-x: Final[Literal[True]] # PYI064
|
||||
3 |+x: Final = True # PYI064
|
||||
4 4 | y: Final[Literal[None]] = None # PYI064
|
||||
5 5 | z: Final[Literal["this is a really long literal, that won't be rendered in the issue text"]] # PYI064
|
||||
6 6 |
|
||||
|
||||
PYI064.pyi:4:1: PYI064 [*] `Final[Literal[None]]` can be replaced with a bare `Final`
|
||||
|
|
||||
3 | x: Final[Literal[True]] # PYI064
|
||||
4 | y: Final[Literal[None]] = None # PYI064
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI064
|
||||
5 | z: Final[Literal["this is a really long literal, that won't be rendered in the issue text"]] # PYI064
|
||||
|
|
||||
= help: Replace with `Final`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | from typing import Final, Literal
|
||||
2 2 |
|
||||
3 3 | x: Final[Literal[True]] # PYI064
|
||||
4 |-y: Final[Literal[None]] = None # PYI064
|
||||
4 |+y: Final = None # PYI064
|
||||
5 5 | z: Final[Literal["this is a really long literal, that won't be rendered in the issue text"]] # PYI064
|
||||
6 6 |
|
||||
7 7 | # This should be fixable, and marked as safe
|
||||
|
||||
PYI064.pyi:5:1: PYI064 [*] `Final[Literal[...]]` can be replaced with a bare `Final`
|
||||
|
|
||||
3 | x: Final[Literal[True]] # PYI064
|
||||
4 | y: Final[Literal[None]] = None # PYI064
|
||||
5 | z: Final[Literal["this is a really long literal, that won't be rendered in the issue text"]] # PYI064
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI064
|
||||
6 |
|
||||
7 | # This should be fixable, and marked as safe
|
||||
|
|
||||
= help: Replace with `Final`
|
||||
|
||||
ℹ Safe fix
|
||||
2 2 |
|
||||
3 3 | x: Final[Literal[True]] # PYI064
|
||||
4 4 | y: Final[Literal[None]] = None # PYI064
|
||||
5 |-z: Final[Literal["this is a really long literal, that won't be rendered in the issue text"]] # PYI064
|
||||
5 |+z: Final = "this is a really long literal, that won't be rendered in the issue text" # PYI064
|
||||
6 6 |
|
||||
7 7 | # This should be fixable, and marked as safe
|
||||
8 8 | w1: Final[Literal[123]] # PYI064
|
||||
|
||||
PYI064.pyi:8:1: PYI064 [*] `Final[Literal[123]]` can be replaced with a bare `Final`
|
||||
|
|
||||
7 | # This should be fixable, and marked as safe
|
||||
8 | w1: Final[Literal[123]] # PYI064
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^ PYI064
|
||||
9 |
|
||||
10 | # This should not be fixable
|
||||
|
|
||||
= help: Replace with `Final`
|
||||
|
||||
ℹ Safe fix
|
||||
5 5 | z: Final[Literal["this is a really long literal, that won't be rendered in the issue text"]] # PYI064
|
||||
6 6 |
|
||||
7 7 | # This should be fixable, and marked as safe
|
||||
8 |-w1: Final[Literal[123]] # PYI064
|
||||
8 |+w1: Final = 123 # PYI064
|
||||
9 9 |
|
||||
10 10 | # This should not be fixable
|
||||
11 11 | w2: Final[Literal[123]] = "random value" # PYI064
|
||||
|
||||
PYI064.pyi:11:1: PYI064 `Final[Literal[123]]` can be replaced with a bare `Final`
|
||||
|
|
||||
10 | # This should not be fixable
|
||||
11 | w2: Final[Literal[123]] = "random value" # PYI064
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI064
|
||||
12 |
|
||||
13 | n1: Final[Literal[True, False]] # No issue here
|
||||
|
|
||||
= help: Replace with `Final`
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs
|
||||
---
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs
|
||||
---
|
||||
PYI066.pyi:3:4: PYI066 Use `>=` when using `if`-`else` with `sys.version_info` comparisons
|
||||
|
|
||||
1 | import sys
|
||||
2 |
|
||||
3 | if sys.version_info < (3, 10): # Y066 When using if/else with sys.version_info, put the code for new Python versions first, e.g. "if sys.version_info >= (3, 10)"
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI066
|
||||
4 | def foo(x): ...
|
||||
5 | else:
|
||||
|
|
||||
|
||||
PYI066.pyi:8:4: PYI066 Use `>=` when using `if`-`else` with `sys.version_info` comparisons
|
||||
|
|
||||
6 | def foo(x, *, bar=True): ...
|
||||
7 |
|
||||
8 | if sys.version_info < (3, 8): # Y066 When using if/else with sys.version_info, put the code for new Python versions first, e.g. "if sys.version_info >= (3, 8)"
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^ PYI066
|
||||
9 | def bar(x): ...
|
||||
10 | elif sys.version_info < (3, 9): # Y066 When using if/else with sys.version_info, put the code for new Python versions first, e.g. "if sys.version_info >= (3, 9)"
|
||||
|
|
||||
|
||||
PYI066.pyi:10:6: PYI066 Use `>=` when using `if`-`else` with `sys.version_info` comparisons
|
||||
|
|
||||
8 | if sys.version_info < (3, 8): # Y066 When using if/else with sys.version_info, put the code for new Python versions first, e.g. "if sys.version_info >= (3, 8)"
|
||||
9 | def bar(x): ...
|
||||
10 | elif sys.version_info < (3, 9): # Y066 When using if/else with sys.version_info, put the code for new Python versions first, e.g. "if sys.version_info >= (3, 9)"
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^ PYI066
|
||||
11 | def bar(x, *, bar=True): ...
|
||||
12 | elif sys.version_info < (3, 11): # Y066 When using if/else with sys.version_info, put the code for new Python versions first, e.g. "if sys.version_info >= (3, 10)"
|
||||
|
|
||||
|
||||
PYI066.pyi:12:6: PYI066 Use `>=` when using `if`-`else` with `sys.version_info` comparisons
|
||||
|
|
||||
10 | elif sys.version_info < (3, 9): # Y066 When using if/else with sys.version_info, put the code for new Python versions first, e.g. "if sys.version_info >= (3, 9)"
|
||||
11 | def bar(x, *, bar=True): ...
|
||||
12 | elif sys.version_info < (3, 11): # Y066 When using if/else with sys.version_info, put the code for new Python versions first, e.g. "if sys.version_info >= (3, 10)"
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI066
|
||||
13 | def bar(x, *, bar=True, baz=False): ...
|
||||
14 | else:
|
||||
|
|
||||
|
||||
PYI066.pyi:20:6: PYI066 Use `>=` when using `if`-`else` with `sys.version_info` comparisons
|
||||
|
|
||||
18 | if sys.version_info >= (3, 5):
|
||||
19 | ...
|
||||
20 | elif sys.version_info < (3, 9): # Y066 When using if/else with sys.version_info, put the code for new Python versions first, e.g. "if sys.version_info >= (3, 10)"
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^ PYI066
|
||||
21 | ...
|
||||
22 | else:
|
||||
|
|
||||
@@ -384,11 +384,7 @@ pub(crate) fn unittest_raises_assertion(
|
||||
},
|
||||
call.func.range(),
|
||||
);
|
||||
if !checker
|
||||
.indexer()
|
||||
.comment_ranges()
|
||||
.has_comments(call, checker.locator())
|
||||
{
|
||||
if !checker.indexer().has_comments(call, checker.locator()) {
|
||||
if let Some(args) = to_pytest_raises_args(checker, attr.as_str(), &call.arguments) {
|
||||
diagnostic.try_set_fix(|| {
|
||||
let (import_edit, binding) = checker.importer().get_or_import_symbol(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use ruff_python_ast::{AnyStringFlags, StringFlags};
|
||||
use ruff_python_ast::AnyStringFlags;
|
||||
use ruff_text_size::TextLen;
|
||||
|
||||
/// Returns the raw contents of the string given the string's contents and flags.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::visitor::{walk_f_string, Visitor};
|
||||
use ruff_python_ast::{self as ast, AnyStringFlags, StringFlags, StringLike};
|
||||
use ruff_python_ast::{self as ast, AnyStringFlags, StringLike};
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, AnyStringFlags, StringFlags, StringLike};
|
||||
use ruff_python_ast::{self as ast, AnyStringFlags, StringLike};
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
|
||||
@@ -20,9 +20,6 @@ use crate::checkers::ast::Checker;
|
||||
/// versions, that it will have the same type, or that it will have the same
|
||||
/// behavior. Instead, use the class's public interface.
|
||||
///
|
||||
/// This rule ignores accesses on dunder methods (e.g., `__init__`) and sunder
|
||||
/// methods (e.g., `_missing_`).
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// class Class:
|
||||
@@ -73,143 +70,128 @@ pub(crate) fn private_member_access(checker: &mut Checker, expr: &Expr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore non-private accesses.
|
||||
if !attr.starts_with('_') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore dunder accesses.
|
||||
let is_dunder = attr.starts_with("__") && attr.ends_with("__");
|
||||
if is_dunder {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore sunder accesses.
|
||||
let is_sunder = attr.starts_with('_')
|
||||
&& attr.ends_with('_')
|
||||
&& !attr.starts_with("__")
|
||||
&& !attr.ends_with("__");
|
||||
if is_sunder {
|
||||
return;
|
||||
}
|
||||
|
||||
if checker
|
||||
.settings
|
||||
.flake8_self
|
||||
.ignore_names
|
||||
.contains(attr.as_ref())
|
||||
if (attr.starts_with("__") && !attr.ends_with("__"))
|
||||
|| (attr.starts_with('_') && !attr.starts_with("__"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore accesses on instances within special methods (e.g., `__eq__`).
|
||||
if let ScopeKind::Function(ast::StmtFunctionDef { name, .. }) =
|
||||
checker.semantic().current_scope().kind
|
||||
{
|
||||
if matches!(
|
||||
name.as_str(),
|
||||
"__lt__"
|
||||
| "__le__"
|
||||
| "__eq__"
|
||||
| "__ne__"
|
||||
| "__gt__"
|
||||
| "__ge__"
|
||||
| "__add__"
|
||||
| "__sub__"
|
||||
| "__mul__"
|
||||
| "__matmul__"
|
||||
| "__truediv__"
|
||||
| "__floordiv__"
|
||||
| "__mod__"
|
||||
| "__divmod__"
|
||||
| "__pow__"
|
||||
| "__lshift__"
|
||||
| "__rshift__"
|
||||
| "__and__"
|
||||
| "__xor__"
|
||||
| "__or__"
|
||||
| "__radd__"
|
||||
| "__rsub__"
|
||||
| "__rmul__"
|
||||
| "__rmatmul__"
|
||||
| "__rtruediv__"
|
||||
| "__rfloordiv__"
|
||||
| "__rmod__"
|
||||
| "__rdivmod__"
|
||||
| "__rpow__"
|
||||
| "__rlshift__"
|
||||
| "__rrshift__"
|
||||
| "__rand__"
|
||||
| "__rxor__"
|
||||
| "__ror__"
|
||||
| "__iadd__"
|
||||
| "__isub__"
|
||||
| "__imul__"
|
||||
| "__imatmul__"
|
||||
| "__itruediv__"
|
||||
| "__ifloordiv__"
|
||||
| "__imod__"
|
||||
| "__ipow__"
|
||||
| "__ilshift__"
|
||||
| "__irshift__"
|
||||
| "__iand__"
|
||||
| "__ixor__"
|
||||
| "__ior__"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Allow some documented private methods, like `os._exit()`.
|
||||
if let Some(qualified_name) = checker.semantic().resolve_qualified_name(expr) {
|
||||
if matches!(qualified_name.segments(), ["os", "_exit"]) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() {
|
||||
// Ignore `super()` calls.
|
||||
if let Some(name) = UnqualifiedName::from_expr(func) {
|
||||
if matches!(name.segments(), ["super"]) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(name) = UnqualifiedName::from_expr(value) {
|
||||
// Ignore `self` and `cls` accesses.
|
||||
if matches!(name.segments(), ["self" | "cls" | "mcs"]) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if let Expr::Name(name) = value.as_ref() {
|
||||
// Ignore accesses on class members from _within_ the class.
|
||||
if checker
|
||||
.semantic()
|
||||
.resolve_name(name)
|
||||
.and_then(|id| {
|
||||
if let BindingKind::ClassDefinition(scope) = checker.semantic().binding(id).kind {
|
||||
Some(scope)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.is_some_and(|scope| {
|
||||
checker
|
||||
.semantic()
|
||||
.current_scope_ids()
|
||||
.any(|parent| scope == parent)
|
||||
})
|
||||
.settings
|
||||
.flake8_self
|
||||
.ignore_names
|
||||
.contains(attr.as_ref())
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
PrivateMemberAccess {
|
||||
access: attr.to_string(),
|
||||
},
|
||||
expr.range(),
|
||||
));
|
||||
// Ignore accesses on instances within special methods (e.g., `__eq__`).
|
||||
if let ScopeKind::Function(ast::StmtFunctionDef { name, .. }) =
|
||||
checker.semantic().current_scope().kind
|
||||
{
|
||||
if matches!(
|
||||
name.as_str(),
|
||||
"__lt__"
|
||||
| "__le__"
|
||||
| "__eq__"
|
||||
| "__ne__"
|
||||
| "__gt__"
|
||||
| "__ge__"
|
||||
| "__add__"
|
||||
| "__sub__"
|
||||
| "__mul__"
|
||||
| "__matmul__"
|
||||
| "__truediv__"
|
||||
| "__floordiv__"
|
||||
| "__mod__"
|
||||
| "__divmod__"
|
||||
| "__pow__"
|
||||
| "__lshift__"
|
||||
| "__rshift__"
|
||||
| "__and__"
|
||||
| "__xor__"
|
||||
| "__or__"
|
||||
| "__radd__"
|
||||
| "__rsub__"
|
||||
| "__rmul__"
|
||||
| "__rmatmul__"
|
||||
| "__rtruediv__"
|
||||
| "__rfloordiv__"
|
||||
| "__rmod__"
|
||||
| "__rdivmod__"
|
||||
| "__rpow__"
|
||||
| "__rlshift__"
|
||||
| "__rrshift__"
|
||||
| "__rand__"
|
||||
| "__rxor__"
|
||||
| "__ror__"
|
||||
| "__iadd__"
|
||||
| "__isub__"
|
||||
| "__imul__"
|
||||
| "__imatmul__"
|
||||
| "__itruediv__"
|
||||
| "__ifloordiv__"
|
||||
| "__imod__"
|
||||
| "__ipow__"
|
||||
| "__ilshift__"
|
||||
| "__irshift__"
|
||||
| "__iand__"
|
||||
| "__ixor__"
|
||||
| "__ior__"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Allow some documented private methods, like `os._exit()`.
|
||||
if let Some(qualified_name) = checker.semantic().resolve_qualified_name(expr) {
|
||||
if matches!(qualified_name.segments(), ["os", "_exit"]) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() {
|
||||
// Ignore `super()` calls.
|
||||
if let Some(name) = UnqualifiedName::from_expr(func) {
|
||||
if matches!(name.segments(), ["super"]) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(name) = UnqualifiedName::from_expr(value) {
|
||||
// Ignore `self` and `cls` accesses.
|
||||
if matches!(name.segments(), ["self" | "cls" | "mcs"]) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if let Expr::Name(name) = value.as_ref() {
|
||||
// Ignore accesses on class members from _within_ the class.
|
||||
if checker
|
||||
.semantic()
|
||||
.resolve_name(name)
|
||||
.and_then(|id| {
|
||||
if let BindingKind::ClassDefinition(scope) = checker.semantic().binding(id).kind
|
||||
{
|
||||
Some(scope)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.is_some_and(|scope| {
|
||||
checker
|
||||
.semantic()
|
||||
.current_scope_ids()
|
||||
.any(|parent| scope == parent)
|
||||
})
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
PrivateMemberAccess {
|
||||
access: attr.to_string(),
|
||||
},
|
||||
expr.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -526,11 +526,7 @@ pub(crate) fn compare_with_tuple(checker: &mut Checker, expr: &Expr) {
|
||||
}
|
||||
|
||||
// Avoid removing comments.
|
||||
if checker
|
||||
.indexer()
|
||||
.comment_ranges()
|
||||
.has_comments(expr, checker.locator())
|
||||
{
|
||||
if checker.indexer().has_comments(expr, checker.locator()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -209,11 +209,7 @@ pub(crate) fn if_else_block_instead_of_dict_get(checker: &mut Checker, stmt_if:
|
||||
},
|
||||
stmt_if.range(),
|
||||
);
|
||||
if !checker
|
||||
.indexer()
|
||||
.comment_ranges()
|
||||
.has_comments(stmt_if, checker.locator())
|
||||
{
|
||||
if !checker.indexer().has_comments(stmt_if, checker.locator()) {
|
||||
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
|
||||
contents,
|
||||
stmt_if.range(),
|
||||
@@ -299,11 +295,7 @@ pub(crate) fn if_exp_instead_of_dict_get(
|
||||
},
|
||||
expr.range(),
|
||||
);
|
||||
if !checker
|
||||
.indexer()
|
||||
.comment_ranges()
|
||||
.has_comments(expr, checker.locator())
|
||||
{
|
||||
if !checker.indexer().has_comments(expr, checker.locator()) {
|
||||
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
|
||||
contents,
|
||||
expr.range(),
|
||||
|
||||
@@ -142,11 +142,7 @@ pub(crate) fn if_else_block_instead_of_if_exp(checker: &mut Checker, stmt_if: &a
|
||||
},
|
||||
stmt_if.range(),
|
||||
);
|
||||
if !checker
|
||||
.indexer()
|
||||
.comment_ranges()
|
||||
.has_comments(stmt_if, checker.locator())
|
||||
{
|
||||
if !checker.indexer().has_comments(stmt_if, checker.locator()) {
|
||||
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
|
||||
contents,
|
||||
stmt_if.range(),
|
||||
|
||||
@@ -193,11 +193,7 @@ pub(crate) fn needless_bool(checker: &mut Checker, stmt: &Stmt) {
|
||||
}
|
||||
|
||||
// Generate the replacement condition.
|
||||
let condition = if checker
|
||||
.indexer()
|
||||
.comment_ranges()
|
||||
.has_comments(&range, checker.locator())
|
||||
{
|
||||
let condition = if checker.indexer().has_comments(&range, checker.locator()) {
|
||||
None
|
||||
} else {
|
||||
// If the return values are inverted, wrap the condition in a `not`.
|
||||
|
||||
@@ -2,7 +2,7 @@ use ruff_python_ast::{self as ast, Expr, Stmt};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_semantic::{ScopeKind, SemanticModel};
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -114,27 +114,24 @@ fn match_exit_stack(semantic: &SemanticModel) -> bool {
|
||||
|
||||
/// Return `true` if `func` is the builtin `open` or `pathlib.Path(...).open`.
|
||||
fn is_open(semantic: &SemanticModel, func: &Expr) -> bool {
|
||||
// Ex) `open(...)`
|
||||
// open(...)
|
||||
if semantic.match_builtin_expr(func, "open") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Ex) `pathlib.Path(...).open()`
|
||||
// pathlib.Path(...).open()
|
||||
let Expr::Attribute(ast::ExprAttribute { attr, value, .. }) = func else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if attr != "open" {
|
||||
return false;
|
||||
}
|
||||
|
||||
let Expr::Call(ast::ExprCall {
|
||||
func: value_func, ..
|
||||
}) = &**value
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
|
||||
semantic
|
||||
.resolve_qualified_name(value_func)
|
||||
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["pathlib", "Path"]))
|
||||
@@ -192,15 +189,6 @@ pub(crate) fn open_file_with_context_handler(checker: &mut Checker, func: &Expr)
|
||||
return;
|
||||
}
|
||||
|
||||
// Ex) `def __enter__(self): ...`
|
||||
if let ScopeKind::Function(ast::StmtFunctionDef { name, .. }) =
|
||||
&checker.semantic().current_scope().kind
|
||||
{
|
||||
if name == "__enter__" {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(OpenFileWithContextHandler, func.range()));
|
||||
|
||||
@@ -125,11 +125,7 @@ pub(crate) fn suppressible_exception(
|
||||
},
|
||||
stmt.range(),
|
||||
);
|
||||
if !checker
|
||||
.indexer()
|
||||
.comment_ranges()
|
||||
.has_comments(stmt, checker.locator())
|
||||
{
|
||||
if !checker.indexer().has_comments(stmt, checker.locator()) {
|
||||
diagnostic.try_set_fix(|| {
|
||||
// let range = statement_range(stmt, checker.locator(), checker.indexer());
|
||||
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
|
||||
---
|
||||
singledispatch.py:11:20: TCH002 [*] Move third-party import `pandas.DataFrame` into a type-checking block
|
||||
singledispatch.py:10:20: TCH002 [*] Move third-party import `pandas.DataFrame` into a type-checking block
|
||||
|
|
||||
9 | from numpy.typing import ArrayLike
|
||||
10 | from scipy.sparse import spmatrix
|
||||
11 | from pandas import DataFrame
|
||||
8 | from numpy.typing import ArrayLike
|
||||
9 | from scipy.sparse import spmatrix
|
||||
10 | from pandas import DataFrame
|
||||
| ^^^^^^^^^ TCH002
|
||||
12 |
|
||||
13 | if TYPE_CHECKING:
|
||||
11 |
|
||||
12 | if TYPE_CHECKING:
|
||||
|
|
||||
= help: Move into type-checking block
|
||||
|
||||
ℹ Unsafe fix
|
||||
8 8 | from numpy import asarray
|
||||
9 9 | from numpy.typing import ArrayLike
|
||||
10 10 | from scipy.sparse import spmatrix
|
||||
11 |-from pandas import DataFrame
|
||||
12 11 |
|
||||
13 12 | if TYPE_CHECKING:
|
||||
13 |+ from pandas import DataFrame
|
||||
14 14 | from numpy import ndarray
|
||||
7 7 | from numpy import asarray
|
||||
8 8 | from numpy.typing import ArrayLike
|
||||
9 9 | from scipy.sparse import spmatrix
|
||||
10 |-from pandas import DataFrame
|
||||
11 10 |
|
||||
12 11 | if TYPE_CHECKING:
|
||||
12 |+ from pandas import DataFrame
|
||||
13 13 | from numpy import ndarray
|
||||
14 14 |
|
||||
15 15 |
|
||||
16 16 |
|
||||
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user