Compare commits

..

1 Commits

Author SHA1 Message Date
Dhruv Manilawala
909798ffa2 Offset syntax error for each cell 2024-05-17 13:36:21 +05:30
254 changed files with 2900 additions and 7585 deletions

View File

@@ -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
View File

@@ -15,6 +15,3 @@
# Script for fuzzing the parser
/scripts/fuzz-parser/ @AlexWaygood
# red-knot
/crates/red_knot/ @carljm @MichaReiser

View File

@@ -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

View File

@@ -1,5 +0,0 @@
{
"recommendations": [
"rust-lang.rust-analyzer"
]
}

View File

@@ -1,6 +0,0 @@
{
"rust-analyzer.check.extraArgs": [
"--all-features"
],
"rust-analyzer.check.command": "clippy",
}

View File

@@ -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

View File

@@ -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
View File

@@ -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",

View File

@@ -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" }

View File

@@ -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/)

View File

@@ -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 {

View File

@@ -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`.

View File

@@ -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);

View File

@@ -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]

View File

@@ -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}]"),
}
}
}

View File

@@ -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(())
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.4.7"
version = "0.4.4"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -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\"`.`");
}

View File

@@ -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))
}

View 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()?;

View File

@@ -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,

View File

@@ -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

View File

@@ -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);
});
},
);
}

View File

@@ -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>

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_linter"
version = "0.4.7"
version = "0.4.4"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -77,8 +77,3 @@ print(foo._asdict())
import os
os._exit()
from enum import Enum
Enum._missing_(1) # OK

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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])

View File

@@ -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

View File

@@ -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 \."

View File

@@ -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
"""

View File

@@ -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

View File

@@ -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:

View File

@@ -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 "

View File

@@ -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)

View File

@@ -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,)
)

View File

@@ -38,12 +38,3 @@ z = (
else
y
)
# FURB110
z = (
x
if x
else y
if y > 0
else None
)

View File

@@ -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):

View File

@@ -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,

View File

@@ -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, .. }) => {

View File

@@ -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 {

View File

@@ -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),

View File

@@ -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,

View File

@@ -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,
}
}
}

View File

@@ -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,

View File

@@ -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()
}

View File

@@ -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

View File

@@ -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.
///

View File

@@ -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(

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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"

View File

@@ -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"
]
)
})

View File

@@ -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);

View File

@@ -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):

View File

@@ -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(

View File

@@ -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);
}

View File

@@ -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")

View File

@@ -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")

View File

@@ -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")

View File

@@ -1,4 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs
---

View File

@@ -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!")

View File

@@ -383,7 +383,7 @@ impl AlwaysFixableViolation for LoggingWarn {
}
fn fix_title(&self) -> String {
"Convert to `warning`".to_string()
"Convert to `warn`".to_string()
}
}

View File

@@ -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"))]

View File

@@ -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()));
}

View File

@@ -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()));
}
}
}

View File

@@ -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;

View File

@@ -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)
}
}

View File

@@ -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
|

View File

@@ -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
|

View File

@@ -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
|

View File

@@ -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`

View File

@@ -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`

View File

@@ -1,4 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs
---

View File

@@ -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:
|

View File

@@ -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(

View File

@@ -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.

View File

@@ -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};

View File

@@ -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};

View File

@@ -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(),
));
}
}

View File

@@ -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;
}

View File

@@ -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(),

View File

@@ -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(),

View File

@@ -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`.

View File

@@ -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()));

View File

@@ -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());

View File

@@ -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