Compare commits

..

1 Commits

Author SHA1 Message Date
Charlie Marsh
4b3488b5b2 Use SmallVec for binding references 2023-05-24 10:33:51 -04:00
954 changed files with 22683 additions and 39294 deletions

View File

@@ -1,6 +1,6 @@
[alias]
dev = "run --package ruff_dev --bin ruff_dev"
benchmark = "bench -p ruff_benchmark --bench linter --bench formatter --"
benchmark = "bench -p ruff_benchmark --"
[target.'cfg(all())']
rustflags = [

View File

@@ -1,46 +0,0 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/rust
{
"name": "Ruff",
"image": "mcr.microsoft.com/devcontainers/rust:0-1-bullseye",
"mounts": [
{
"source": "devcontainer-cargo-cache-${devcontainerId}",
"target": "/usr/local/cargo",
"type": "volume"
}
],
"customizations": {
"codespaces": {
"openFiles": [
"CONTRIBUTING.md"
]
},
"vscode": {
"extensions": [
"ms-python.python",
"rust-lang.rust-analyzer",
"serayuzgur.crates",
"tamasfe.even-better-toml",
"Swellaby.vscode-rust-test-adapter",
"charliermarsh.ruff"
],
"settings": {
"rust-analyzer.updates.askBeforeDownload": false
}
}
},
// Features to add to the dev container. More info: https://containers.dev/features.
"features": {
"ghcr.io/devcontainers/features/python": {
"installTools": false
}
},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
"postCreateCommand": ".devcontainer/post-create.sh"
// Configure tool-specific properties.
// "customizations": {},
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}

View File

@@ -1,8 +0,0 @@
#!/usr/bin/env bash
rustup default < rust-toolchain
rustup component add clippy rustfmt
cargo install cargo-insta
cargo fetch
pip install maturin pre-commit

View File

@@ -14,7 +14,4 @@ indent_size = 2
indent_size = 4
[*.snap]
trim_trailing_whitespace = false
[*.md]
max_line_length = 100
trim_trailing_whitespace = false

5
.github/release.yml vendored
View File

@@ -1,9 +1,5 @@
# https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes#configuring-automatically-generated-release-notes
changelog:
exclude:
labels:
- internal
- documentation
categories:
- title: Breaking Changes
labels:
@@ -15,7 +11,6 @@ changelog:
- title: Settings
labels:
- configuration
- cli
- title: Bug Fixes
labels:
- bug

View File

@@ -1,9 +1,9 @@
name: mkdocs
on:
workflow_dispatch:
release:
types: [ published ]
types: [published]
workflow_dispatch:
jobs:
mkdocs:

View File

@@ -52,7 +52,7 @@ jobs:
- name: "Build wheels - universal2"
uses: PyO3/maturin-action@v1
with:
args: --release --target universal2-apple-darwin --out dist -m ./${{ env.CRATE_NAME }}/Cargo.toml
args: --release --universal2 --out dist -m ./${{ env.CRATE_NAME }}/Cargo.toml
- name: "Install built wheel - universal2"
run: |
pip install dist/${{ env.CRATE_NAME }}-*universal2.whl --force-reinstall

View File

@@ -2,8 +2,8 @@ name: "[Playground] Release"
on:
workflow_dispatch:
release:
types: [ published ]
push:
branches: [main]
env:
CARGO_INCREMENTAL: 0

View File

@@ -94,7 +94,7 @@ jobs:
- name: "Build wheels - universal2"
uses: PyO3/maturin-action@v1
with:
args: --release --target universal2-apple-darwin --out dist
args: --release --universal2 --out dist
- name: "Test wheel - universal2"
run: |
pip install dist/${{ env.PACKAGE_NAME }}-*universal2.whl --force-reinstall
@@ -394,22 +394,18 @@ jobs:
- musllinux
- musllinux-cross
if: "startsWith(github.ref, 'refs/tags/')"
environment:
name: release
permissions:
# For pypi trusted publishing
id-token: write
steps:
- uses: actions/download-artifact@v3
with:
name: wheels
path: wheels
- uses: actions/setup-python@v4
- name: "Publish to PyPi"
uses: pypa/gh-action-pypi-publish@release/v1
with:
skip-existing: true
packages-dir: wheels
verbose: true
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.RUFF_TOKEN }}
run: |
pip install --upgrade twine
twine upload --skip-existing *
- uses: actions/download-artifact@v3
with:
name: binaries

1
.gitignore vendored
View File

@@ -5,7 +5,6 @@ ruff-old
github_search*.jsonl
schemastore
.venv*
scratch.py
###
# Rust.gitignore

View File

@@ -1,14 +0,0 @@
# default to true for all rules
default: true
# MD033/no-inline-html
MD033: false
# MD041/first-line-h1
MD041: false
# MD013/line-length
MD013:
line_length: 100
code_blocks: false
ignore_code_blocks: true

View File

@@ -1,12 +1,4 @@
fail_fast: true
exclude: |
(?x)^(
crates/ruff/resources/.*|
crates/ruff_python_formatter/resources/.*|
crates/ruff_python_formatter/src/snapshots/.*
)$
repos:
- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.12.1
@@ -25,9 +17,15 @@ repos:
rev: v0.33.0
hooks:
- id: markdownlint-fix
args:
- --disable
- MD013 # line-length
- MD033 # no-inline-html
- MD041 # first-line-h1
- --
- repo: https://github.com/crate-ci/typos
rev: v1.14.12
rev: v1.14.8
hooks:
- id: typos

View File

@@ -86,8 +86,7 @@ the intention of adding a stable public API in the future.
### `select`, `extend-select`, `ignore`, and `extend-ignore` have new semantics ([#2312](https://github.com/charliermarsh/ruff/pull/2312))
Previously, the interplay between `select` and its related options could lead to unexpected
behavior. For example, `ruff --select E501 --ignore ALL` and `ruff --select E501 --extend-ignore ALL`
behaved differently. (See [#2312](https://github.com/charliermarsh/ruff/pull/2312) for more
behavior. For example, `ruff --select E501 --ignore ALL` and `ruff --select E501 --extend-ignore ALL` behaved differently. (See [#2312](https://github.com/charliermarsh/ruff/pull/2312) for more
examples.)
When Ruff determines the enabled rule set, it has to reconcile `select` and `ignore` from a variety

View File

@@ -8,7 +8,6 @@ Welcome! We're happy to have you here. Thank you in advance for your contributio
- [Project Structure](#project-structure)
- [Example: Adding a new lint rule](#example-adding-a-new-lint-rule)
- [Rule naming convention](#rule-naming-convention)
- [Rule testing: fixtures and snapshots](#rule-testing-fixtures-and-snapshots)
- [Example: Adding a new configuration option](#example-adding-a-new-configuration-option)
- [MkDocs](#mkdocs)
- [Release Process](#release-process)
@@ -94,11 +93,9 @@ At time of writing, the repository includes the following crates:
- `crates/ruff`: library crate containing all lint rules and the core logic for running them.
- `crates/ruff_cli`: binary crate containing Ruff's command-line interface.
- `crates/ruff_dev`: binary crate containing utilities used in the development of Ruff itself (e.g.,
`cargo dev generate-all`).
- `crates/ruff_dev`: binary crate containing utilities used in the development of Ruff itself (e.g., `cargo dev generate-all`).
- `crates/ruff_macros`: library crate containing macros used by Ruff.
- `crates/ruff_python`: library crate implementing Python-specific functionality (e.g., lists of
standard library modules by version).
- `crates/ruff_python`: library crate implementing Python-specific functionality (e.g., lists of standard library modules by versionb).
- `crates/flake8_to_ruff`: binary crate for generating Ruff configuration from Flake8 configuration.
### Example: Adding a new lint rule
@@ -106,20 +103,14 @@ At time of writing, the repository includes the following crates:
At a high level, the steps involved in adding a new lint rule are as follows:
1. Determine a name for the new rule as per our [rule naming convention](#rule-naming-convention).
1. Create a file for your rule (e.g., `crates/ruff/src/rules/flake8_bugbear/rules/abstract_base_class.rs`).
1. In that file, define a violation struct. You can grep for `#[violation]` to see examples.
1. Map the violation struct to a rule code in `crates/ruff/src/codes.rs` (e.g., `E402`).
1. Define the logic for triggering the violation in `crates/ruff/src/checkers/ast/mod.rs` (for
AST-based checks), `crates/ruff/src/checkers/tokens.rs` (for token-based checks),
`crates/ruff/src/checkers/lines.rs` (for text-based checks), or
`crates/ruff/src/checkers/filesystem.rs` (for filesystem-based checks).
1. Add proper [testing](#rule-testing-fixtures-and-snapshots) for your rule.
1. Map the violation struct to a rule code in `crates/ruff/src/registry.rs` (e.g., `E402`).
1. Define the logic for triggering the violation in `crates/ruff/src/checkers/ast/mod.rs` (for AST-based
checks), `crates/ruff/src/checkers/tokens.rs` (for token-based checks), `crates/ruff/src/checkers/lines.rs`
(for text-based checks), or `crates/ruff/src/checkers/filesystem.rs` (for filesystem-based
checks).
1. Add a test fixture.
1. Update the generated files (documentation and generated code).
To define the violation, start by creating a dedicated file for your rule under the appropriate
@@ -134,8 +125,18 @@ collecting diagnostics as it goes.
If you need to inspect the AST, you can run `cargo dev print-ast` with a Python file. Grep
for the `Check::new` invocations to understand how other, similar rules are implemented.
Once you're satisfied with your code, add tests for your rule. See [rule testing](#rule-testing-fixtures-and-snapshots)
for more details.
To add a test fixture, create a file under `crates/ruff/resources/test/fixtures/[linter]`, named to match
the code you defined earlier (e.g., `crates/ruff/resources/test/fixtures/pycodestyle/E402.py`). This file should
contain a variety of violations and non-violations designed to evaluate and demonstrate the behavior
of your lint rule.
Run `cargo dev generate-all` to generate the code for your new fixture. Then run Ruff
locally with (e.g.) `cargo run -p ruff_cli -- check crates/ruff/resources/test/fixtures/pycodestyle/E402.py --no-cache --select E402`.
Once you're satisfied with the output, codify the behavior as a snapshot test by adding a new
`test_case` macro in the relevant `crates/ruff/src/rules/[linter]/mod.rs` file. Then, run `cargo test`.
Your test will fail, but you'll be prompted to follow-up with `cargo insta review`. Accept the
generated snapshot, then commit the snapshot file alongside the rest of your changes.
Finally, regenerate the documentation and generated code with `cargo dev generate-all`.
@@ -153,38 +154,6 @@ This implies that rule names:
When re-implementing rules from other linters, this convention is given more importance than
preserving the original rule name.
#### Rule testing: fixtures and snapshots
To test rules, Ruff uses snapshots of Ruff's output for a given file (fixture). Generally, there
will be one file per rule (e.g., `E402.py`), and each file will contain all necessary examples of
both violations and non-violations. `cargo insta review` will generate a snapshot file containing
Ruff's output for each fixture, which you can then commit alongside your changes.
Once you've completed the code for the rule itself, you can define tests with the following steps:
1. Add a Python file to `crates/ruff/resources/test/fixtures/[linter]` that contains the code you
want to test. The file name should match the rule name (e.g., `E402.py`), and it should include
examples of both violations and non-violations.
1. Run Ruff locally against your file and verify the output is as expected. Once you're satisfied
with the output (you see the violations you expect, and no others), proceed to the next step.
For example, if you're adding a new rule named `E402`, you would run:
```shell
cargo run -p ruff_cli -- check crates/ruff/resources/test/fixtures/pycodestyle/E402.py --no-cache
```
1. Add the test to the relevant `crates/ruff/src/rules/[linter]/mod.rs` file. If you're contributing
a rule to a pre-existing set, you should be able to find a similar example to pattern-match
against. If you're adding a new linter, you'll need to create a new `mod.rs` file (see,
e.g., `crates/ruff/src/rules/flake8_bugbear/mod.rs`)
1. Run `cargo test`. Your test will fail, but you'll be prompted to follow-up
with `cargo insta review`. Run `cargo insta review`, review and accept the generated snapshot,
then commit the snapshot file alongside the rest of your changes.
1. Run `cargo test` again to ensure that your test passes.
### Example: Adding a new configuration option
Ruff's user-facing settings live in a few different places.
@@ -215,8 +184,6 @@ Finally, regenerate the documentation and generated code with `cargo dev generat
To preview any changes to the documentation locally:
1. Install the [Rust toolchain](https://www.rust-lang.org/tools/install).
1. Install MkDocs and Material for MkDocs with:
```shell

386
Cargo.lock generated
View File

@@ -14,6 +14,17 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "ahash"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
dependencies = [
"getrandom",
"once_cell",
"version_check",
]
[[package]]
name = "aho-corasick"
version = "0.7.20"
@@ -32,12 +43,6 @@ dependencies = [
"memchr",
]
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
@@ -194,9 +199,9 @@ checksum = "6776fc96284a0bb647b615056fc496d1fe1644a7ab01829818a6d91cae888b84"
[[package]]
name = "bstr"
version = "1.5.0"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a246e68bb43f6cd9db24bea052a53e40405417c5fb372e3d1a8a7f770a564ef5"
checksum = "c3d4260bcc2e8fc9df1eac4919a720effeb63a3f0952f5bf4944adfa18897f09"
dependencies = [
"memchr",
"once_cell",
@@ -206,9 +211,9 @@ dependencies = [
[[package]]
name = "bumpalo"
version = "3.13.0"
version = "3.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1"
checksum = "3c6ed94e98ecff0c12dd1b04c15ec0d7d9458ca8fe806cea6f12954efe74c63b"
[[package]]
name = "cachedir"
@@ -248,13 +253,13 @@ dependencies = [
[[package]]
name = "chrono"
version = "0.4.26"
version = "0.4.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5"
checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-integer",
"num-traits",
"time",
"wasm-bindgen",
@@ -290,9 +295,21 @@ dependencies = [
[[package]]
name = "clap"
version = "4.3.1"
version = "3.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ed2379f8603fa2b7509891660e802b88c70a79a6427a70abb5968054de2c28"
checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123"
dependencies = [
"bitflags 1.3.2",
"clap_lex 0.2.4",
"indexmap",
"textwrap",
]
[[package]]
name = "clap"
version = "4.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34d21f9bf1b425d2968943631ec91202fe5e837264063503708b83013f8fc938"
dependencies = [
"clap_builder",
"clap_derive",
@@ -301,24 +318,24 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.3.1"
version = "4.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72394f3339a76daf211e57d4bcb374410f3965dcc606dd0e03738c7888766980"
checksum = "914c8c79fb560f238ef6429439a30023c862f7a28e688c58f7203f12b29970bd"
dependencies = [
"anstream",
"anstyle",
"bitflags 1.3.2",
"clap_lex",
"clap_lex 0.4.1",
"strsim",
]
[[package]]
name = "clap_complete"
version = "4.3.1"
version = "4.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f6b5c519bab3ea61843a7923d074b04245624bb84a64a8c150f5deb014e388b"
checksum = "1594fe2312ec4abf402076e407628f5c313e54c32ade058521df4ee34ecac8a8"
dependencies = [
"clap",
"clap 4.2.7",
]
[[package]]
@@ -327,7 +344,7 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "183495371ea78d4c9ff638bfc6497d46fed2396e4f9c50aebc1278a4a9919a3d"
dependencies = [
"clap",
"clap 4.2.7",
"clap_complete",
"clap_complete_fig",
"clap_complete_nushell",
@@ -335,41 +352,50 @@ dependencies = [
[[package]]
name = "clap_complete_fig"
version = "4.3.1"
version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99fee1d30a51305a6c2ed3fc5709be3c8af626c9c958e04dd9ae94e27bcbce9f"
checksum = "f3af28956330989baa428ed4d3471b853715d445c62de21b67292e22cf8a41fa"
dependencies = [
"clap",
"clap 4.2.7",
"clap_complete",
]
[[package]]
name = "clap_complete_nushell"
version = "0.1.11"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d02bc8b1a18ee47c4d2eec3fb5ac034dc68ebea6125b1509e9ccdffcddce66e"
checksum = "c7fa41f5e6aa83bd151b70fd0ceaee703d68cd669522795dc812df9edad1252c"
dependencies = [
"clap",
"clap 4.2.7",
"clap_complete",
]
[[package]]
name = "clap_derive"
version = "4.3.1"
version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59e9ef9a08ee1c0e1f2e162121665ac45ac3783b0f897db7244ae75ad9a8f65b"
checksum = "3f9644cd56d6b87dbe899ef8b053e331c0637664e9e21a33dfcdc36093f5c5c4"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.18",
"syn 2.0.15",
]
[[package]]
name = "clap_lex"
version = "0.5.0"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b"
checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
dependencies = [
"os_str_bytes",
]
[[package]]
name = "clap_lex"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a2dd5a6fe8c6e3502f568a6353e5273bbb15193ad9a89e457b9970798efbea1"
[[package]]
name = "clearscreen"
@@ -409,14 +435,14 @@ checksum = "5458d9d1a587efaf5091602c59d299696a3877a439c8f6d461a2d3cce11df87a"
[[package]]
name = "console"
version = "0.15.7"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8"
checksum = "c3d79fbe8970a77e3e34151cc13d3b3e248aa0faaecb9f6091fa07ebefe5ad60"
dependencies = [
"encode_unicode",
"lazy_static",
"libc",
"windows-sys 0.45.0",
"windows-sys 0.42.0",
]
[[package]]
@@ -445,12 +471,6 @@ version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
[[package]]
name = "countme"
version = "3.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636"
[[package]]
name = "crc32fast"
version = "1.3.2"
@@ -462,19 +482,19 @@ dependencies = [
[[package]]
name = "criterion"
version = "0.5.1"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
checksum = "e7c76e09c1aae2bc52b3d2f29e13c6572553b30c4aa1b8a49fd70de6412654cb"
dependencies = [
"anes",
"atty",
"cast",
"ciborium",
"clap",
"clap 3.2.25",
"criterion-plot",
"is-terminal",
"itertools",
"lazy_static",
"num-traits",
"once_cell",
"oorandom",
"plotters",
"rayon",
@@ -691,10 +711,10 @@ dependencies = [
[[package]]
name = "flake8-to-ruff"
version = "0.0.271"
version = "0.0.269"
dependencies = [
"anyhow",
"clap",
"clap 4.2.7",
"colored",
"configparser",
"once_cell",
@@ -749,8 +769,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasm-bindgen",
]
[[package]]
@@ -783,6 +805,9 @@ name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
dependencies = [
"ahash",
]
[[package]]
name = "heck"
@@ -888,7 +913,6 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
"hashbrown",
"serde",
]
[[package]]
@@ -935,9 +959,9 @@ dependencies = [
[[package]]
name = "io-lifetimes"
version = "1.0.11"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220"
dependencies = [
"hermit-abi 0.3.1",
"libc",
@@ -986,9 +1010,9 @@ checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"
[[package]]
name = "js-sys"
version = "0.3.63"
version = "0.3.62"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f37a4a5928311ac501dee68b3c7613a1037d0edb30c8e5427bd832d55d1b790"
checksum = "68c16e1bfd491478ab155fd8b4896b86f9ede344949b641e61501e07c2b8b4d5"
dependencies = [
"wasm-bindgen",
]
@@ -1103,15 +1127,18 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "linux-raw-sys"
version = "0.3.8"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
checksum = "ece97ea872ece730aed82664c424eb4c8291e1ff2480247ccf7409044bc6479f"
[[package]]
name = "log"
version = "0.4.18"
version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de"
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
dependencies = [
"cfg-if",
]
[[package]]
name = "matches"
@@ -1160,14 +1187,14 @@ dependencies = [
[[package]]
name = "mio"
version = "0.8.8"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9"
dependencies = [
"libc",
"log",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.48.0",
"windows-sys 0.45.0",
]
[[package]]
@@ -1212,9 +1239,9 @@ dependencies = [
[[package]]
name = "notify"
version = "5.2.0"
version = "5.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "729f63e1ca555a43fe3efa4f3efdf4801c479da85b432242a7b726f353c88486"
checksum = "58ea850aa68a06e48fdb069c0ec44d0d64c8dbffa49bf3b6f7f0a901fdea1ba9"
dependencies = [
"bitflags 1.3.2",
"crossbeam-channel",
@@ -1225,7 +1252,7 @@ dependencies = [
"libc",
"mio",
"walkdir",
"windows-sys 0.45.0",
"windows-sys 0.42.0",
]
[[package]]
@@ -1270,9 +1297,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.17.2"
version = "1.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9670a07f94779e00908f3e686eab508878ebb390ba6e604d3a284c00e8d0487b"
checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
[[package]]
name = "oorandom"
@@ -1374,22 +1401,6 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "pep508_rs"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "969679a29dfdc8278a449f75b3dd45edf57e649bd59f7502429c2840751c46d8"
dependencies = [
"once_cell",
"pep440_rs",
"regex",
"serde",
"thiserror",
"tracing",
"unicode-width",
"url",
]
[[package]]
name = "percent-encoding"
version = "2.2.0"
@@ -1545,26 +1556,13 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.59"
version = "1.0.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b"
checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435"
dependencies = [
"unicode-ident",
]
[[package]]
name = "pyproject-toml"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f04dbbb336bd88583943c7cd973a32fed323578243a7569f40cb0c7da673321b"
dependencies = [
"indexmap",
"pep440_rs",
"pep508_rs",
"serde",
"toml",
]
[[package]]
name = "quick-junit"
version = "0.3.2"
@@ -1590,9 +1588,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.28"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488"
checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500"
dependencies = [
"proc-macro2",
]
@@ -1665,9 +1663,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.8.3"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81ca098a9821bd52d6b24fd8b10bd081f47d39c22778cafaa75a2857a62c6390"
checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370"
dependencies = [
"aho-corasick 1.0.1",
"memchr",
@@ -1682,9 +1680,9 @@ checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
[[package]]
name = "regex-syntax"
version = "0.7.2"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78"
checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c"
[[package]]
name = "result-like"
@@ -1725,13 +1723,13 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.271"
version = "0.0.269"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
"bitflags 2.3.1",
"chrono",
"clap",
"clap 4.2.7",
"colored",
"dirs 5.0.1",
"fern",
@@ -1753,20 +1751,17 @@ dependencies = [
"pathdiff",
"pep440_rs",
"pretty_assertions",
"pyproject-toml",
"quick-junit",
"regex",
"result-like",
"ruff_cache",
"ruff_diagnostics",
"ruff_macros",
"ruff_newlines",
"ruff_python_ast",
"ruff_python_semantic",
"ruff_python_stdlib",
"ruff_rustpython",
"ruff_text_size",
"ruff_textwrap",
"rustc-hash",
"rustpython-format",
"rustpython-parser",
@@ -1780,6 +1775,7 @@ dependencies = [
"strum",
"strum_macros",
"test-case",
"textwrap",
"thiserror",
"toml",
"typed-arena",
@@ -1796,7 +1792,6 @@ dependencies = [
"once_cell",
"ruff",
"ruff_python_ast",
"ruff_python_formatter",
"rustpython-parser",
"serde",
"serde_json",
@@ -1818,7 +1813,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.0.271"
version = "0.0.269"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -1829,7 +1824,7 @@ dependencies = [
"bitflags 2.3.1",
"cachedir",
"chrono",
"clap",
"clap 4.2.7",
"clap_complete_command",
"clearscreen",
"colored",
@@ -1847,16 +1842,15 @@ dependencies = [
"ruff_cache",
"ruff_diagnostics",
"ruff_python_ast",
"ruff_python_formatter",
"ruff_python_stdlib",
"ruff_text_size",
"ruff_textwrap",
"rustc-hash",
"serde",
"serde_json",
"shellexpand",
"similar",
"strum",
"textwrap",
"tikv-jemallocator",
"ureq",
"walkdir",
@@ -1868,7 +1862,7 @@ name = "ruff_dev"
version = "0.0.0"
dependencies = [
"anyhow",
"clap",
"clap 4.2.7",
"itertools",
"libcst",
"once_cell",
@@ -1877,13 +1871,13 @@ dependencies = [
"ruff",
"ruff_cli",
"ruff_diagnostics",
"ruff_textwrap",
"rustpython-format",
"rustpython-parser",
"schemars",
"serde_json",
"strum",
"strum_macros",
"textwrap",
]
[[package]]
@@ -1906,7 +1900,6 @@ dependencies = [
"rustc-hash",
"schemars",
"serde",
"static_assertions",
"tracing",
"unicode-width",
]
@@ -1926,16 +1919,8 @@ dependencies = [
"itertools",
"proc-macro2",
"quote",
"ruff_textwrap",
"syn 2.0.18",
]
[[package]]
name = "ruff_newlines"
version = "0.0.0"
dependencies = [
"memchr",
"ruff_text_size",
"syn 2.0.15",
"textwrap",
]
[[package]]
@@ -1944,7 +1929,6 @@ version = "0.0.0"
dependencies = [
"anyhow",
"bitflags 2.3.1",
"insta",
"is-macro",
"itertools",
"log",
@@ -1952,7 +1936,6 @@ dependencies = [
"num-bigint",
"num-traits",
"once_cell",
"ruff_newlines",
"ruff_text_size",
"rustc-hash",
"rustpython-ast",
@@ -1967,15 +1950,14 @@ name = "ruff_python_formatter"
version = "0.0.0"
dependencies = [
"anyhow",
"clap",
"countme",
"clap 4.2.7",
"insta",
"is-macro",
"itertools",
"once_cell",
"ruff_formatter",
"ruff_newlines",
"ruff_python_ast",
"ruff_rustpython",
"ruff_testing_macros",
"ruff_text_size",
"rustc-hash",
@@ -1991,7 +1973,6 @@ dependencies = [
"bitflags 2.3.1",
"is-macro",
"nohash-hasher",
"num-traits",
"ruff_index",
"ruff_python_ast",
"ruff_python_stdlib",
@@ -2024,32 +2005,25 @@ dependencies = [
"glob",
"proc-macro2",
"quote",
"syn 2.0.18",
"syn 2.0.15",
]
[[package]]
name = "ruff_text_size"
version = "0.0.0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd#7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=335780aeeac1e6fcd85994ba001d7b8ce99fcf65#335780aeeac1e6fcd85994ba001d7b8ce99fcf65"
dependencies = [
"schemars",
"serde",
]
[[package]]
name = "ruff_textwrap"
version = "0.0.0"
dependencies = [
"ruff_newlines",
"ruff_text_size",
]
[[package]]
name = "ruff_wasm"
version = "0.0.0"
dependencies = [
"console_error_panic_hook",
"console_log",
"getrandom",
"js-sys",
"log",
"ruff",
@@ -2108,7 +2082,7 @@ dependencies = [
[[package]]
name = "rustpython-ast"
version = "0.2.0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd#7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=335780aeeac1e6fcd85994ba001d7b8ce99fcf65#335780aeeac1e6fcd85994ba001d7b8ce99fcf65"
dependencies = [
"is-macro",
"num-bigint",
@@ -2119,7 +2093,7 @@ dependencies = [
[[package]]
name = "rustpython-format"
version = "0.2.0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd#7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=335780aeeac1e6fcd85994ba001d7b8ce99fcf65#335780aeeac1e6fcd85994ba001d7b8ce99fcf65"
dependencies = [
"bitflags 2.3.1",
"itertools",
@@ -2131,7 +2105,7 @@ dependencies = [
[[package]]
name = "rustpython-literal"
version = "0.2.0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd#7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=335780aeeac1e6fcd85994ba001d7b8ce99fcf65#335780aeeac1e6fcd85994ba001d7b8ce99fcf65"
dependencies = [
"hexf-parse",
"is-macro",
@@ -2143,7 +2117,7 @@ dependencies = [
[[package]]
name = "rustpython-parser"
version = "0.2.0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd#7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=335780aeeac1e6fcd85994ba001d7b8ce99fcf65#335780aeeac1e6fcd85994ba001d7b8ce99fcf65"
dependencies = [
"anyhow",
"is-macro",
@@ -2166,7 +2140,7 @@ dependencies = [
[[package]]
name = "rustpython-parser-core"
version = "0.2.0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd#7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=335780aeeac1e6fcd85994ba001d7b8ce99fcf65#335780aeeac1e6fcd85994ba001d7b8ce99fcf65"
dependencies = [
"is-macro",
"ruff_text_size",
@@ -2273,7 +2247,7 @@ checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.18",
"syn 2.0.15",
]
[[package]]
@@ -2301,9 +2275,9 @@ dependencies = [
[[package]]
name = "serde_spanned"
version = "0.6.2"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93107647184f6027e3b7dcb2e11034cf95ffa1e3a682c67951963ac69c1c007d"
checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4"
dependencies = [
"serde",
]
@@ -2335,6 +2309,12 @@ version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
[[package]]
name = "smawk"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043"
[[package]]
name = "spin"
version = "0.5.2"
@@ -2388,9 +2368,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.18"
version = "2.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e"
checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822"
dependencies = [
"proc-macro2",
"quote",
@@ -2473,6 +2453,17 @@ dependencies = [
"test-case-core",
]
[[package]]
name = "textwrap"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d"
dependencies = [
"smawk",
"unicode-linebreak",
"unicode-width",
]
[[package]]
name = "thiserror"
version = "1.0.40"
@@ -2490,7 +2481,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.18",
"syn 2.0.15",
]
[[package]]
@@ -2570,9 +2561,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "toml"
version = "0.7.4"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6135d499e69981f9ff0ef2167955a5333c35e36f6937d382974566b3d5b94ec"
checksum = "b403acf6f2bb0859c93c7f0d967cb4a75a7ac552100f9322faf64dc047669b21"
dependencies = [
"serde",
"serde_spanned",
@@ -2582,18 +2573,18 @@ dependencies = [
[[package]]
name = "toml_datetime"
version = "0.6.2"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a76a9312f5ba4c2dec6b9161fdf25d87ad8a09256ccea5a556fef03c706a10f"
checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.19.10"
version = "0.19.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2380d56e8670370eee6566b0bfd4265f65b3f432e8c6d85623f728d4fa31f739"
checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13"
dependencies = [
"indexmap",
"serde",
@@ -2609,7 +2600,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
dependencies = [
"cfg-if",
"log",
"pin-project-lite",
"tracing-attributes",
"tracing-core",
@@ -2623,7 +2613,7 @@ checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.18",
"syn 2.0.15",
]
[[package]]
@@ -2713,9 +2703,19 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460"
[[package]]
name = "unicode-ident"
version = "1.0.9"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0"
checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4"
[[package]]
name = "unicode-linebreak"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137"
dependencies = [
"hashbrown",
"regex",
]
[[package]]
name = "unicode-normalization"
@@ -2771,7 +2771,6 @@ dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
"serde",
]
[[package]]
@@ -2782,9 +2781,9 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "uuid"
version = "1.3.3"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2"
checksum = "4dad5567ad0cf5b760e5665964bec1b47dfd077ba8a2544b513f3556d3d239a2"
[[package]]
name = "version_check"
@@ -2825,9 +2824,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.86"
version = "0.2.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bba0e8cb82ba49ff4e229459ff22a191bbe9a1cb3a341610c9c33efc27ddf73"
checksum = "5b6cb788c4e39112fbe1822277ef6fb3c55cd86b95cb3d3c4c1c9597e4ac74b4"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
@@ -2835,24 +2834,24 @@ dependencies = [
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.86"
version = "0.2.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b04bc93f9d6bdee709f6bd2118f57dd6679cf1176a1af464fca3ab0d66d8fb"
checksum = "35e522ed4105a9d626d885b35d62501b30d9666283a5c8be12c14a8bdafe7822"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.18",
"syn 2.0.15",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.36"
version = "0.4.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d1985d03709c53167ce907ff394f5316aa22cb4e12761295c5dc57dacb6297e"
checksum = "083abe15c5d88556b77bdf7aef403625be9e327ad37c62c4e4129af740168163"
dependencies = [
"cfg-if",
"js-sys",
@@ -2862,9 +2861,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.86"
version = "0.2.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14d6b024f1a526bb0234f52840389927257beb670610081360e5a03c5df9c258"
checksum = "358a79a0cb89d21db8120cbfb91392335913e4890665b1a7981d9e956903b434"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -2872,28 +2871,28 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.86"
version = "0.2.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8"
checksum = "4783ce29f09b9d93134d41297aded3a712b7b979e9c6f28c32cb88c973a94869"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.18",
"syn 2.0.15",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.86"
version = "0.2.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93"
checksum = "a901d592cafaa4d711bc324edfaff879ac700b19c3dfd60058d2b445be2691eb"
[[package]]
name = "wasm-bindgen-test"
version = "0.3.36"
version = "0.3.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9e636f3a428ff62b3742ebc3c70e254dfe12b8c2b469d688ea59cdd4abcf502"
checksum = "b27e15b4a3030b9944370ba1d8cec6f21f66a1ad4fd14725c5685600460713ec"
dependencies = [
"console_error_panic_hook",
"js-sys",
@@ -2905,9 +2904,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-test-macro"
version = "0.3.36"
version = "0.3.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f18c1fad2f7c4958e7bcce014fa212f59a65d5e3721d0f77e6c0b27ede936ba3"
checksum = "1dbaa9b9a574eac00c4f3a9c4941ac051f07632ecd0484a8588abd95af6b99d2"
dependencies = [
"proc-macro2",
"quote",
@@ -2915,9 +2914,9 @@ dependencies = [
[[package]]
name = "web-sys"
version = "0.3.63"
version = "0.3.62"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bdd9ef4e984da1187bf8110c5cf5b845fbc87a23602cdf912386a76fcd3a7c2"
checksum = "16b5f940c7edfdc6d12126d98c9ef4d1b3d470011c47c76a6581df47ad9ba721"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -3002,6 +3001,21 @@ dependencies = [
"windows-targets 0.48.0",
]
[[package]]
name = "windows-sys"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
dependencies = [
"windows_aarch64_gnullvm 0.42.2",
"windows_aarch64_msvc 0.42.2",
"windows_i686_gnu 0.42.2",
"windows_i686_msvc 0.42.2",
"windows_x86_64_gnu 0.42.2",
"windows_x86_64_gnullvm 0.42.2",
"windows_x86_64_msvc 0.42.2",
]
[[package]]
name = "windows-sys"
version = "0.45.0"

View File

@@ -3,7 +3,7 @@ members = ["crates/*"]
[workspace.package]
edition = "2021"
rust-version = "1.70"
rust-version = "1.69"
homepage = "https://beta.ruff.rs/docs/"
documentation = "https://beta.ruff.rs/docs/"
repository = "https://github.com/charliermarsh/ruff"
@@ -24,21 +24,18 @@ is-macro = { version = "0.2.2" }
itertools = { version = "0.10.5" }
libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "80e4c1399f95e5beb532fdd1e209ad2dbb470438" }
log = { version = "0.4.17" }
memchr = "2.5.0"
nohash-hasher = { version = "0.2.0" }
num-bigint = { version = "0.4.3" }
num-traits = { version = "0.2.15" }
once_cell = { version = "1.17.1" }
path-absolutize = { version = "3.0.14" }
proc-macro2 = { version = "1.0.51" }
quote = { version = "1.0.23" }
regex = { version = "1.7.1" }
rustc-hash = { version = "1.1.0" }
ruff_text_size = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd" }
rustpython-ast = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd", default-features = false, features = ["all-nodes-with-ranges"]}
rustpython-format = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd" }
rustpython-literal = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd" }
rustpython-parser = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd", default-features = false, features = ["full-lexer", "all-nodes-with-ranges"] }
ruff_text_size = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "335780aeeac1e6fcd85994ba001d7b8ce99fcf65" }
rustpython-ast = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "335780aeeac1e6fcd85994ba001d7b8ce99fcf65", default-features = false, features = ["all-nodes-with-ranges"]}
rustpython-format = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "335780aeeac1e6fcd85994ba001d7b8ce99fcf65" }
rustpython-literal = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "335780aeeac1e6fcd85994ba001d7b8ce99fcf65" }
rustpython-parser = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "335780aeeac1e6fcd85994ba001d7b8ce99fcf65", default-features = false, features = ["full-lexer", "all-nodes-with-ranges"] }
schemars = { version = "0.8.12" }
serde = { version = "1.0.152", features = ["derive"] }
serde_json = { version = "1.0.93", features = ["preserve_order"] }
@@ -49,6 +46,7 @@ strum = { version = "0.24.1", features = ["strum_macros"] }
strum_macros = { version = "0.24.3" }
syn = { version = "2.0.15" }
test-case = { version = "3.0.0" }
textwrap = { version = "0.16.0" }
toml = { version = "0.7.2" }
[profile.release]

View File

@@ -24,18 +24,17 @@ An extremely fast Python linter, written in Rust.
<i>Linting the CPython codebase from scratch.</i>
</p>
- ⚡️ 10-100x faster than existing linters
- 🐍 Installable via `pip`
- 🛠️ `pyproject.toml` support
- 🤝 Python 3.11 compatibility
- 📦 Built-in caching, to avoid re-analyzing unchanged files
- 🔧 Autofix support, for automatic error correction (e.g., automatically remove unused imports)
- 📏 Over [500 built-in rules](https://beta.ruff.rs/docs/rules/)
- ⚖️ [Near-parity](https://beta.ruff.rs/docs/faq/#how-does-ruff-compare-to-flake8) with the
built-in Flake8 rule set
- 🔌 Native re-implementations of dozens of Flake8 plugins, like flake8-bugbear
- ⌨️ First-party editor integrations for [VS Code](https://github.com/astral-sh/ruff-vscode) and [more](https://github.com/astral-sh/ruff-lsp)
- 🌎 Monorepo-friendly, with [hierarchical and cascading configuration](https://beta.ruff.rs/docs/configuration/#pyprojecttoml-discovery)
- ⚡️ 10-100x faster than existing linters
- 🐍 Installable via `pip`
- 🛠️ `pyproject.toml` support
- 🤝 Python 3.11 compatibility
- 📦 Built-in caching, to avoid re-analyzing unchanged files
- 🔧 Autofix support, for automatic error correction (e.g., automatically remove unused imports)
- 📏 Over [500 built-in rules](https://beta.ruff.rs/docs/rules/)
- ⚖️ [Near-parity](https://beta.ruff.rs/docs/faq/#how-does-ruff-compare-to-flake8) with the built-in Flake8 rule set
- 🔌 Native re-implementations of dozens of Flake8 plugins, like flake8-bugbear
- ⌨️ First-party editor integrations for [VS Code](https://github.com/astral-sh/ruff-vscode) and [more](https://github.com/astral-sh/ruff-lsp)
- 🌎 Monorepo-friendly, with [hierarchical and cascading configuration](https://beta.ruff.rs/docs/configuration/#pyprojecttoml-discovery)
Ruff aims to be orders of magnitude faster than alternative tools while integrating more
functionality behind a single, common interface.
@@ -85,8 +84,7 @@ of [Conda](https://docs.conda.io/en/latest/):
[**Timothy Crosley**](https://twitter.com/timothycrosley/status/1606420868514877440),
creator of [isort](https://github.com/PyCQA/isort):
> Just switched my first project to Ruff. Only one downside so far: it's so fast I couldn't believe
> it was working till I intentionally introduced some errors.
> Just switched my first project to Ruff. Only one downside so far: it's so fast I couldn't believe it was working till I intentionally introduced some errors.
[**Tim Abbott**](https://github.com/charliermarsh/ruff/issues/465#issuecomment-1317400028), lead
developer of [Zulip](https://github.com/zulip/zulip):
@@ -139,7 +137,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com) hook:
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.0.271
rev: 'v0.0.269'
hooks:
- id: ruff
```
@@ -244,8 +242,6 @@ stylistic rules made obsolete by the use of an autoformatter, like
If you're just getting started with Ruff, **the default rule set is a great place to start**: it
catches a wide variety of common errors (like unused imports) with zero configuration.
<!-- End section: Rules -->
Beyond the defaults, Ruff re-implements some of the most popular Flake8 plugins and related code
quality tools, including:
@@ -295,11 +291,12 @@ quality tools, including:
- [pep8-naming](https://pypi.org/project/pep8-naming/)
- [pydocstyle](https://pypi.org/project/pydocstyle/)
- [pygrep-hooks](https://github.com/pre-commit/pygrep-hooks)
- [pylint-airflow](https://pypi.org/project/pylint-airflow/)
- [pyupgrade](https://pypi.org/project/pyupgrade/)
- [tryceratops](https://pypi.org/project/tryceratops/)
- [yesqa](https://pypi.org/project/yesqa/)
<!-- End section: Rules -->
For a complete enumeration of the supported rules, see [_Rules_](https://beta.ruff.rs/docs/rules/).
## Contributing
@@ -355,9 +352,7 @@ Ruff is used by a number of major open-source projects and companies, including:
- [FastAPI](https://github.com/tiangolo/fastapi)
- [Gradio](https://github.com/gradio-app/gradio)
- [Great Expectations](https://github.com/great-expectations/great_expectations)
- Hugging Face ([Transformers](https://github.com/huggingface/transformers),
[Datasets](https://github.com/huggingface/datasets),
[Diffusers](https://github.com/huggingface/diffusers))
- Hugging Face ([Transformers](https://github.com/huggingface/transformers), [Datasets](https://github.com/huggingface/datasets), [Diffusers](https://github.com/huggingface/diffusers))
- [Hatch](https://github.com/pypa/hatch)
- [Home Assistant](https://github.com/home-assistant/core)
- [Ibis](https://github.com/ibis-project/ibis)
@@ -369,9 +364,7 @@ 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-sdk))
- Mozilla ([Firefox](https://github.com/mozilla/gecko-dev))
- [MegaLinter](https://github.com/oxsecurity/megalinter)
- Microsoft ([Semantic Kernel](https://github.com/microsoft/semantic-kernel),
[ONNX Runtime](https://github.com/microsoft/onnxruntime),
[LightGBM](https://github.com/microsoft/LightGBM))
- Microsoft ([Semantic Kernel](https://github.com/microsoft/semantic-kernel), [ONNX Runtime](https://github.com/microsoft/onnxruntime), [LightGBM](https://github.com/microsoft/LightGBM))
- Netflix ([Dispatch](https://github.com/Netflix/dispatch))
- [Neon](https://github.com/neondatabase/neon)
- [ONNX](https://github.com/onnx/onnx)

View File

@@ -1,5 +1,5 @@
[files]
extend-exclude = ["resources", "snapshots"]
extend-exclude = ["snapshots", "black"]
[default.extend-words]
trivias = "trivias"

View File

@@ -1,6 +1,6 @@
[package]
name = "flake8-to-ruff"
version = "0.0.271"
version = "0.0.269"
edition = { workspace = true }
rust-version = { workspace = true }

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.0.271"
version = "0.0.269"
authors.workspace = true
edition.workspace = true
rust-version.workspace = true
@@ -17,13 +17,11 @@ name = "ruff"
ruff_cache = { path = "../ruff_cache" }
ruff_diagnostics = { path = "../ruff_diagnostics", features = ["serde"] }
ruff_macros = { path = "../ruff_macros" }
ruff_newlines = { path = "../ruff_newlines" }
ruff_python_ast = { path = "../ruff_python_ast", features = ["serde"] }
ruff_python_semantic = { path = "../ruff_python_semantic" }
ruff_python_stdlib = { path = "../ruff_python_stdlib" }
ruff_rustpython = { path = "../ruff_rustpython" }
ruff_text_size = { workspace = true }
ruff_textwrap = { path = "../ruff_textwrap" }
annotate-snippets = { version = "0.9.1", features = ["color"] }
anyhow = { workspace = true }
@@ -43,8 +41,8 @@ libcst = { workspace = true }
log = { workspace = true }
natord = { version = "1.0.9" }
nohash-hasher = { workspace = true }
num-bigint = { workspace = true }
num-traits = { workspace = true }
num-bigint = { version = "0.4.3" }
num-traits = { version = "0.2.15" }
once_cell = { workspace = true }
path-absolutize = { workspace = true, features = [
"once_cell_cache",
@@ -52,7 +50,6 @@ path-absolutize = { workspace = true, features = [
] }
pathdiff = { version = "0.2.1" }
pep440_rs = { version = "0.3.1", features = ["serde"] }
pyproject-toml = { version = "0.6.0" }
quick-junit = { version = "0.3.2" }
regex = { workspace = true }
result-like = { version = "0.4.6" }
@@ -68,6 +65,7 @@ shellexpand = { workspace = true }
smallvec = { workspace = true }
strum = { workspace = true }
strum_macros = { workspace = true }
textwrap = { workspace = true }
thiserror = { version = "1.0.38" }
toml = { workspace = true }
typed-arena = { version = "2.0.2" }

View File

@@ -1,16 +0,0 @@
from airflow.operators import PythonOperator
def my_callable():
pass
my_task = PythonOperator(task_id="my_task", callable=my_callable)
my_task_2 = PythonOperator(callable=my_callable, task_id="my_task_2")
incorrect_name = PythonOperator(task_id="my_task")
incorrect_name_2 = PythonOperator(callable=my_callable, task_id="my_task_2")
from my_module import MyClass
incorrect_name = MyClass(task_id="my_task")

View File

@@ -1,8 +0,0 @@
import os
import subprocess
os.popen("chmod +w foo*")
subprocess.Popen("/bin/chown root: *", shell=True)
subprocess.Popen(["/usr/local/bin/rsync", "*", "some_where:"], shell=True)
subprocess.Popen("/usr/local/bin/rsync * no_injection_here:")
os.system("tar cf foo.tar bar/*")

View File

@@ -57,16 +57,12 @@ dict.fromkeys(("world",), True)
{}.deploy(True, False)
getattr(someobj, attrname, False)
mylist.index(True)
bool(False)
int(True)
str(int(False))
cfg.get("hello", True)
cfg.getint("hello", True)
cfg.getfloat("hello", True)
cfg.getboolean("hello", True)
os.set_blocking(0, False)
g_action.set_enabled(True)
settings.set_enable_developer_extras(True)
class Registry:
@@ -84,6 +80,3 @@ class Registry:
# FBT001: Boolean positional arg in function definition
def foo(self, value: bool) -> None:
pass
def foo(self) -> None:
object.__setattr__(self, "flag", True)

View File

@@ -1,7 +1,6 @@
import collections
import datetime as dt
from decimal import Decimal
from fractions import Fraction
import logging
import operator
from pathlib import Path
@@ -159,37 +158,12 @@ def float_infinity_literal(value=float("1e999")):
pass
# Allow standard floats
def float_int_okay(value=float(3)):
# But don't allow standard floats
def float_int_is_wrong(value=float(3)):
pass
def float_str_not_inf_or_nan_okay(value=float("3.14")):
pass
# Allow immutable str() value
def str_okay(value=str("foo")):
pass
# Allow immutable bool() value
def bool_okay(value=bool("bar")):
pass
# Allow immutable int() value
def int_okay(value=int("12")):
pass
# Allow immutable complex() value
def complex_okay(value=complex(1,2)):
pass
# Allow immutable Fraction() value
def fraction_okay(value=Fraction(1,2)):
def float_str_not_inf_or_nan_is_wrong(value=float("3.14")):
pass

View File

@@ -120,11 +120,3 @@ class AbstractClass(ABC):
@abstractmethod
def empty_1(self, foo: Union[str, int, list, float]):
...
from dataclasses import dataclass
@dataclass
class Foo(ABC): # noqa: B024
...

View File

@@ -9,10 +9,6 @@ def f_a_short():
raise RuntimeError("Error")
def f_a_empty():
raise RuntimeError("")
def f_b():
example = "example"
raise RuntimeError(f"This is an {example} exception")

View File

@@ -1,8 +0,0 @@
# TODO: todo
# todo: todo
# XXX: xxx
# xxx: xxx
# HACK: hack
# hack: hack
# FIXME: fixme
# fixme: fixme

View File

@@ -1,9 +0,0 @@
import collections
person: collections.namedtuple # OK
from collections import namedtuple
person: namedtuple # OK
person = namedtuple("Person", ["name", "age"]) # OK

View File

@@ -1,11 +0,0 @@
import collections
person: collections.namedtuple # Y024 Use "typing.NamedTuple" instead of "collections.namedtuple"
from collections import namedtuple
person: namedtuple # Y024 Use "typing.NamedTuple" instead of "collections.namedtuple"
person = namedtuple(
"Person", ["name", "age"]
) # Y024 Use "typing.NamedTuple" instead of "collections.namedtuple"

View File

@@ -1,19 +0,0 @@
from collections.abc import Set as AbstractSet # Ok
from collections.abc import Set # Ok
from collections.abc import (
Container,
Sized,
Set, # Ok
ValuesView
)
from collections.abc import (
Container,
Sized,
Set as AbstractSet, # Ok
ValuesView
)

View File

@@ -1,19 +0,0 @@
from collections.abc import Set as AbstractSet # Ok
from collections.abc import Set # PYI025
from collections.abc import (
Container,
Sized,
Set, # PYI025
ValuesView
)
from collections.abc import (
Container,
Sized,
Set as AbstractSet,
ValuesView # Ok
)

View File

@@ -1,57 +0,0 @@
import builtins
from abc import abstractmethod
def __repr__(self) -> str:
...
def __str__(self) -> builtins.str:
...
def __repr__(self, /, foo) -> str:
...
def __repr__(self, *, foo) -> str:
...
class ShouldRemoveSingle:
def __str__(self) -> builtins.str:
...
class ShouldRemove:
def __repr__(self) -> str:
...
def __str__(self) -> builtins.str:
...
class NoReturnSpecified:
def __str__(self):
...
def __repr__(self):
...
class NonMatchingArgs:
def __str__(self, *, extra) -> builtins.str:
...
def __repr__(self, /, extra) -> str:
...
class MatchingArgsButAbstract:
@abstractmethod
def __str__(self) -> builtins.str:
...
@abstractmethod
def __repr__(self) -> str:
...

View File

@@ -1,28 +0,0 @@
import builtins
from abc import abstractmethod
def __repr__(self) -> str: ...
def __str__(self) -> builtins.str: ...
def __repr__(self, /, foo) -> str: ...
def __repr__(self, *, foo) -> str: ...
class ShouldRemoveSingle:
def __str__(self) -> builtins.str: ... # Error: PYI029
class ShouldRemove:
def __repr__(self) -> str: ... # Error: PYI029
def __str__(self) -> builtins.str: ... # Error: PYI029
class NoReturnSpecified:
def __str__(self): ...
def __repr__(self): ...
class NonMatchingArgs:
def __str__(self, *, extra) -> builtins.str: ...
def __repr__(self, /, extra) -> str: ...
class MatchingArgsButAbstract:
@abstractmethod
def __str__(self) -> builtins.str: ...
@abstractmethod
def __repr__(self) -> str: ...

View File

@@ -1,24 +0,0 @@
from typing import Any
import typing
class Bad:
def __eq__(self, other: Any) -> bool: ... # Fine because not a stub file
def __ne__(self, other: typing.Any) -> typing.Any: ... # Fine because not a stub file
class Good:
def __eq__(self, other: object) -> bool: ...
def __ne__(self, obj: object) -> int: ...
class WeirdButFine:
def __eq__(self, other: Any, strange_extra_arg: list[str]) -> Any: ...
def __ne__(self, *, kw_only_other: Any) -> bool: ...
class Unannotated:
def __eq__(self) -> Any: ...
def __ne__(self) -> bool: ...

View File

@@ -1,24 +0,0 @@
from typing import Any
import typing
class Bad:
def __eq__(self, other: Any) -> bool: ... # Y032
def __ne__(self, other: typing.Any) -> typing.Any: ... # Y032
class Good:
def __eq__(self, other: object) -> bool: ...
def __ne__(self, obj: object) -> int: ...
class WeirdButFine:
def __eq__(self, other: Any, strange_extra_arg: list[str]) -> Any: ...
def __ne__(self, *, kw_only_other: Any) -> bool: ...
class Unannotated:
def __eq__(self) -> Any: ...
def __ne__(self) -> bool: ...

View File

@@ -1,280 +0,0 @@
# flags: --extend-ignore=Y023
import abc
import builtins
import collections.abc
import typing
from abc import abstractmethod
from collections.abc import AsyncIterable, AsyncIterator, Iterable, Iterator
from typing import Any, overload
import typing_extensions
from _typeshed import Self
from typing_extensions import final
class Bad(
object
): # Y040 Do not inherit from "object" explicitly, as it is redundant in Python 3
def __new__(cls, *args: Any, **kwargs: Any) -> Bad:
... # Y034 "__new__" methods usually return "self" at runtime. Consider using "typing_extensions.Self" in "Bad.__new__", e.g. "def __new__(cls, *args: Any, **kwargs: Any) -> Self: ..."
def __repr__(self) -> str:
... # Y029 Defining __repr__ or __str__ in a stub is almost always redundant
def __str__(self) -> builtins.str:
... # Y029 Defining __repr__ or __str__ in a stub is almost always redundant
def __eq__(self, other: Any) -> bool:
... # Y032 Prefer "object" to "Any" for the second parameter in "__eq__" methods
def __ne__(self, other: typing.Any) -> typing.Any:
... # Y032 Prefer "object" to "Any" for the second parameter in "__ne__" methods
def __enter__(self) -> Bad:
... # Y034 "__enter__" methods in classes like "Bad" usually return "self" at runtime. Consider using "typing_extensions.Self" in "Bad.__enter__", e.g. "def __enter__(self) -> Self: ..."
async def __aenter__(self) -> Bad:
... # Y034 "__aenter__" methods in classes like "Bad" usually return "self" at runtime. Consider using "typing_extensions.Self" in "Bad.__aenter__", e.g. "async def __aenter__(self) -> Self: ..."
def __iadd__(self, other: Bad) -> Bad:
... # Y034 "__iadd__" methods in classes like "Bad" usually return "self" at runtime. Consider using "typing_extensions.Self" in "Bad.__iadd__", e.g. "def __iadd__(self, other: Bad) -> Self: ..."
class AlsoBad(int, builtins.object):
... # Y040 Do not inherit from "object" explicitly, as it is redundant in Python 3
class Good:
def __new__(cls: type[Self], *args: Any, **kwargs: Any) -> Self:
...
@abstractmethod
def __str__(self) -> str:
...
@abc.abstractmethod
def __repr__(self) -> str:
...
def __eq__(self, other: object) -> bool:
...
def __ne__(self, obj: object) -> int:
...
def __enter__(self: Self) -> Self:
...
async def __aenter__(self: Self) -> Self:
...
def __ior__(self: Self, other: Self) -> Self:
...
class Fine:
@overload
def __new__(cls, foo: int) -> FineSubclass:
...
@overload
def __new__(cls, *args: Any, **kwargs: Any) -> Fine:
...
@abc.abstractmethod
def __str__(self) -> str:
...
@abc.abstractmethod
def __repr__(self) -> str:
...
def __eq__(self, other: Any, strange_extra_arg: list[str]) -> Any:
...
def __ne__(self, *, kw_only_other: Any) -> bool:
...
def __enter__(self) -> None:
...
async def __aenter__(self) -> bool:
...
class FineSubclass(Fine):
...
class StrangeButAcceptable(str):
@typing_extensions.overload
def __new__(cls, foo: int) -> StrangeButAcceptableSubclass:
...
@typing_extensions.overload
def __new__(cls, *args: Any, **kwargs: Any) -> StrangeButAcceptable:
...
def __str__(self) -> StrangeButAcceptable:
...
def __repr__(self) -> StrangeButAcceptable:
...
class StrangeButAcceptableSubclass(StrangeButAcceptable):
...
class FineAndDandy:
def __str__(self, weird_extra_arg) -> str:
...
def __repr__(self, weird_extra_arg_with_default=...) -> str:
...
@final
class WillNotBeSubclassed:
def __new__(cls, *args: Any, **kwargs: Any) -> WillNotBeSubclassed:
...
def __enter__(self) -> WillNotBeSubclassed:
...
async def __aenter__(self) -> WillNotBeSubclassed:
...
# we don't emit an error for these; out of scope for a linter
class InvalidButPluginDoesNotCrash:
def __new__() -> InvalidButPluginDoesNotCrash:
...
def __enter__() -> InvalidButPluginDoesNotCrash:
...
async def __aenter__() -> InvalidButPluginDoesNotCrash:
...
class BadIterator1(Iterator[int]):
def __iter__(self) -> Iterator[int]:
... # Y034 "__iter__" methods in classes like "BadIterator1" usually return "self" at runtime. Consider using "typing_extensions.Self" in "BadIterator1.__iter__", e.g. "def __iter__(self) -> Self: ..."
class BadIterator2(
typing.Iterator[int]
): # Y022 Use "collections.abc.Iterator[T]" instead of "typing.Iterator[T]" (PEP 585 syntax)
def __iter__(self) -> Iterator[int]:
... # Y034 "__iter__" methods in classes like "BadIterator2" usually return "self" at runtime. Consider using "typing_extensions.Self" in "BadIterator2.__iter__", e.g. "def __iter__(self) -> Self: ..."
class BadIterator3(
typing.Iterator[int]
): # Y022 Use "collections.abc.Iterator[T]" instead of "typing.Iterator[T]" (PEP 585 syntax)
def __iter__(self) -> collections.abc.Iterator[int]:
... # Y034 "__iter__" methods in classes like "BadIterator3" usually return "self" at runtime. Consider using "typing_extensions.Self" in "BadIterator3.__iter__", e.g. "def __iter__(self) -> Self: ..."
class BadIterator4(Iterator[int]):
# Note: *Iterable*, not *Iterator*, returned!
def __iter__(self) -> Iterable[int]:
... # Y034 "__iter__" methods in classes like "BadIterator4" usually return "self" at runtime. Consider using "typing_extensions.Self" in "BadIterator4.__iter__", e.g. "def __iter__(self) -> Self: ..."
class IteratorReturningIterable:
def __iter__(self) -> Iterable[str]:
... # Y045 "__iter__" methods should return an Iterator, not an Iterable
class BadAsyncIterator(collections.abc.AsyncIterator[str]):
def __aiter__(self) -> typing.AsyncIterator[str]:
... # Y034 "__aiter__" methods in classes like "BadAsyncIterator" usually return "self" at runtime. Consider using "typing_extensions.Self" in "BadAsyncIterator.__aiter__", e.g. "def __aiter__(self) -> Self: ..." # Y022 Use "collections.abc.AsyncIterator[T]" instead of "typing.AsyncIterator[T]" (PEP 585 syntax)
class AsyncIteratorReturningAsyncIterable:
def __aiter__(self) -> AsyncIterable[str]:
... # Y045 "__aiter__" methods should return an AsyncIterator, not an AsyncIterable
class Abstract(Iterator[str]):
@abstractmethod
def __iter__(self) -> Iterator[str]:
...
@abstractmethod
def __enter__(self) -> Abstract:
...
@abstractmethod
async def __aenter__(self) -> Abstract:
...
class GoodIterator(Iterator[str]):
def __iter__(self: Self) -> Self:
...
class GoodAsyncIterator(AsyncIterator[int]):
def __aiter__(self: Self) -> Self:
...
class DoesNotInheritFromIterator:
def __iter__(self) -> DoesNotInheritFromIterator:
...
class Unannotated:
def __new__(cls, *args, **kwargs):
...
def __iter__(self):
...
def __aiter__(self):
...
async def __aenter__(self):
...
def __repr__(self):
...
def __str__(self):
...
def __eq__(self):
...
def __ne__(self):
...
def __iadd__(self):
...
def __ior__(self):
...
def __repr__(self) -> str:
...
def __str__(self) -> str:
...
def __eq__(self, other: Any) -> bool:
...
def __ne__(self, other: Any) -> bool:
...
def __imul__(self, other: Any) -> list[str]:
...

View File

@@ -1,188 +0,0 @@
# flags: --extend-ignore=Y023
import abc
import builtins
import collections.abc
import typing
from abc import abstractmethod
from collections.abc import AsyncIterable, AsyncIterator, Iterable, Iterator
from typing import Any, overload
import typing_extensions
from _typeshed import Self
from typing_extensions import final
class Bad(
object
): # Y040 Do not inherit from "object" explicitly, as it is redundant in Python 3
def __new__(
cls, *args: Any, **kwargs: Any
) -> Bad: ... # Y034 "__new__" methods usually return "self" at runtime. Consider using "typing_extensions.Self" in "Bad.__new__", e.g. "def __new__(cls, *args: Any, **kwargs: Any) -> Self: ..."
def __repr__(
self,
) -> str: ... # Y029 Defining __repr__ or __str__ in a stub is almost always redundant
def __str__(
self,
) -> builtins.str: ... # Y029 Defining __repr__ or __str__ in a stub is almost always redundant
def __eq__(
self, other: Any
) -> bool: ... # Y032 Prefer "object" to "Any" for the second parameter in "__eq__" methods
def __ne__(
self, other: typing.Any
) -> typing.Any: ... # Y032 Prefer "object" to "Any" for the second parameter in "__ne__" methods
def __enter__(
self,
) -> Bad: ... # Y034 "__enter__" methods in classes like "Bad" usually return "self" at runtime. Consider using "typing_extensions.Self" in "Bad.__enter__", e.g. "def __enter__(self) -> Self: ..."
async def __aenter__(
self,
) -> Bad: ... # Y034 "__aenter__" methods in classes like "Bad" usually return "self" at runtime. Consider using "typing_extensions.Self" in "Bad.__aenter__", e.g. "async def __aenter__(self) -> Self: ..."
def __iadd__(
self, other: Bad
) -> Bad: ... # Y034 "__iadd__" methods in classes like "Bad" usually return "self" at runtime. Consider using "typing_extensions.Self" in "Bad.__iadd__", e.g. "def __iadd__(self, other: Bad) -> Self: ..."
class AlsoBad(
int, builtins.object
): ... # Y040 Do not inherit from "object" explicitly, as it is redundant in Python 3
class Good:
def __new__(cls: type[Self], *args: Any, **kwargs: Any) -> Self: ...
@abstractmethod
def __str__(self) -> str: ...
@abc.abstractmethod
def __repr__(self) -> str: ...
def __eq__(self, other: object) -> bool: ...
def __ne__(self, obj: object) -> int: ...
def __enter__(self: Self) -> Self: ...
async def __aenter__(self: Self) -> Self: ...
def __ior__(self: Self, other: Self) -> Self: ...
class Fine:
@overload
def __new__(cls, foo: int) -> FineSubclass: ...
@overload
def __new__(cls, *args: Any, **kwargs: Any) -> Fine: ...
@abc.abstractmethod
def __str__(self) -> str: ...
@abc.abstractmethod
def __repr__(self) -> str: ...
def __eq__(self, other: Any, strange_extra_arg: list[str]) -> Any: ...
def __ne__(self, *, kw_only_other: Any) -> bool: ...
def __enter__(self) -> None: ...
async def __aenter__(self) -> bool: ...
class FineSubclass(Fine): ...
class StrangeButAcceptable(str):
@typing_extensions.overload
def __new__(cls, foo: int) -> StrangeButAcceptableSubclass: ...
@typing_extensions.overload
def __new__(cls, *args: Any, **kwargs: Any) -> StrangeButAcceptable: ...
def __str__(self) -> StrangeButAcceptable: ...
def __repr__(self) -> StrangeButAcceptable: ...
class StrangeButAcceptableSubclass(StrangeButAcceptable): ...
class FineAndDandy:
def __str__(self, weird_extra_arg) -> str: ...
def __repr__(self, weird_extra_arg_with_default=...) -> str: ...
@final
class WillNotBeSubclassed:
def __new__(cls, *args: Any, **kwargs: Any) -> WillNotBeSubclassed: ...
def __enter__(self) -> WillNotBeSubclassed: ...
async def __aenter__(self) -> WillNotBeSubclassed: ...
# we don't emit an error for these; out of scope for a linter
class InvalidButPluginDoesNotCrash:
def __new__() -> InvalidButPluginDoesNotCrash: ...
def __enter__() -> InvalidButPluginDoesNotCrash: ...
async def __aenter__() -> InvalidButPluginDoesNotCrash: ...
class BadIterator1(Iterator[int]):
def __iter__(
self,
) -> Iterator[
int
]: ... # Y034 "__iter__" methods in classes like "BadIterator1" usually return "self" at runtime. Consider using "typing_extensions.Self" in "BadIterator1.__iter__", e.g. "def __iter__(self) -> Self: ..."
class BadIterator2(
typing.Iterator[int]
): # Y022 Use "collections.abc.Iterator[T]" instead of "typing.Iterator[T]" (PEP 585 syntax)
def __iter__(
self,
) -> Iterator[
int
]: ... # Y034 "__iter__" methods in classes like "BadIterator2" usually return "self" at runtime. Consider using "typing_extensions.Self" in "BadIterator2.__iter__", e.g. "def __iter__(self) -> Self: ..."
class BadIterator3(
typing.Iterator[int]
): # Y022 Use "collections.abc.Iterator[T]" instead of "typing.Iterator[T]" (PEP 585 syntax)
def __iter__(
self,
) -> collections.abc.Iterator[
int
]: ... # Y034 "__iter__" methods in classes like "BadIterator3" usually return "self" at runtime. Consider using "typing_extensions.Self" in "BadIterator3.__iter__", e.g. "def __iter__(self) -> Self: ..."
class BadIterator4(Iterator[int]):
# Note: *Iterable*, not *Iterator*, returned!
def __iter__(
self,
) -> Iterable[
int
]: ... # Y034 "__iter__" methods in classes like "BadIterator4" usually return "self" at runtime. Consider using "typing_extensions.Self" in "BadIterator4.__iter__", e.g. "def __iter__(self) -> Self: ..."
class IteratorReturningIterable:
def __iter__(
self,
) -> Iterable[
str
]: ... # Y045 "__iter__" methods should return an Iterator, not an Iterable
class BadAsyncIterator(collections.abc.AsyncIterator[str]):
def __aiter__(
self,
) -> typing.AsyncIterator[
str
]: ... # Y034 "__aiter__" methods in classes like "BadAsyncIterator" usually return "self" at runtime. Consider using "typing_extensions.Self" in "BadAsyncIterator.__aiter__", e.g. "def __aiter__(self) -> Self: ..." # Y022 Use "collections.abc.AsyncIterator[T]" instead of "typing.AsyncIterator[T]" (PEP 585 syntax)
class AsyncIteratorReturningAsyncIterable:
def __aiter__(
self,
) -> AsyncIterable[
str
]: ... # Y045 "__aiter__" methods should return an AsyncIterator, not an AsyncIterable
class Abstract(Iterator[str]):
@abstractmethod
def __iter__(self) -> Iterator[str]: ...
@abstractmethod
def __enter__(self) -> Abstract: ...
@abstractmethod
async def __aenter__(self) -> Abstract: ...
class GoodIterator(Iterator[str]):
def __iter__(self: Self) -> Self: ...
class GoodAsyncIterator(AsyncIterator[int]):
def __aiter__(self: Self) -> Self: ...
class DoesNotInheritFromIterator:
def __iter__(self) -> DoesNotInheritFromIterator: ...
class Unannotated:
def __new__(cls, *args, **kwargs): ...
def __iter__(self): ...
def __aiter__(self): ...
async def __aenter__(self): ...
def __repr__(self): ...
def __str__(self): ...
def __eq__(self): ...
def __ne__(self): ...
def __iadd__(self): ...
def __ior__(self): ...
def __repr__(self) -> str: ...
def __str__(self) -> str: ...
def __eq__(self, other: Any) -> bool: ...
def __ne__(self, other: Any) -> bool: ...
def __imul__(self, other: Any) -> list[str]: ...

View File

@@ -1,15 +0,0 @@
__all__: list[str]
__all__: list[str] = ["foo"]
class Foo:
__all__: list[str]
__match_args__: tuple[str, ...]
__slots__: tuple[str, ...]
class Bar:
__all__: list[str] = ["foo"]
__match_args__: tuple[str, ...] = (1,)
__slots__: tuple[str, ...] = "foo"

View File

@@ -1,13 +0,0 @@
__all__: list[str] # Error: PYI035
__all__: list[str] = ["foo"]
class Foo:
__all__: list[str]
__match_args__: tuple[str, ...] # Error: PYI035
__slots__: tuple[str, ...] # Error: PYI035
class Bar:
__all__: list[str] = ["foo"]
__match_args__: tuple[str, ...] = (1,)
__slots__: tuple[str, ...] = "foo"

View File

@@ -1,85 +0,0 @@
import collections.abc
import typing
from collections.abc import Iterator, Iterable
class NoReturn:
def __iter__(self):
...
class TypingIterableTReturn:
def __iter__(self) -> typing.Iterable[int]:
...
def not_iter(self) -> typing.Iterable[int]:
...
class TypingIterableReturn:
def __iter__(self) -> typing.Iterable:
...
def not_iter(self) -> typing.Iterable:
...
class CollectionsIterableTReturn:
def __iter__(self) -> collections.abc.Iterable[int]:
...
def not_iter(self) -> collections.abc.Iterable[int]:
...
class CollectionsIterableReturn:
def __iter__(self) -> collections.abc.Iterable:
...
def not_iter(self) -> collections.abc.Iterable:
...
class IterableReturn:
def __iter__(self) -> Iterable:
...
class IteratorReturn:
def __iter__(self) -> Iterator:
...
class IteratorTReturn:
def __iter__(self) -> Iterator[int]:
...
class TypingIteratorReturn:
def __iter__(self) -> typing.Iterator:
...
class TypingIteratorTReturn:
def __iter__(self) -> typing.Iterator[int]:
...
class CollectionsIteratorReturn:
def __iter__(self) -> collections.abc.Iterator:
...
class CollectionsIteratorTReturn:
def __iter__(self) -> collections.abc.Iterator[int]:
...
class TypingAsyncIterableTReturn:
def __aiter__(self) -> typing.AsyncIterable[int]:
...
class TypingAsyncIterableReturn:
def __aiter__(self) -> typing.AsyncIterable:
...

View File

@@ -1,49 +0,0 @@
import collections.abc
import typing
from collections.abc import Iterator, Iterable
class NoReturn:
def __iter__(self): ...
class TypingIterableTReturn:
def __iter__(self) -> typing.Iterable[int]: ... # Error: PYI045
def not_iter(self) -> typing.Iterable[int]: ...
class TypingIterableReturn:
def __iter__(self) -> typing.Iterable: ... # Error: PYI045
def not_iter(self) -> typing.Iterable: ...
class CollectionsIterableTReturn:
def __iter__(self) -> collections.abc.Iterable[int]: ... # Error: PYI045
def not_iter(self) -> collections.abc.Iterable[int]: ...
class CollectionsIterableReturn:
def __iter__(self) -> collections.abc.Iterable: ... # Error: PYI045
def not_iter(self) -> collections.abc.Iterable: ...
class IterableReturn:
def __iter__(self) -> Iterable: ... # Error: PYI045
class IteratorReturn:
def __iter__(self) -> Iterator: ...
class IteratorTReturn:
def __iter__(self) -> Iterator[int]: ...
class TypingIteratorReturn:
def __iter__(self) -> typing.Iterator: ...
class TypingIteratorTReturn:
def __iter__(self) -> typing.Iterator[int]: ...
class CollectionsIteratorReturn:
def __iter__(self) -> collections.abc.Iterator: ...
class CollectionsIteratorTReturn:
def __iter__(self) -> collections.abc.Iterator[int]: ...
class TypingAsyncIterableTReturn:
def __aiter__(self) -> typing.AsyncIterable[int]: ... # Error: PYI045
class TypingAsyncIterableReturn:
def __aiter__(self) -> typing.AsyncIterable: ... # Error: PYI045

View File

@@ -1,19 +0,0 @@
def bar(): # OK
...
def oof(): # OK, docstrings are handled by another rule
"""oof"""
print("foo")
def foo(): # Ok not in Stub file
"""foo"""
print("foo")
print("foo")
def buzz(): # Ok not in Stub file
print("fizz")
print("buzz")
print("test")

View File

@@ -1,20 +0,0 @@
def bar():
... # OK
def oof(): # OK, docstrings are handled by another rule
"""oof"""
print("foo")
def foo(): # ERROR PYI048
"""foo"""
print("foo")
print("foo")
def buzz(): # ERROR PYI048
print("fizz")
print("buzz")
print("test")

View File

@@ -1,38 +0,0 @@
def f1(x: str = "50 character stringggggggggggggggggggggggggggggggg") -> None:
...
def f2(x: str = "51 character stringgggggggggggggggggggggggggggggggg") -> None:
...
def f3(x: str = "50 character stringggggggggggggggggggggggggggggg\U0001f600") -> None:
...
def f4(x: str = "51 character stringgggggggggggggggggggggggggggggg\U0001f600") -> None:
...
def f5(x: bytes = b"50 character byte stringgggggggggggggggggggggggggg") -> None:
...
def f6(x: bytes = b"51 character byte stringgggggggggggggggggggggggggg") -> None:
...
def f7(x: bytes = b"50 character byte stringggggggggggggggggggggggggg\xff") -> None:
...
def f8(x: bytes = b"50 character byte stringgggggggggggggggggggggggggg\xff") -> None:
...
foo: str = "50 character stringggggggggggggggggggggggggggggggg"
bar: str = "51 character stringgggggggggggggggggggggggggggggggg"
baz: bytes = b"50 character byte stringgggggggggggggggggggggggggg"
qux: bytes = b"51 character byte stringggggggggggggggggggggggggggg\xff"

View File

@@ -1,30 +0,0 @@
def f1(x: str = "50 character stringggggggggggggggggggggggggggggggg") -> None: ... # OK
def f2(
x: str = "51 character stringgggggggggggggggggggggggggggggggg", # Error: PYI053
) -> None: ...
def f3(
x: str = "50 character stringgggggggggggggggggggggggggggggg\U0001f600", # OK
) -> None: ...
def f4(
x: str = "51 character stringggggggggggggggggggggggggggggggg\U0001f600", # Error: PYI053
) -> None: ...
def f5(
x: bytes = b"50 character byte stringgggggggggggggggggggggggggg", # OK
) -> None: ...
def f6(
x: bytes = b"51 character byte stringgggggggggggggggggggggggggg", # Error: PYI053
) -> None: ...
def f7(
x: bytes = b"50 character byte stringggggggggggggggggggggggggg\xff", # OK
) -> None: ...
def f8(
x: bytes = b"51 character byte stringgggggggggggggggggggggggggg\xff", # Error: PYI053
) -> None: ...
foo: str = "50 character stringggggggggggggggggggggggggggggggg" # OK
bar: str = "51 character stringgggggggggggggggggggggggggggggggg" # Error: PYI053
baz: bytes = b"50 character byte stringgggggggggggggggggggggggggg" # OK
qux: bytes = b"51 character byte stringggggggggggggggggggggggggggg\xff" # Error: PYI053

View File

@@ -1,20 +0,0 @@
field01: int = 0xFFFFFFFF
field02: int = 0xFFFFFFFFF
field03: int = -0xFFFFFFFF
field04: int = -0xFFFFFFFFF
field05: int = 1234567890
field06: int = 12_456_890
field07: int = 12345678901
field08: int = -1234567801
field09: int = -234_567_890
field10: float = 123.456789
field11: float = 123.4567890
field12: float = -123.456789
field13: float = -123.567_890
field14: complex = 1e1234567j
field15: complex = 1e12345678j
field16: complex = -1e1234567j
field17: complex = 1e123456789j

View File

@@ -1,20 +0,0 @@
field01: int = 0xFFFFFFFF
field02: int = 0xFFFFFFFFF # Error: PYI054
field03: int = -0xFFFFFFFF
field04: int = -0xFFFFFFFFF # Error: PYI054
field05: int = 1234567890
field06: int = 12_456_890
field07: int = 12345678901 # Error: PYI054
field08: int = -1234567801
field09: int = -234_567_890 # Error: PYI054
field10: float = 123.456789
field11: float = 123.4567890 # Error: PYI054
field12: float = -123.456789
field13: float = -123.567_890 # Error: PYI054
field14: complex = 1e1234567j
field15: complex = 1e12345678j # Error: PYI054
field16: complex = -1e1234567j
field17: complex = 1e123456789j # Error: PYI054

View File

@@ -272,34 +272,3 @@ def str_to_bool(val):
if isinstance(val, bool):
return some_obj
return val
# Mixed assignments
def function_assignment(x):
def f(): ...
return f
def class_assignment(x):
class Foo: ...
return Foo
def mixed_function_assignment(x):
if x:
def f(): ...
else:
f = 42
return f
def mixed_class_assignment(x):
if x:
class Foo: ...
else:
Foo = 42
return Foo

View File

@@ -6,4 +6,3 @@
# T001 - errors
# XXX (evanrittenhouse): this is not fine
# FIXME (evanrittenhouse): this is not fine
# foo # XXX: this isn't fine either

View File

@@ -5,4 +5,3 @@
# TODO: this has no author
# FIXME: neither does this
# TODO : and neither does this
# foo # TODO: this doesn't either

View File

@@ -26,6 +26,4 @@ def foo(x):
# TODO: followed by a new TODO with an issue link
# TDO-3870
# foo # TODO: no link!
# TODO: here's a TODO on the last line with no link

View File

@@ -4,5 +4,3 @@
# TODO this has no colon
# TODO(evanrittenhouse 😀) this has no colon
# FIXME add a colon
# foo # TODO add a colon
# TODO this has a colon but it doesn't terminate the tag, so this should throw. https://www.google.com

View File

@@ -4,4 +4,3 @@
# TODO(evanrittenhouse):
# TODO(evanrittenhouse)
# FIXME
# foo # TODO

View File

@@ -3,4 +3,3 @@
# TDO006 - error
# ToDo (evanrittenhouse): invalid capitalization
# todo (evanrittenhouse): another invalid capitalization
# foo # todo: invalid capitalization

View File

@@ -6,5 +6,3 @@
# TODO (evanrittenhouse):this doesn't either
# TODO:neither does this
# FIXME:and lastly neither does this
# foo # TODO:this is really the last one
# TODO this colon doesn't terminate the tag, so don't check it. https://www.google.com

View File

@@ -150,17 +150,3 @@ def f():
def f():
import pandas as pd
def f():
from pandas import DataFrame # noqa: TCH002
x: DataFrame = 2
def f():
from pandas import ( # noqa: TCH002
DataFrame,
)
x: DataFrame = 2

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
def f():
# Even in strict mode, this shouldn't raise an error, since `pkg` is used at runtime,
# Even in strict mode, this shouldn't rase an error, since `pkg` is used at runtime,
# and implicitly imports `pkg.bar`.
import pkg
import pkg.bar
@@ -12,7 +12,7 @@ def f():
def f():
# Even in strict mode, this shouldn't raise an error, since `pkg.bar` is used at
# Even in strict mode, this shouldn't rase an error, since `pkg.bar` is used at
# runtime, and implicitly imports `pkg`.
import pkg
import pkg.bar
@@ -22,7 +22,7 @@ def f():
def f():
# In un-strict mode, this shouldn't raise an error, since `pkg` is used at runtime.
# In un-strict mode, this shouldn't rase an error, since `pkg` is used at runtime.
import pkg
from pkg import A
@@ -31,7 +31,7 @@ def f():
def f():
# In un-strict mode, this shouldn't raise an error, since `pkg` is used at runtime.
# In un-strict mode, this shouldn't rase an error, since `pkg` is used at runtime.
from pkg import A, B
def test(value: A):
@@ -39,7 +39,7 @@ def f():
def f():
# Even in strict mode, this shouldn't raise an error, since `pkg.baz` is used at
# Even in strict mode, this shouldn't rase an error, since `pkg.baz` is used at
# runtime, and implicitly imports `pkg.bar`.
import pkg.bar
import pkg.baz
@@ -49,56 +49,9 @@ def f():
def f():
# In un-strict mode, this _should_ raise an error, since `pkg.bar` isn't used at runtime
# In un-strict mode, this _should_ rase an error, since `pkg` is used at runtime.
import pkg
from pkg.bar import A
def test(value: A):
return pkg.B()
def f():
# In un-strict mode, this shouldn't raise an error, since `pkg.bar` is used at runtime.
import pkg
import pkg.bar as B
def test(value: pkg.A):
return B()
def f():
# In un-strict mode, this shouldn't raise an error, since `pkg.foo.bar` is used at runtime.
import pkg.foo as F
import pkg.foo.bar as B
def test(value: F.Foo):
return B()
def f():
# In un-strict mode, this shouldn't raise an error, since `pkg.foo.bar` is used at runtime.
import pkg
import pkg.foo.bar as B
def test(value: pkg.A):
return B()
def f():
# In un-strict mode, this _should_ raise an error, since `pkg` isn't used at runtime.
# Note that `pkg` is a prefix of `pkgfoo` which are both different modules. This is
# testing the implementation.
import pkg
import pkgfoo.bar as B
def test(value: pkg.A):
return B()
def f():
# In un-strict mode, this shouldn't raise an error, since `pkg` is used at runtime.
import pkg.bar as B
import pkg.foo as F
def test(value: F.Foo):
return B.Bar()

View File

@@ -1,9 +0,0 @@
from typing import TYPE_CHECKING
from django.db.models import ForeignKey
if TYPE_CHECKING:
from pathlib import Path
class Foo:
var = ForeignKey["Path"]()

View File

@@ -1,15 +0,0 @@
"""Test that `__all__` exports are respected even with multiple declarations."""
import random
def some_dependency_check():
return random.uniform(0.0, 1.0) > 0.49999
if some_dependency_check():
import math
__all__ = ["math"]
else:
__all__ = []

View File

@@ -2,6 +2,3 @@
"{bar}{}".format(1, bar=2, spam=3) # F522
"{bar:{spam}}".format(bar=2, spam=3) # No issues
"{bar:{spam}}".format(bar=2, spam=3, eggs=4, ham=5) # F522
# Not fixable
(''
.format(x=2))

View File

@@ -17,17 +17,3 @@
"{0}{1}".format(1, *args) # No issues
"{0}{1}".format(1, 2, *args) # No issues
"{0}{1}".format(1, 2, 3, *args) # F523
# With nested quotes
"''1{0}".format(1, 2, 3) # F523
"\"\"{1}{0}".format(1, 2, 3) # F523
'""{1}{0}'.format(1, 2, 3) # F523
# With modified indexes
"{1}{2}".format(1, 2, 3) # F523, # F524
"{1}{3}".format(1, 2, 3, 4) # F523, # F524
"{1} {8}".format(0, 1) # F523, # F524
# Not fixable
(''
.format(2))

View File

@@ -4,4 +4,3 @@
"{0} {bar}".format(1) # F524
"{0} {bar}".format() # F524
"{bar} {0}".format() # F524
"{1} {8}".format(0, 1)

View File

@@ -83,11 +83,6 @@ def f():
pass
def f():
with (Nested(m)) as (cm):
pass
def f():
toplevel = tt = lexer.get_token()
if not tt:

View File

@@ -1,28 +0,0 @@
class Str:
def __str__(self):
return 1
class Float:
def __str__(self):
return 3.05
class Int:
def __str__(self):
return 0
class Bool:
def __str__(self):
return False
class Str2:
def __str__(self):
x = "ruff"
return x
# TODO fixme once Ruff has better type checking
def return_int():
return 3
class ComplexReturn:
def __str__(self):
return return_int()

View File

@@ -1,38 +0,0 @@
# Errors
for item in {"apples", "lemons", "water"}: # flags in-line set literals
print(f"I like {item}.")
numbers_list = [i for i in {1, 2, 3}] # flags sets in list comprehensions
numbers_set = {i for i in {1, 2, 3}} # flags sets in set comprehensions
numbers_dict = {str(i): i for i in {1, 2, 3}} # flags sets in dict comprehensions
numbers_gen = (i for i in {1, 2, 3}) # flags sets in generator expressions
# Non-errors
items = {"apples", "lemons", "water"}
for item in items: # only complains about in-line sets (as per Pylint)
print(f"I like {item}.")
for item in ["apples", "lemons", "water"]: # lists are fine
print(f"I like {item}.")
for item in ("apples", "lemons", "water"): # tuples are fine
print(f"I like {item}.")
numbers_list = [i for i in [1, 2, 3]] # lists in comprehensions are fine
numbers_set = {i for i in (1, 2, 3)} # tuples in comprehensions are fine
numbers_dict = {str(i): i for i in [1, 2, 3]} # lists in dict comprehensions are fine
numbers_gen = (i for i in (1, 2, 3)) # tuples in generator expressions are fine
for item in set(("apples", "lemons", "water")): # set constructor is fine
print(f"I like {item}.")
for number in {i for i in range(10)}: # set comprehensions are fine
print(number)

View File

@@ -25,14 +25,3 @@ min(1, min(a))
min(1, min(i for i in range(10)))
max(1, max(a))
max(1, max(i for i in range(10)))
tuples_list = [
(1, 2),
(2, 3),
(3, 4),
(4, 5),
(5, 6),
]
min(min(tuples_list))
max(max(tuples_list))

View File

@@ -17,30 +17,3 @@ def f():
def f():
nonlocal y
def f():
x = 1
def g():
nonlocal x
del x
def f():
def g():
nonlocal x
del x
def f():
try:
pass
except Exception as x:
pass
def g():
nonlocal x
x = 2

View File

@@ -1,8 +0,0 @@
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from sys import exit as bar
def main():
exit(0)

View File

@@ -1,7 +0,0 @@
async def success():
yield 42
async def fail():
l = (1, 2, 3)
yield from l

View File

@@ -1,10 +0,0 @@
from __future__ import annotations
import typing
if typing.TYPE_CHECKING:
from collections import defaultdict
def f(x: typing.DefaultDict[str, str]) -> None:
...

View File

@@ -1,8 +0,0 @@
import typing
if typing.TYPE_CHECKING:
from collections import defaultdict
def f(x: typing.DefaultDict[str, str]) -> None:
...

View File

@@ -1,8 +0,0 @@
import typing
if typing.TYPE_CHECKING:
from collections import defaultdict
def f(x: "typing.DefaultDict[str, str]") -> None:
...

View File

@@ -15,7 +15,6 @@ bytes("foo", **a)
bytes(b"foo"
b"bar")
bytes("foo")
f"{f'{str()}'}"
# These become string or byte literals
str()

View File

@@ -1,30 +0,0 @@
import sys
if sys.version_info < (3, 8):
def a():
if b:
print(1)
elif c:
print(2)
return None
else:
pass
import sys
if sys.version_info < (3, 8):
pass
else:
def a():
if b:
print(1)
elif c:
print(2)
else:
print(3)
return None

View File

@@ -2,10 +2,10 @@ import datetime
import re
import typing
from dataclasses import dataclass, field
from fractions import Fraction
from pathlib import Path
from typing import ClassVar, NamedTuple
def default_function() -> list[int]:
return []
@@ -25,12 +25,7 @@ class A:
fine_timedelta: datetime.timedelta = datetime.timedelta(hours=7)
fine_tuple: tuple[int] = tuple([1])
fine_regex: re.Pattern = re.compile(r".*")
fine_float: float = float('-inf')
fine_int: int = int(12)
fine_complex: complex = complex(1, 2)
fine_str: str = str("foo")
fine_bool: bool = bool("foo")
fine_fraction: Fraction = Fraction(1,2)
DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES = ImmutableType(40)
DEFAULT_A_FOR_ALL_DATACLASSES = A([1, 2, 3])

View File

@@ -12,8 +12,6 @@ f"{str(d['a'])}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010
f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010
f"{bla!s}, {(repr(bla))}, {(ascii(bla))}" # RUF010
f"{foo(bla)}" # OK
f"{str(bla, 'ascii')}, {str(bla, encoding='cp1255')}" # OK

View File

@@ -1,7 +0,0 @@
[project]
name = "hello-world"
version = "0.1.0"
# There's a comma missing here
dependencies = [
"tinycss2>=1.1.0<1.2",
]

View File

@@ -1,7 +0,0 @@
[project]
name = "hello-world"
version = "0.1.0"
# Ensure that the spans from toml handle utf-8 correctly
authors = [
{ name = "Z͑ͫ̓ͪ̂ͫ̽͏̴̙̤̞͉͚̯̞̠͍A̴̵̜̰͔ͫ͗͢L̠ͨͧͩ͘G̴̻͈͍͔̹̑͗̎̅͛́Ǫ̵̹̻̝̳͂̌̌͘", email = 1 }
]

View File

@@ -1,57 +0,0 @@
# This is a valid pyproject.toml
# https://github.com/PyO3/maturin/blob/87ac3d9f74dd79ef2df9a20880b9f1fa23f9a437/pyproject.toml
[build-system]
requires = ["setuptools", "wheel>=0.36.2", "tomli>=1.1.0 ; python_version<'3.11'", "setuptools-rust>=1.4.0"]
build-backend = "setuptools.build_meta"
[project]
name = "maturin"
requires-python = ">=3.7"
classifiers = [
"Topic :: Software Development :: Build Tools",
"Programming Language :: Rust",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
]
dependencies = ["tomli>=1.1.0 ; python_version<'3.11'"]
dynamic = [
"authors",
"description",
"license",
"readme",
"version"
]
[project.optional-dependencies]
zig = [
"ziglang~=0.10.0",
]
patchelf = [
"patchelf",
]
[project.urls]
"Source Code" = "https://github.com/PyO3/maturin"
Issues = "https://github.com/PyO3/maturin/issues"
Documentation = "https://maturin.rs"
Changelog = "https://maturin.rs/changelog.html"
[tool.maturin]
bindings = "bin"
[tool.black]
target_version = ['py37']
extend-exclude = '''
# Ignore cargo-generate templates
^/src/templates
'''
[tool.ruff]
line-length = 120
target-version = "py37"
[tool.mypy]
disallow_untyped_defs = true
disallow_incomplete_defs = true
warn_no_return = true
ignore_missing_imports = true

View File

@@ -1,39 +0,0 @@
# license-files is wrong here
# https://github.com/PyO3/maturin/issues/1615
[build-system]
requires = [ "maturin>=0.14", "numpy", "wheel", "patchelf",]
build-backend = "maturin"
[project]
name = "..."
license-files = [ "license.txt",]
requires-python = ">=3.8"
requires-dist = [ "maturin>=0.14", "...",]
dependencies = [ "packaging", "...",]
zip-safe = false
version = "..."
readme = "..."
description = "..."
classifiers = [ "...",]
[[project.authors]]
name = "..."
email = "..."
[project.urls]
homepage = "..."
documentation = "..."
repository = "..."
[project.optional-dependencies]
test = [ "coverage", "...",]
docs = [ "sphinx", "sphinx-rtd-theme",]
devel = []
[tool.maturin]
include = [ "...",]
bindings = "pyo3"
compatibility = "manylinux2014"
[tool.pytest.ini_options]
testpaths = [ "...",]
addopts = "--color=yes --tb=native --cov-report term --cov-report html:docs/dist_coverage --cov=aisdb --doctest-modules --envfile .env"

View File

@@ -4,7 +4,6 @@ Use '.exception' over '.error' inside except blocks
"""
import logging
import sys
logger = logging.getLogger(__name__)
@@ -61,17 +60,3 @@ def good():
a = 1
except Exception:
foo.exception("Context message here")
def fine():
try:
a = 1
except Exception:
logger.error("Context message here", exc_info=True)
def fine():
try:
a = 1
except Exception:
logger.error("Context message here", exc_info=sys.exc_info())

View File

@@ -1,15 +1,156 @@
//! Interface for generating autofix edits from higher-level actions (e.g., "remove an argument").
use anyhow::{bail, Result};
use itertools::Itertools;
use libcst_native::{
Codegen, CodegenState, ImportNames, ParenthesizableWhitespace, SmallStatement, Statement,
};
use ruff_text_size::{TextLen, TextRange, TextSize};
use rustpython_parser::ast::{self, Excepthandler, Expr, Keyword, Ranged, Stmt};
use rustpython_parser::{lexer, Mode, Tok};
use ruff_diagnostics::Edit;
use ruff_newlines::NewlineWithTrailingNewline;
use ruff_python_ast::helpers;
use ruff_python_ast::newlines::NewlineWithTrailingNewline;
use ruff_python_ast::source_code::{Indexer, Locator, Stylist};
use crate::autofix::codemods;
use crate::cst::helpers::compose_module_path;
use crate::cst::matchers::match_statement;
/// Determine if a body contains only a single statement, taking into account
/// deleted.
fn has_single_child(body: &[Stmt], deleted: &[&Stmt]) -> bool {
body.iter().filter(|child| !deleted.contains(child)).count() == 1
}
/// Determine if a child is the only statement in its body.
fn is_lone_child(child: &Stmt, parent: &Stmt, deleted: &[&Stmt]) -> Result<bool> {
match parent {
Stmt::FunctionDef(ast::StmtFunctionDef { body, .. })
| Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { body, .. })
| Stmt::ClassDef(ast::StmtClassDef { body, .. })
| Stmt::With(ast::StmtWith { body, .. })
| Stmt::AsyncWith(ast::StmtAsyncWith { body, .. }) => {
if body.iter().contains(child) {
Ok(has_single_child(body, deleted))
} else {
bail!("Unable to find child in parent body")
}
}
Stmt::For(ast::StmtFor { body, orelse, .. })
| Stmt::AsyncFor(ast::StmtAsyncFor { body, orelse, .. })
| Stmt::While(ast::StmtWhile { body, orelse, .. })
| Stmt::If(ast::StmtIf { body, orelse, .. }) => {
if body.iter().contains(child) {
Ok(has_single_child(body, deleted))
} else if orelse.iter().contains(child) {
Ok(has_single_child(orelse, deleted))
} else {
bail!("Unable to find child in parent body")
}
}
Stmt::Try(ast::StmtTry {
body,
handlers,
orelse,
finalbody,
range: _,
})
| Stmt::TryStar(ast::StmtTryStar {
body,
handlers,
orelse,
finalbody,
range: _,
}) => {
if body.iter().contains(child) {
Ok(has_single_child(body, deleted))
} else if orelse.iter().contains(child) {
Ok(has_single_child(orelse, deleted))
} else if finalbody.iter().contains(child) {
Ok(has_single_child(finalbody, deleted))
} else if let Some(body) = handlers.iter().find_map(|handler| match handler {
Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { body, .. }) => {
if body.iter().contains(child) {
Some(body)
} else {
None
}
}
}) {
Ok(has_single_child(body, deleted))
} else {
bail!("Unable to find child in parent body")
}
}
Stmt::Match(ast::StmtMatch { cases, .. }) => {
if let Some(body) = cases.iter().find_map(|case| {
if case.body.iter().contains(child) {
Some(&case.body)
} else {
None
}
}) {
Ok(has_single_child(body, deleted))
} else {
bail!("Unable to find child in parent body")
}
}
_ => bail!("Unable to find child in parent body"),
}
}
/// Return the location of a trailing semicolon following a `Stmt`, if it's part
/// of a multi-statement line.
fn trailing_semicolon(stmt: &Stmt, locator: &Locator) -> Option<TextSize> {
let contents = locator.after(stmt.end());
for line in NewlineWithTrailingNewline::from(contents) {
let trimmed = line.trim_start();
if trimmed.starts_with(';') {
let colon_offset = line.text_len() - trimmed.text_len();
return Some(stmt.end() + line.start() + colon_offset);
}
if !trimmed.starts_with('\\') {
break;
}
}
None
}
/// Find the next valid break for a `Stmt` after a semicolon.
fn next_stmt_break(semicolon: TextSize, locator: &Locator) -> TextSize {
let start_location = semicolon + TextSize::from(1);
let contents = &locator.contents()[usize::from(start_location)..];
for line in NewlineWithTrailingNewline::from(contents) {
let trimmed = line.trim();
// Skip past any continuations.
if trimmed.starts_with('\\') {
continue;
}
return start_location
+ if trimmed.is_empty() {
// If the line is empty, then despite the previous statement ending in a
// semicolon, we know that it's not a multi-statement line.
line.start()
} else {
// Otherwise, find the start of the next statement. (Or, anything that isn't
// whitespace.)
let relative_offset = line.find(|c: char| !c.is_whitespace()).unwrap();
line.start() + TextSize::try_from(relative_offset).unwrap()
};
}
locator.line_end(start_location)
}
/// Return `true` if a `Stmt` occurs at the end of a file.
fn is_end_of_file(stmt: &Stmt, locator: &Locator) -> bool {
stmt.end() == locator.contents().text_len()
}
/// Return the `Fix` to use when deleting a `Stmt`.
///
@@ -27,19 +168,21 @@ use crate::autofix::codemods;
pub(crate) fn delete_stmt(
stmt: &Stmt,
parent: Option<&Stmt>,
deleted: &[&Stmt],
locator: &Locator,
indexer: &Indexer,
stylist: &Stylist,
) -> Edit {
) -> Result<Edit> {
if parent
.map(|parent| is_lone_child(stmt, parent))
.map(|parent| is_lone_child(stmt, parent, deleted))
.map_or(Ok(None), |v| v.map(Some))?
.unwrap_or_default()
{
// If removing this node would lead to an invalid syntax tree, replace
// it with a `pass`.
Edit::range_replacement("pass".to_string(), stmt.range())
Ok(Edit::range_replacement("pass".to_string(), stmt.range()))
} else {
if let Some(semicolon) = trailing_semicolon(stmt, locator) {
Ok(if let Some(semicolon) = trailing_semicolon(stmt, locator) {
let next = next_stmt_break(semicolon, locator);
Edit::deletion(stmt.start(), next)
} else if helpers::has_leading_content(stmt, locator) {
@@ -54,22 +197,124 @@ pub(crate) fn delete_stmt(
} else {
let range = locator.full_lines_range(stmt.range());
Edit::range_deletion(range)
}
})
}
}
/// Generate a `Fix` to remove the specified imports from an `import` statement.
/// Generate a `Fix` to remove any unused imports from an `import` statement.
pub(crate) fn remove_unused_imports<'a>(
unused_imports: impl Iterator<Item = &'a str>,
stmt: &Stmt,
parent: Option<&Stmt>,
deleted: &[&Stmt],
locator: &Locator,
indexer: &Indexer,
stylist: &Stylist,
) -> Result<Edit> {
match codemods::remove_imports(unused_imports, stmt, locator, stylist)? {
None => Ok(delete_stmt(stmt, parent, locator, indexer, stylist)),
Some(content) => Ok(Edit::range_replacement(content, stmt.range())),
let module_text = locator.slice(stmt.range());
let mut tree = match_statement(module_text)?;
let Statement::Simple(body) = &mut tree else {
bail!("Expected Statement::Simple");
};
let (aliases, import_module) = match body.body.first_mut() {
Some(SmallStatement::Import(import_body)) => (&mut import_body.names, None),
Some(SmallStatement::ImportFrom(import_body)) => {
if let ImportNames::Aliases(names) = &mut import_body.names {
(
names,
Some((&import_body.relative, import_body.module.as_ref())),
)
} else if let ImportNames::Star(..) = &import_body.names {
// Special-case: if the import is a `from ... import *`, then we delete the
// entire statement.
let mut found_star = false;
for unused_import in unused_imports {
let full_name = match import_body.module.as_ref() {
Some(module_name) => format!("{}.*", compose_module_path(module_name)),
None => "*".to_string(),
};
if unused_import == full_name {
found_star = true;
} else {
bail!(
"Expected \"*\" for unused import (got: \"{}\")",
unused_import
);
}
}
if !found_star {
bail!("Expected \'*\' for unused import");
}
return delete_stmt(stmt, parent, deleted, locator, indexer, stylist);
} else {
bail!("Expected: ImportNames::Aliases | ImportNames::Star");
}
}
_ => bail!("Expected: SmallStatement::ImportFrom | SmallStatement::Import"),
};
// Preserve the trailing comma (or not) from the last entry.
let trailing_comma = aliases.last().and_then(|alias| alias.comma.clone());
for unused_import in unused_imports {
let alias_index = aliases.iter().position(|alias| {
let full_name = match import_module {
Some((relative, module)) => {
let module = module.map(compose_module_path);
let member = compose_module_path(&alias.name);
let mut full_name = String::with_capacity(
relative.len() + module.as_ref().map_or(0, String::len) + member.len() + 1,
);
for _ in 0..relative.len() {
full_name.push('.');
}
if let Some(module) = module {
full_name.push_str(&module);
full_name.push('.');
}
full_name.push_str(&member);
full_name
}
None => compose_module_path(&alias.name),
};
full_name == unused_import
});
if let Some(index) = alias_index {
aliases.remove(index);
}
}
// But avoid destroying any trailing comments.
if let Some(alias) = aliases.last_mut() {
let has_comment = if let Some(comma) = &alias.comma {
match &comma.whitespace_after {
ParenthesizableWhitespace::SimpleWhitespace(_) => false,
ParenthesizableWhitespace::ParenthesizedWhitespace(whitespace) => {
whitespace.first_line.comment.is_some()
}
}
} else {
false
};
if !has_comment {
alias.comma = trailing_comma;
}
}
if aliases.is_empty() {
delete_stmt(stmt, parent, deleted, locator, indexer, stylist)
} else {
let mut state = CodegenState {
default_newline: &stylist.line_ending(),
default_indent: stylist.indentation(),
..CodegenState::default()
};
tree.codegen(&mut state);
Ok(Edit::range_replacement(state.to_string(), stmt.range()))
}
}
@@ -100,7 +345,7 @@ pub(crate) fn remove_argument(
if n_arguments == 1 {
// Case 1: there is only one argument.
let mut count = 0u32;
let mut count: usize = 0;
for (tok, range) in lexer::lex_starts_at(contents, Mode::Module, call_at).flatten() {
if matches!(tok, Tok::Lpar) {
if count == 0 {
@@ -110,11 +355,11 @@ pub(crate) fn remove_argument(
range.start() + TextSize::from(1)
});
}
count = count.saturating_add(1);
count += 1;
}
if matches!(tok, Tok::Rpar) {
count = count.saturating_sub(1);
count -= 1;
if count == 0 {
fix_end = Some(if remove_parentheses {
range.end()
@@ -175,147 +420,32 @@ pub(crate) fn remove_argument(
}
}
/// Determine if a vector contains only one, specific element.
fn is_only<T: PartialEq>(vec: &[T], value: &T) -> bool {
vec.len() == 1 && vec[0] == *value
}
/// Determine if a child is the only statement in its body.
fn is_lone_child(child: &Stmt, parent: &Stmt) -> bool {
match parent {
Stmt::FunctionDef(ast::StmtFunctionDef { body, .. })
| Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { body, .. })
| Stmt::ClassDef(ast::StmtClassDef { body, .. })
| Stmt::With(ast::StmtWith { body, .. })
| Stmt::AsyncWith(ast::StmtAsyncWith { body, .. }) => {
if is_only(body, child) {
return true;
}
}
Stmt::For(ast::StmtFor { body, orelse, .. })
| Stmt::AsyncFor(ast::StmtAsyncFor { body, orelse, .. })
| Stmt::While(ast::StmtWhile { body, orelse, .. })
| Stmt::If(ast::StmtIf { body, orelse, .. }) => {
if is_only(body, child) || is_only(orelse, child) {
return true;
}
}
Stmt::Try(ast::StmtTry {
body,
handlers,
orelse,
finalbody,
range: _,
})
| Stmt::TryStar(ast::StmtTryStar {
body,
handlers,
orelse,
finalbody,
range: _,
}) => {
if is_only(body, child)
|| is_only(orelse, child)
|| is_only(finalbody, child)
|| handlers.iter().any(|handler| match handler {
Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler {
body, ..
}) => is_only(body, child),
})
{
return true;
}
}
Stmt::Match(ast::StmtMatch { cases, .. }) => {
if cases.iter().any(|case| is_only(&case.body, child)) {
return true;
}
}
_ => {}
}
false
}
/// Return the location of a trailing semicolon following a `Stmt`, if it's part
/// of a multi-statement line.
fn trailing_semicolon(stmt: &Stmt, locator: &Locator) -> Option<TextSize> {
let contents = locator.after(stmt.end());
for line in NewlineWithTrailingNewline::from(contents) {
let trimmed = line.trim_start();
if trimmed.starts_with(';') {
let colon_offset = line.text_len() - trimmed.text_len();
return Some(stmt.end() + line.start() + colon_offset);
}
if !trimmed.starts_with('\\') {
break;
}
}
None
}
/// Find the next valid break for a `Stmt` after a semicolon.
fn next_stmt_break(semicolon: TextSize, locator: &Locator) -> TextSize {
let start_location = semicolon + TextSize::from(1);
let contents = &locator.contents()[usize::from(start_location)..];
for line in NewlineWithTrailingNewline::from(contents) {
let trimmed = line.trim();
// Skip past any continuations.
if trimmed.starts_with('\\') {
continue;
}
return start_location
+ if trimmed.is_empty() {
// If the line is empty, then despite the previous statement ending in a
// semicolon, we know that it's not a multi-statement line.
line.start()
} else {
// Otherwise, find the start of the next statement. (Or, anything that isn't
// whitespace.)
let relative_offset = line.find(|c: char| !c.is_whitespace()).unwrap();
line.start() + TextSize::try_from(relative_offset).unwrap()
};
}
locator.line_end(start_location)
}
/// Return `true` if a `Stmt` occurs at the end of a file.
fn is_end_of_file(stmt: &Stmt, locator: &Locator) -> bool {
stmt.end() == locator.contents().text_len()
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use ruff_text_size::TextSize;
use rustpython_parser::ast::Suite;
use rustpython_parser::Parse;
use rustpython_parser as parser;
use ruff_python_ast::source_code::Locator;
use crate::autofix::edits::{next_stmt_break, trailing_semicolon};
use crate::autofix::actions::{next_stmt_break, trailing_semicolon};
#[test]
fn find_semicolon() -> Result<()> {
let contents = "x = 1";
let program = Suite::parse(contents, "<filename>")?;
let program = parser::parse_program(contents, "<filename>")?;
let stmt = program.first().unwrap();
let locator = Locator::new(contents);
assert_eq!(trailing_semicolon(stmt, &locator), None);
let contents = "x = 1; y = 1";
let program = Suite::parse(contents, "<filename>")?;
let program = parser::parse_program(contents, "<filename>")?;
let stmt = program.first().unwrap();
let locator = Locator::new(contents);
assert_eq!(trailing_semicolon(stmt, &locator), Some(TextSize::from(5)));
let contents = "x = 1 ; y = 1";
let program = Suite::parse(contents, "<filename>")?;
let program = parser::parse_program(contents, "<filename>")?;
let stmt = program.first().unwrap();
let locator = Locator::new(contents);
assert_eq!(trailing_semicolon(stmt, &locator), Some(TextSize::from(6)));
@@ -325,7 +455,7 @@ x = 1 \
; y = 1
"#
.trim();
let program = Suite::parse(contents, "<filename>")?;
let program = parser::parse_program(contents, "<filename>")?;
let stmt = program.first().unwrap();
let locator = Locator::new(contents);
assert_eq!(trailing_semicolon(stmt, &locator), Some(TextSize::from(10)));

View File

@@ -1,214 +0,0 @@
//! Interface for editing code snippets. These functions take statements or expressions as input,
//! and return the modified code snippet as output.
use anyhow::{bail, Result};
use libcst_native::{
Codegen, CodegenState, ImportNames, ParenthesizableWhitespace, SmallStatement, Statement,
};
use rustpython_parser::ast::{Ranged, Stmt};
use ruff_python_ast::source_code::{Locator, Stylist};
use crate::cst::helpers::compose_module_path;
use crate::cst::matchers::match_statement;
/// Glue code to make libcst codegen work with ruff's Stylist
pub(crate) trait CodegenStylist<'a>: Codegen<'a> {
fn codegen_stylist(&self, stylist: &'a Stylist) -> String;
}
impl<'a, T: Codegen<'a>> CodegenStylist<'a> for T {
fn codegen_stylist(&self, stylist: &'a Stylist) -> String {
let mut state = CodegenState {
default_newline: stylist.line_ending().as_str(),
default_indent: stylist.indentation(),
..Default::default()
};
self.codegen(&mut state);
state.to_string()
}
}
/// Given an import statement, remove any imports that are specified in the `imports` iterator.
///
/// Returns `Ok(None)` if the statement is empty after removing the imports.
pub(crate) fn remove_imports<'a>(
imports: impl Iterator<Item = &'a str>,
stmt: &Stmt,
locator: &Locator,
stylist: &Stylist,
) -> Result<Option<String>> {
let module_text = locator.slice(stmt.range());
let mut tree = match_statement(module_text)?;
let Statement::Simple(body) = &mut tree else {
bail!("Expected Statement::Simple");
};
let (aliases, import_module) = match body.body.first_mut() {
Some(SmallStatement::Import(import_body)) => (&mut import_body.names, None),
Some(SmallStatement::ImportFrom(import_body)) => {
if let ImportNames::Aliases(names) = &mut import_body.names {
(
names,
Some((&import_body.relative, import_body.module.as_ref())),
)
} else if let ImportNames::Star(..) = &import_body.names {
// Special-case: if the import is a `from ... import *`, then we delete the
// entire statement.
let mut found_star = false;
for import in imports {
let qualified_name = match import_body.module.as_ref() {
Some(module_name) => format!("{}.*", compose_module_path(module_name)),
None => "*".to_string(),
};
if import == qualified_name {
found_star = true;
} else {
bail!("Expected \"*\" for unused import (got: \"{}\")", import);
}
}
if !found_star {
bail!("Expected \'*\' for unused import");
}
return Ok(None);
} else {
bail!("Expected: ImportNames::Aliases | ImportNames::Star");
}
}
_ => bail!("Expected: SmallStatement::ImportFrom | SmallStatement::Import"),
};
// Preserve the trailing comma (or not) from the last entry.
let trailing_comma = aliases.last().and_then(|alias| alias.comma.clone());
for import in imports {
let alias_index = aliases.iter().position(|alias| {
let qualified_name = match import_module {
Some((relative, module)) => {
let module = module.map(compose_module_path);
let member = compose_module_path(&alias.name);
let mut qualified_name = String::with_capacity(
relative.len() + module.as_ref().map_or(0, String::len) + member.len() + 1,
);
for _ in 0..relative.len() {
qualified_name.push('.');
}
if let Some(module) = module {
qualified_name.push_str(&module);
qualified_name.push('.');
}
qualified_name.push_str(&member);
qualified_name
}
None => compose_module_path(&alias.name),
};
qualified_name == import
});
if let Some(index) = alias_index {
aliases.remove(index);
}
}
// But avoid destroying any trailing comments.
if let Some(alias) = aliases.last_mut() {
let has_comment = if let Some(comma) = &alias.comma {
match &comma.whitespace_after {
ParenthesizableWhitespace::SimpleWhitespace(_) => false,
ParenthesizableWhitespace::ParenthesizedWhitespace(whitespace) => {
whitespace.first_line.comment.is_some()
}
}
} else {
false
};
if !has_comment {
alias.comma = trailing_comma;
}
}
if aliases.is_empty() {
return Ok(None);
}
Ok(Some(tree.codegen_stylist(stylist)))
}
/// Given an import statement, remove any imports that are not specified in the `imports` slice.
///
/// Returns the modified import statement.
pub(crate) fn retain_imports(
imports: &[&str],
stmt: &Stmt,
locator: &Locator,
stylist: &Stylist,
) -> Result<String> {
let module_text = locator.slice(stmt.range());
let mut tree = match_statement(module_text)?;
let Statement::Simple(body) = &mut tree else {
bail!("Expected Statement::Simple");
};
let (aliases, import_module) = match body.body.first_mut() {
Some(SmallStatement::Import(import_body)) => (&mut import_body.names, None),
Some(SmallStatement::ImportFrom(import_body)) => {
if let ImportNames::Aliases(names) = &mut import_body.names {
(
names,
Some((&import_body.relative, import_body.module.as_ref())),
)
} else {
bail!("Expected: ImportNames::Aliases");
}
}
_ => bail!("Expected: SmallStatement::ImportFrom | SmallStatement::Import"),
};
// Preserve the trailing comma (or not) from the last entry.
let trailing_comma = aliases.last().and_then(|alias| alias.comma.clone());
aliases.retain(|alias| {
imports.iter().any(|import| {
let qualified_name = match import_module {
Some((relative, module)) => {
let module = module.map(compose_module_path);
let member = compose_module_path(&alias.name);
let mut qualified_name = String::with_capacity(
relative.len() + module.as_ref().map_or(0, String::len) + member.len() + 1,
);
for _ in 0..relative.len() {
qualified_name.push('.');
}
if let Some(module) = module {
qualified_name.push_str(&module);
qualified_name.push('.');
}
qualified_name.push_str(&member);
qualified_name
}
None => compose_module_path(&alias.name),
};
qualified_name == *import
})
});
// But avoid destroying any trailing comments.
if let Some(alias) = aliases.last_mut() {
let has_comment = if let Some(comma) = &alias.comma {
match &comma.whitespace_after {
ParenthesizableWhitespace::SimpleWhitespace(_) => false,
ParenthesizableWhitespace::ParenthesizedWhitespace(whitespace) => {
whitespace.first_line.comment.is_some()
}
}
} else {
false
};
if !has_comment {
alias.comma = trailing_comma;
}
}
Ok(tree.codegen_stylist(stylist))
}

View File

@@ -1,18 +1,16 @@
use std::collections::BTreeSet;
use itertools::Itertools;
use nohash_hasher::IntSet;
use ruff_text_size::{TextRange, TextSize};
use rustc_hash::FxHashMap;
use ruff_diagnostics::{Diagnostic, Edit, Fix, IsolationLevel};
use ruff_diagnostics::{Diagnostic, Edit, Fix};
use ruff_python_ast::source_code::Locator;
use crate::linter::FixTable;
use crate::registry::{AsRule, Rule};
pub(crate) mod codemods;
pub(crate) mod edits;
pub(crate) mod actions;
/// Auto-fix errors in a file, and write the fixed source code to disk.
pub(crate) fn fix_file(
@@ -39,7 +37,6 @@ fn apply_fixes<'a>(
let mut output = String::with_capacity(locator.len());
let mut last_pos: Option<TextSize> = None;
let mut applied: BTreeSet<&Edit> = BTreeSet::default();
let mut isolated: IntSet<u32> = IntSet::default();
let mut fixed = FxHashMap::default();
for (rule, fix) in diagnostics
@@ -67,19 +64,7 @@ fn apply_fixes<'a>(
continue;
}
// If this fix requires isolation, and we've already applied another fix in the
// same isolation group, skip it.
if let IsolationLevel::Group(id) = fix.isolation() {
if !isolated.insert(id) {
continue;
}
}
for edit in fix
.edits()
.iter()
.sorted_unstable_by_key(|edit| edit.start())
{
for edit in fix.edits() {
// Add all contents from `last_pos` to `fix.location`.
let slice = locator.slice(TextRange::new(last_pos.unwrap_or_default(), edit.start()));
output.push_str(slice);

File diff suppressed because it is too large Load Diff

View File

@@ -39,15 +39,9 @@ pub(crate) fn check_logical_lines(
let mut context = LogicalLinesContext::new(settings);
let should_fix_missing_whitespace = settings.rules.should_fix(Rule::MissingWhitespace);
let should_fix_whitespace_before_parameters =
settings.rules.should_fix(Rule::WhitespaceBeforeParameters);
let should_fix_whitespace_after_open_bracket =
settings.rules.should_fix(Rule::WhitespaceAfterOpenBracket);
let should_fix_whitespace_before_close_bracket = settings
.rules
.should_fix(Rule::WhitespaceBeforeCloseBracket);
let should_fix_whitespace_before_punctuation =
settings.rules.should_fix(Rule::WhitespaceBeforePunctuation);
let mut prev_line = None;
let mut prev_indent_level = None;
@@ -65,13 +59,7 @@ pub(crate) fn check_logical_lines(
.flags()
.intersects(TokenFlags::OPERATOR | TokenFlags::BRACKET | TokenFlags::PUNCTUATION)
{
extraneous_whitespace(
&line,
&mut context,
should_fix_whitespace_after_open_bracket,
should_fix_whitespace_before_close_bracket,
should_fix_whitespace_before_punctuation,
);
extraneous_whitespace(&line, &mut context);
}
if line.flags().contains(TokenFlags::KEYWORD) {

View File

@@ -18,9 +18,10 @@ pub(crate) fn check_noqa(
locator: &Locator,
comment_ranges: &[TextRange],
noqa_line_for: &NoqaMapping,
analyze_directives: bool,
settings: &Settings,
) -> Vec<usize> {
let enforce_noqa = settings.rules.enabled(Rule::UnusedNOQA);
// Identify any codes that are globally exempted (within the current file).
let exemption = noqa::file_exemption(locator.contents(), comment_ranges);
@@ -92,7 +93,7 @@ pub(crate) fn check_noqa(
}
// Enforce that the noqa directive was actually used (RUF100).
if analyze_directives && settings.rules.enabled(Rule::UnusedNOQA) {
if enforce_noqa {
for line in noqa_directives.lines() {
match &line.directive {
Directive::All(leading_spaces, noqa_range, trailing_spaces) => {

View File

@@ -4,7 +4,7 @@ use ruff_text_size::TextSize;
use std::path::Path;
use ruff_diagnostics::Diagnostic;
use ruff_newlines::StrExt;
use ruff_python_ast::newlines::StrExt;
use ruff_python_ast::source_code::{Indexer, Locator, Stylist};
use crate::registry::Rule;

View File

@@ -3,13 +3,12 @@
use rustpython_parser::lexer::LexResult;
use rustpython_parser::Tok;
use crate::directives::TodoComment;
use crate::lex::docstring_detection::StateMachine;
use crate::registry::{AsRule, Rule};
use crate::rules::ruff::rules::Context;
use crate::rules::{
eradicate, flake8_commas, flake8_fixme, flake8_implicit_str_concat, flake8_pyi, flake8_quotes,
flake8_todos, pycodestyle, pylint, pyupgrade, ruff,
eradicate, flake8_commas, flake8_implicit_str_concat, flake8_pyi, flake8_quotes, flake8_todos,
pycodestyle, pylint, pyupgrade, ruff,
};
use crate::settings::Settings;
use ruff_diagnostics::Diagnostic;
@@ -61,8 +60,6 @@ pub(crate) fn check_tokens(
]);
let enforce_extraneous_parenthesis = settings.rules.enabled(Rule::ExtraneousParentheses);
let enforce_type_comment_in_stub = settings.rules.enabled(Rule::TypeCommentInStub);
// Combine flake8_todos and flake8_fixme so that we can reuse detected [`TodoDirective`]s.
let enforce_todos = settings.rules.any_enabled(&[
Rule::InvalidTodoTag,
Rule::MissingTodoAuthor,
@@ -71,10 +68,6 @@ pub(crate) fn check_tokens(
Rule::MissingTodoDescription,
Rule::InvalidTodoCapitalization,
Rule::MissingSpaceAfterTodoColon,
Rule::LineContainsFixme,
Rule::LineContainsXxx,
Rule::LineContainsTodo,
Rule::LineContainsHack,
]);
// RUF001, RUF002, RUF003
@@ -191,26 +184,9 @@ pub(crate) fn check_tokens(
}
// TD001, TD002, TD003, TD004, TD005, TD006, TD007
// T001, T002, T003, T004
if enforce_todos {
let todo_comments: Vec<TodoComment> = indexer
.comment_ranges()
.iter()
.enumerate()
.filter_map(|(i, comment_range)| {
let comment = locator.slice(*comment_range);
TodoComment::from_comment(comment, *comment_range, i)
})
.collect();
diagnostics.extend(
flake8_todos::rules::todos(&todo_comments, indexer, locator, settings)
.into_iter()
.filter(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())),
);
diagnostics.extend(
flake8_fixme::rules::todos(&todo_comments)
flake8_todos::rules::todos(indexer, locator, settings)
.into_iter()
.filter(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())),
);

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@ use libcst_native::{
Arg, Attribute, Call, Comparison, CompoundStatement, Dict, Expression, FormattedString,
FormattedStringContent, FormattedStringExpression, FunctionDef, GeneratorExp, If, Import,
ImportAlias, ImportFrom, ImportNames, IndentedBlock, Lambda, ListComp, Module, Name,
SmallStatement, Statement, Suite, Tuple, With,
SimpleString, SmallStatement, Statement, Suite, Tuple, With,
};
pub(crate) fn match_module(module_text: &str) -> Result<Module> {
@@ -109,6 +109,16 @@ pub(crate) fn match_attribute<'a, 'b>(
}
}
pub(crate) fn match_simple_string<'a, 'b>(
expression: &'a mut Expression<'b>,
) -> Result<&'a mut SimpleString<'b>> {
if let Expression::SimpleString(simple_string) = expression {
Ok(simple_string)
} else {
bail!("Expected Expression::SimpleString")
}
}
pub(crate) fn match_formatted_string<'a, 'b>(
expression: &'a mut Expression<'b>,
) -> Result<&'a mut FormattedString<'b>> {

View File

@@ -1,6 +1,4 @@
//! Extract `# noqa`, `# isort: skip`, and `# TODO` directives from tokenized source.
use std::str::FromStr;
//! Extract `# noqa` and `# isort: skip` directives from tokenized source.
use bitflags::bitflags;
use ruff_text_size::{TextLen, TextRange, TextSize};
@@ -219,133 +217,6 @@ fn extract_isort_directives(lxr: &[LexResult], locator: &Locator) -> IsortDirect
}
}
/// A comment that contains a [`TodoDirective`]
pub(crate) struct TodoComment<'a> {
/// The comment's text
pub(crate) content: &'a str,
/// The directive found within the comment.
pub(crate) directive: TodoDirective<'a>,
/// The comment's actual [`TextRange`].
pub(crate) range: TextRange,
/// The comment range's position in [`Indexer`].comment_ranges()
pub(crate) range_index: usize,
}
impl<'a> TodoComment<'a> {
/// Attempt to transform a normal comment into a [`TodoComment`].
pub(crate) fn from_comment(
content: &'a str,
range: TextRange,
range_index: usize,
) -> Option<Self> {
TodoDirective::from_comment(content, range).map(|directive| Self {
content,
directive,
range,
range_index,
})
}
}
#[derive(Debug, PartialEq)]
pub(crate) struct TodoDirective<'a> {
/// The actual directive
pub(crate) content: &'a str,
/// The directive's [`TextRange`] in the file.
pub(crate) range: TextRange,
/// The directive's kind: HACK, XXX, FIXME, or TODO.
pub(crate) kind: TodoDirectiveKind,
}
impl<'a> TodoDirective<'a> {
/// Extract a [`TodoDirective`] from a comment.
pub(crate) fn from_comment(comment: &'a str, comment_range: TextRange) -> Option<Self> {
// The directive's offset from the start of the comment.
let mut relative_offset = TextSize::new(0);
let mut subset_opt = Some(comment);
// Loop over `#`-delimited sections of the comment to check for directives. This will
// correctly handle cases like `# foo # TODO`.
while let Some(subset) = subset_opt {
let trimmed = subset.trim_start_matches('#').trim_start();
let offset = subset.text_len() - trimmed.text_len();
relative_offset += offset;
// If we detect a TodoDirectiveKind variant substring in the comment, construct and
// return the appropriate TodoDirective
if let Ok(directive_kind) = trimmed.parse::<TodoDirectiveKind>() {
let len = directive_kind.len();
return Some(Self {
content: &comment[TextRange::at(relative_offset, len)],
range: TextRange::at(comment_range.start() + relative_offset, len),
kind: directive_kind,
});
}
// Shrink the subset to check for the next phrase starting with "#".
subset_opt = if let Some(new_offset) = trimmed.find('#') {
relative_offset += TextSize::try_from(new_offset).unwrap();
subset.get(relative_offset.to_usize()..)
} else {
None
};
}
None
}
}
#[derive(Debug, PartialEq)]
pub(crate) enum TodoDirectiveKind {
Todo,
Fixme,
Xxx,
Hack,
}
impl FromStr for TodoDirectiveKind {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
// The lengths of the respective variant strings: TODO, FIXME, HACK, XXX
for length in [3, 4, 5] {
let Some(substr) = s.get(..length) else {
break;
};
match substr.to_lowercase().as_str() {
"fixme" => {
return Ok(TodoDirectiveKind::Fixme);
}
"hack" => {
return Ok(TodoDirectiveKind::Hack);
}
"todo" => {
return Ok(TodoDirectiveKind::Todo);
}
"xxx" => {
return Ok(TodoDirectiveKind::Xxx);
}
_ => continue,
}
}
Err(())
}
}
impl TodoDirectiveKind {
fn len(&self) -> TextSize {
match self {
TodoDirectiveKind::Xxx => TextSize::new(3),
TodoDirectiveKind::Hack | TodoDirectiveKind::Todo => TextSize::new(4),
TodoDirectiveKind::Fixme => TextSize::new(5),
}
}
}
#[cfg(test)]
mod tests {
use ruff_text_size::{TextLen, TextRange, TextSize};
@@ -354,9 +225,7 @@ mod tests {
use ruff_python_ast::source_code::{Indexer, Locator};
use crate::directives::{
extract_isort_directives, extract_noqa_line_for, TodoDirective, TodoDirectiveKind,
};
use crate::directives::{extract_isort_directives, extract_noqa_line_for};
use crate::noqa::NoqaMapping;
fn noqa_mappings(contents: &str) -> NoqaMapping {
@@ -559,62 +428,4 @@ z = x + 1";
vec![TextSize::from(13)]
);
}
#[test]
fn todo_directives() {
let test_comment = "# TODO: todo tag";
let test_comment_range = TextRange::at(TextSize::new(0), test_comment.text_len());
let expected = TodoDirective {
content: "TODO",
range: TextRange::new(TextSize::new(2), TextSize::new(6)),
kind: TodoDirectiveKind::Todo,
};
assert_eq!(
expected,
TodoDirective::from_comment(test_comment, test_comment_range).unwrap()
);
let test_comment = "#TODO: todo tag";
let test_comment_range = TextRange::at(TextSize::new(0), test_comment.text_len());
let expected = TodoDirective {
content: "TODO",
range: TextRange::new(TextSize::new(1), TextSize::new(5)),
kind: TodoDirectiveKind::Todo,
};
assert_eq!(
expected,
TodoDirective::from_comment(test_comment, test_comment_range).unwrap()
);
let test_comment = "# fixme: fixme tag";
let test_comment_range = TextRange::at(TextSize::new(0), test_comment.text_len());
let expected = TodoDirective {
content: "fixme",
range: TextRange::new(TextSize::new(2), TextSize::new(7)),
kind: TodoDirectiveKind::Fixme,
};
assert_eq!(
expected,
TodoDirective::from_comment(test_comment, test_comment_range).unwrap()
);
let test_comment = "# noqa # TODO: todo";
let test_comment_range = TextRange::at(TextSize::new(0), test_comment.text_len());
let expected = TodoDirective {
content: "TODO",
range: TextRange::new(TextSize::new(9), TextSize::new(13)),
kind: TodoDirectiveKind::Todo,
};
assert_eq!(
expected,
TodoDirective::from_comment(test_comment, test_comment_range).unwrap()
);
let test_comment = "# no directive";
let test_comment_range = TextRange::at(TextSize::new(0), test_comment.text_len());
assert_eq!(
None,
TodoDirective::from_comment(test_comment, test_comment_range)
);
}
}

View File

@@ -8,7 +8,7 @@ use rustpython_parser::ast::{self, Constant, Expr, Ranged, Stmt, Suite};
use rustpython_parser::lexer::LexResult;
use rustpython_parser::Tok;
use ruff_newlines::UniversalNewlineIterator;
use ruff_python_ast::newlines::UniversalNewlineIterator;
use ruff_python_ast::source_code::Locator;
use ruff_python_ast::statement_visitor::{walk_stmt, StatementVisitor};

View File

@@ -4,7 +4,7 @@ use std::iter::FusedIterator;
use ruff_text_size::{TextLen, TextRange, TextSize};
use strum_macros::EnumIter;
use ruff_newlines::{StrExt, UniversalNewlineIterator};
use ruff_python_ast::newlines::{StrExt, UniversalNewlineIterator};
use ruff_python_ast::whitespace;
use crate::docstrings::styles::SectionStyle;

View File

@@ -10,7 +10,7 @@ use crate::rules::flake8_pytest_style::types::{
ParametrizeNameType, ParametrizeValuesRowType, ParametrizeValuesType,
};
use crate::rules::flake8_quotes::settings::Quote;
use crate::rules::flake8_tidy_imports::settings::Strictness;
use crate::rules::flake8_tidy_imports::relative_imports::Strictness;
use crate::rules::pydocstyle::settings::Convention;
use crate::rules::{
flake8_annotations, flake8_bugbear, flake8_builtins, flake8_errmsg, flake8_pytest_style,

View File

@@ -1,86 +1,24 @@
//! Insert statements into Python code.
use std::ops::Add;
use ruff_text_size::TextSize;
use rustpython_parser::ast::{Ranged, Stmt};
use rustpython_parser::{lexer, Mode, Tok};
use ruff_diagnostics::Edit;
use ruff_newlines::UniversalNewlineIterator;
use ruff_python_ast::helpers::is_docstring_stmt;
use ruff_python_ast::source_code::{Locator, Stylist};
use ruff_textwrap::indent;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum Placement<'a> {
/// The content will be inserted inline with the existing code (i.e., within semicolon-delimited
/// statements).
Inline,
/// The content will be inserted on its own line.
OwnLine,
/// The content will be inserted as an indented block.
Indented(&'a str),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct Insertion<'a> {
pub(super) struct Insertion {
/// The content to add before the insertion.
prefix: &'a str,
prefix: &'static str,
/// The location at which to insert.
location: TextSize,
/// The content to add after the insertion.
suffix: &'a str,
/// The line placement of insertion.
placement: Placement<'a>,
suffix: &'static str,
}
impl<'a> Insertion<'a> {
/// Create an [`Insertion`] to insert (e.g.) an import statement at the start of a given
/// file, along with a prefix and suffix to use for the insertion.
///
/// For example, given the following code:
///
/// ```python
/// """Hello, world!"""
///
/// import os
/// ```
///
/// The insertion returned will begin at the start of the `import os` statement, and will
/// include a trailing newline.
pub(super) fn start_of_file(
body: &[Stmt],
locator: &Locator,
stylist: &Stylist,
) -> Insertion<'static> {
// Skip over any docstrings.
let mut location = if let Some(location) = match_docstring_end(body) {
// If the first token after the docstring is a semicolon, insert after the semicolon as
// an inline statement.
if let Some(offset) = match_leading_semicolon(locator.after(location)) {
return Insertion::inline(" ", location.add(offset).add(TextSize::of(';')), ";");
}
// Otherwise, advance to the next row.
locator.full_line_end(location)
} else {
TextSize::default()
};
// Skip over commented lines.
for line in UniversalNewlineIterator::with_offset(locator.after(location), location) {
if line.trim_start().starts_with('#') {
location = line.full_end();
} else {
break;
}
}
Insertion::own_line("", location, stylist.line_ending().as_str())
}
/// Create an [`Insertion`] to insert (e.g.) an import after the end of the given
/// [`Stmt`], along with a prefix and suffix to use for the insertion.
impl Insertion {
/// Create an [`Insertion`] to insert (e.g.) an import after the end of the given [`Stmt`],
/// along with a prefix and suffix to use for the insertion.
///
/// For example, given the following code:
///
@@ -96,22 +34,18 @@ impl<'a> Insertion<'a> {
/// ```
///
/// The insertion returned will begin after the newline after the last import statement, which
/// in this case is the line after `import math`, and will include a trailing newline.
///
/// The statement itself is assumed to be at the top-level of the module.
pub(super) fn end_of_statement(
stmt: &Stmt,
locator: &Locator,
stylist: &Stylist,
) -> Insertion<'static> {
/// in this case is the line after `import math`, and will include a trailing newline suffix.
pub(super) fn end_of_statement(stmt: &Stmt, locator: &Locator, stylist: &Stylist) -> Insertion {
let location = stmt.end();
if let Some(offset) = match_leading_semicolon(locator.after(location)) {
// If the first token after the statement is a semicolon, insert after the semicolon as
// an inline statement.
Insertion::inline(" ", location.add(offset).add(TextSize::of(';')), ";")
let mut tokens =
lexer::lex_starts_at(locator.after(location), Mode::Module, location).flatten();
if let Some((Tok::Semi, range)) = tokens.next() {
// If the first token after the docstring is a semicolon, insert after the semicolon as an
// inline statement;
Insertion::new(" ", range.end(), ";")
} else {
// Otherwise, insert on the next line.
Insertion::own_line(
Insertion::new(
"",
locator.full_line_end(location),
stylist.line_ending().as_str(),
@@ -119,89 +53,57 @@ impl<'a> Insertion<'a> {
}
}
/// Create an [`Insertion`] to insert (e.g.) an import statement at the start of a given
/// block, along with a prefix and suffix to use for the insertion.
/// Create an [`Insertion`] to insert (e.g.) an import statement at the "top" of a given file,
/// along with a prefix and suffix to use for the insertion.
///
/// For example, given the following code:
///
/// ```python
/// if TYPE_CHECKING:
/// import os
/// """Hello, world!"""
///
/// import os
/// ```
///
/// The insertion returned will begin at the start of the `import os` statement, and will
/// include a trailing newline.
///
/// The block itself is assumed to be at the top-level of the module.
pub(super) fn start_of_block(
mut location: TextSize,
locator: &Locator<'a>,
stylist: &Stylist,
) -> Insertion<'a> {
enum Awaiting {
Colon(u32),
Newline,
Indent,
}
/// include a trailing newline suffix.
pub(super) fn top_of_file(body: &[Stmt], locator: &Locator, stylist: &Stylist) -> Insertion {
// Skip over any docstrings.
let mut location = if let Some(location) = match_docstring_end(body) {
// If the first token after the docstring is a semicolon, insert after the semicolon as an
// inline statement;
let first_token = lexer::lex_starts_at(locator.after(location), Mode::Module, location)
.flatten()
.next();
if let Some((Tok::Semi, range)) = first_token {
return Insertion::new(" ", range.end(), ";");
}
let mut state = Awaiting::Colon(0);
// Otherwise, advance to the next row.
locator.full_line_end(location)
} else {
TextSize::default()
};
// Skip over any comments and empty lines.
for (tok, range) in
lexer::lex_starts_at(locator.after(location), Mode::Module, location).flatten()
{
match state {
// Iterate until we find the colon indicating the start of the block body.
Awaiting::Colon(depth) => match tok {
Tok::Colon if depth == 0 => {
state = Awaiting::Newline;
}
Tok::Lpar | Tok::Lbrace | Tok::Lsqb => {
state = Awaiting::Colon(depth.saturating_add(1));
}
Tok::Rpar | Tok::Rbrace | Tok::Rsqb => {
state = Awaiting::Colon(depth.saturating_sub(1));
}
_ => {}
},
// Once we've seen the colon, we're looking for a newline; otherwise, there's no
// block body (e.g. `if True: pass`).
Awaiting::Newline => match tok {
Tok::Comment(..) => {}
Tok::Newline => {
state = Awaiting::Indent;
}
_ => {
location = range.start();
break;
}
},
// Once we've seen the newline, we're looking for the indentation of the block body.
Awaiting::Indent => match tok {
Tok::Comment(..) => {}
Tok::NonLogicalNewline => {}
Tok::Indent => {
// This is like:
// ```py
// if True:
// pass
// ```
// Where `range` is the indentation before the `pass` token.
return Insertion::indented(
"",
range.start(),
stylist.line_ending().as_str(),
locator.slice(range),
);
}
_ => {
location = range.start();
break;
}
},
if matches!(tok, Tok::Comment(..) | Tok::Newline) {
location = locator.full_line_end(range.end());
} else {
break;
}
}
// This is like: `if True: pass`, where `location` is the start of the `pass` token.
Insertion::inline("", location, "; ")
Insertion::new("", location, stylist.line_ending().as_str())
}
fn new(prefix: &'static str, location: TextSize, suffix: &'static str) -> Self {
Self {
prefix,
location,
suffix,
}
}
/// Convert this [`Insertion`] into an [`Edit`] that inserts the given content.
@@ -210,59 +112,8 @@ impl<'a> Insertion<'a> {
prefix,
location,
suffix,
placement,
} = self;
let content = format!("{prefix}{content}{suffix}");
Edit::insertion(
match placement {
Placement::Indented(indentation) if !indentation.is_empty() => {
indent(&content, indentation).to_string()
}
_ => content,
},
location,
)
}
/// Returns `true` if this [`Insertion`] is inline.
pub(super) fn is_inline(&self) -> bool {
matches!(self.placement, Placement::Inline)
}
/// Create an [`Insertion`] that inserts content inline (i.e., within semicolon-delimited
/// statements).
fn inline(prefix: &'a str, location: TextSize, suffix: &'a str) -> Self {
Self {
prefix,
location,
suffix,
placement: Placement::Inline,
}
}
/// Create an [`Insertion`] that starts on its own line.
fn own_line(prefix: &'a str, location: TextSize, suffix: &'a str) -> Self {
Self {
prefix,
location,
suffix,
placement: Placement::OwnLine,
}
}
/// Create an [`Insertion`] that starts on its own line, with the given indentation.
fn indented(
prefix: &'a str,
location: TextSize,
suffix: &'a str,
indentation: &'a str,
) -> Self {
Self {
prefix,
location,
suffix,
placement: Placement::Indented(indentation),
}
Edit::insertion(format!("{prefix}{content}{suffix}"), location)
}
}
@@ -284,45 +135,32 @@ fn match_docstring_end(body: &[Stmt]) -> Option<TextSize> {
Some(stmt.end())
}
/// If a line starts with a semicolon, return its offset.
fn match_leading_semicolon(s: &str) -> Option<TextSize> {
for (offset, c) in s.char_indices() {
match c {
' ' | '\t' => continue,
';' => return Some(TextSize::try_from(offset).unwrap()),
_ => break,
}
}
None
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use ruff_text_size::TextSize;
use rustpython_parser::ast::Suite;
use rustpython_parser as parser;
use rustpython_parser::lexer::LexResult;
use rustpython_parser::Parse;
use ruff_newlines::LineEnding;
use ruff_python_ast::newlines::LineEnding;
use ruff_python_ast::source_code::{Locator, Stylist};
use super::Insertion;
#[test]
fn start_of_file() -> Result<()> {
fn insert(contents: &str) -> Result<Insertion> {
let program = Suite::parse(contents, "<filename>")?;
let tokens: Vec<LexResult> = ruff_rustpython::tokenize(contents);
let locator = Locator::new(contents);
let stylist = Stylist::from_tokens(&tokens, &locator);
Ok(Insertion::start_of_file(&program, &locator, &stylist))
}
fn insert(contents: &str) -> Result<Insertion> {
let program = parser::parse_program(contents, "<filename>")?;
let tokens: Vec<LexResult> = ruff_rustpython::tokenize(contents);
let locator = Locator::new(contents);
let stylist = Stylist::from_tokens(&tokens, &locator);
Ok(Insertion::top_of_file(&program, &locator, &stylist))
}
#[test]
fn top_of_file() -> Result<()> {
let contents = "";
assert_eq!(
insert(contents)?,
Insertion::own_line("", TextSize::from(0), LineEnding::default().as_str())
Insertion::new("", TextSize::from(0), LineEnding::default().as_str())
);
let contents = r#"
@@ -330,7 +168,7 @@ mod tests {
.trim_start();
assert_eq!(
insert(contents)?,
Insertion::own_line("", TextSize::from(19), LineEnding::default().as_str())
Insertion::new("", TextSize::from(19), LineEnding::default().as_str())
);
let contents = r#"
@@ -339,7 +177,7 @@ mod tests {
.trim_start();
assert_eq!(
insert(contents)?,
Insertion::own_line("", TextSize::from(20), "\n")
Insertion::new("", TextSize::from(20), "\n")
);
let contents = r#"
@@ -349,7 +187,7 @@ mod tests {
.trim_start();
assert_eq!(
insert(contents)?,
Insertion::own_line("", TextSize::from(40), "\n")
Insertion::new("", TextSize::from(40), "\n")
);
let contents = r#"
@@ -358,7 +196,7 @@ x = 1
.trim_start();
assert_eq!(
insert(contents)?,
Insertion::own_line("", TextSize::from(0), "\n")
Insertion::new("", TextSize::from(0), "\n")
);
let contents = r#"
@@ -367,7 +205,7 @@ x = 1
.trim_start();
assert_eq!(
insert(contents)?,
Insertion::own_line("", TextSize::from(23), "\n")
Insertion::new("", TextSize::from(23), "\n")
);
let contents = r#"
@@ -377,7 +215,7 @@ x = 1
.trim_start();
assert_eq!(
insert(contents)?,
Insertion::own_line("", TextSize::from(43), "\n")
Insertion::new("", TextSize::from(43), "\n")
);
let contents = r#"
@@ -387,7 +225,7 @@ x = 1
.trim_start();
assert_eq!(
insert(contents)?,
Insertion::own_line("", TextSize::from(43), "\n")
Insertion::new("", TextSize::from(43), "\n")
);
let contents = r#"
@@ -396,7 +234,7 @@ x = 1
.trim_start();
assert_eq!(
insert(contents)?,
Insertion::own_line("", TextSize::from(0), "\n")
Insertion::new("", TextSize::from(0), "\n")
);
let contents = r#"
@@ -405,7 +243,7 @@ x = 1
.trim_start();
assert_eq!(
insert(contents)?,
Insertion::inline(" ", TextSize::from(20), ";")
Insertion::new(" ", TextSize::from(20), ";")
);
let contents = r#"
@@ -415,35 +253,9 @@ x = 1
.trim_start();
assert_eq!(
insert(contents)?,
Insertion::inline(" ", TextSize::from(20), ";")
Insertion::new(" ", TextSize::from(20), ";")
);
Ok(())
}
#[test]
fn start_of_block() {
fn insert(contents: &str, offset: TextSize) -> Insertion {
let tokens: Vec<LexResult> = ruff_rustpython::tokenize(contents);
let locator = Locator::new(contents);
let stylist = Stylist::from_tokens(&tokens, &locator);
Insertion::start_of_block(offset, &locator, &stylist)
}
let contents = "if True: pass";
assert_eq!(
insert(contents, TextSize::from(0)),
Insertion::inline("", TextSize::from(9), "; ")
);
let contents = r#"
if True:
pass
"#
.trim_start();
assert_eq!(
insert(contents, TextSize::from(0)),
Insertion::indented("", TextSize::from(9), "\n", " ")
);
}
}

View File

@@ -1,19 +1,14 @@
//! Add and modify import statements to make module members available during fix execution.
use std::error::Error;
use anyhow::Result;
use libcst_native::{ImportAlias, Name, NameOrAttribute};
use anyhow::{bail, Result};
use libcst_native::{Codegen, CodegenState, ImportAlias, Name, NameOrAttribute};
use ruff_text_size::TextSize;
use rustpython_parser::ast::{self, Ranged, Stmt, Suite};
use crate::autofix;
use crate::autofix::codemods::CodegenStylist;
use ruff_diagnostics::Edit;
use ruff_python_ast::imports::{AnyImport, Import, ImportFrom};
use ruff_python_ast::imports::{AnyImport, Import};
use ruff_python_ast::source_code::{Locator, Stylist};
use ruff_python_semantic::model::SemanticModel;
use ruff_textwrap::indent;
use crate::cst::matchers::{match_aliases, match_import_from, match_statement};
use crate::importer::insertion::Insertion;
@@ -21,16 +16,10 @@ use crate::importer::insertion::Insertion;
mod insertion;
pub(crate) struct Importer<'a> {
/// The Python AST to which we are adding imports.
python_ast: &'a Suite,
/// The [`Locator`] for the Python AST.
locator: &'a Locator<'a>,
/// The [`Stylist`] for the Python AST.
stylist: &'a Stylist<'a>,
/// The list of visited, top-level runtime imports in the Python AST.
runtime_imports: Vec<&'a Stmt>,
/// The list of visited, top-level `if TYPE_CHECKING:` blocks in the Python AST.
type_checking_blocks: Vec<&'a Stmt>,
ordered_imports: Vec<&'a Stmt>,
}
impl<'a> Importer<'a> {
@@ -43,19 +32,13 @@ impl<'a> Importer<'a> {
python_ast,
locator,
stylist,
runtime_imports: Vec::default(),
type_checking_blocks: Vec::default(),
ordered_imports: Vec::default(),
}
}
/// Visit a top-level import statement.
pub(crate) fn visit_import(&mut self, import: &'a Stmt) {
self.runtime_imports.push(import);
}
/// Visit a top-level type-checking block.
pub(crate) fn visit_type_checking_block(&mut self, type_checking_block: &'a Stmt) {
self.type_checking_blocks.push(type_checking_block);
self.ordered_imports.push(import);
}
/// Add an import statement to import the given module.
@@ -70,134 +53,52 @@ impl<'a> Importer<'a> {
Insertion::end_of_statement(stmt, self.locator, self.stylist)
.into_edit(&required_import)
} else {
// Insert at the start of the file.
Insertion::start_of_file(self.python_ast, self.locator, self.stylist)
// Insert at the top of the file.
Insertion::top_of_file(self.python_ast, self.locator, self.stylist)
.into_edit(&required_import)
}
}
/// Move an existing import to the top-level, thereby making it available at runtime.
///
/// If there are no existing imports, the new import will be added at the top
/// of the file. Otherwise, it will be added after the most recent top-level
/// import statement.
pub(crate) fn runtime_import_edit(
&self,
import: &StmtImport,
at: TextSize,
) -> Result<RuntimeImportEdit> {
// Generate the modified import statement.
let content = autofix::codemods::retain_imports(
&[import.qualified_name],
import.stmt,
self.locator,
self.stylist,
)?;
// Add the import to the top-level.
let insertion = if let Some(stmt) = self.preceding_import(at) {
// Insert after the last top-level import.
Insertion::end_of_statement(stmt, self.locator, self.stylist)
} else {
// Insert at the start of the file.
Insertion::start_of_file(self.python_ast, self.locator, self.stylist)
};
let add_import_edit = insertion.into_edit(&content);
Ok(RuntimeImportEdit { add_import_edit })
}
/// Move an existing import into a `TYPE_CHECKING` block.
///
/// If there are no existing `TYPE_CHECKING` blocks, a new one will be added at the top
/// of the file. Otherwise, it will be added after the most recent top-level
/// `TYPE_CHECKING` block.
pub(crate) fn typing_import_edit(
&self,
import: &StmtImport,
at: TextSize,
semantic_model: &SemanticModel,
) -> Result<TypingImportEdit> {
// Generate the modified import statement.
let content = autofix::codemods::retain_imports(
&[import.qualified_name],
import.stmt,
self.locator,
self.stylist,
)?;
// Import the `TYPE_CHECKING` symbol from the typing module.
let (type_checking_edit, type_checking) = self.get_or_import_symbol(
&ImportRequest::import_from("typing", "TYPE_CHECKING"),
at,
semantic_model,
)?;
// Add the import to a `TYPE_CHECKING` block.
let add_import_edit = if let Some(block) = self.preceding_type_checking_block(at) {
// Add the import to the `TYPE_CHECKING` block.
self.add_to_type_checking_block(&content, block.start())
} else {
// Add the import to a new `TYPE_CHECKING` block.
self.add_type_checking_block(
&format!(
"{}if {type_checking}:{}{}",
self.stylist.line_ending().as_str(),
self.stylist.line_ending().as_str(),
indent(&content, self.stylist.indentation())
),
at,
)?
};
Ok(TypingImportEdit {
type_checking_edit,
add_import_edit,
})
}
/// Generate an [`Edit`] to reference the given symbol. Returns the [`Edit`] necessary to make
/// the symbol available in the current scope along with the bound name of the symbol.
///
/// Attempts to reuse existing imports when possible.
pub(crate) fn get_or_import_symbol(
&self,
symbol: &ImportRequest,
module: &str,
member: &str,
at: TextSize,
semantic_model: &SemanticModel,
) -> Result<(Edit, String), ResolutionError> {
match self.get_symbol(symbol, at, semantic_model) {
Some(result) => result,
None => self.import_symbol(symbol, at, semantic_model),
}
) -> Result<(Edit, String)> {
self.get_symbol(module, member, at, semantic_model)?
.map_or_else(
|| self.import_symbol(module, member, at, semantic_model),
Ok,
)
}
/// Return an [`Edit`] to reference an existing symbol, if it's present in the given [`SemanticModel`].
fn get_symbol(
&self,
symbol: &ImportRequest,
module: &str,
member: &str,
at: TextSize,
semantic_model: &SemanticModel,
) -> Option<Result<(Edit, String), ResolutionError>> {
) -> Result<Option<(Edit, String)>> {
// If the symbol is already available in the current scope, use it.
let imported_name =
semantic_model.resolve_qualified_import_name(symbol.module, symbol.member)?;
let Some((source, binding)) = semantic_model.resolve_qualified_import_name(module, member) else {
return Ok(None);
};
// If the symbol source (i.e., the import statement) comes after the current location,
// abort. For example, we could be generating an edit within a function, and the import
// The exception: the symbol source (i.e., the import statement) comes after the current
// location. For example, we could be generating an edit within a function, and the import
// could be defined in the module scope, but after the function definition. In this case,
// it's unclear whether we can use the symbol (the function could be called between the
// import and the current location, and thus the symbol would not be available). It's also
// unclear whether should add an import statement at the start of the file, since it could
// unclear whether should add an import statement at the top of the file, since it could
// be shadowed between the import and the current location.
if imported_name.range().start() > at {
return Some(Err(ResolutionError::ImportAfterUsage));
}
// If the symbol source (i.e., the import statement) is in a typing-only context, but we're
// in a runtime context, abort.
if imported_name.context().is_typing() && semantic_model.execution_context().is_runtime() {
return Some(Err(ResolutionError::IncompatibleContext));
if source.start() > at {
bail!("Unable to use existing symbol `{binding}` due to late-import");
}
// We also add a no-op edit to force conflicts with any other fixes that might try to
@@ -217,81 +118,66 @@ impl<'a> Importer<'a> {
// By adding this no-op edit, we force the `unused-imports` fix to conflict with the
// `sys-exit-alias` fix, and thus will avoid applying both fixes in the same pass.
let import_edit = Edit::range_replacement(
self.locator.slice(imported_name.range()).to_string(),
imported_name.range(),
self.locator.slice(source.range()).to_string(),
source.range(),
);
Some(Ok((import_edit, imported_name.into_name())))
Ok(Some((import_edit, binding)))
}
/// Generate an [`Edit`] to reference the given symbol. Returns the [`Edit`] necessary to make
/// the symbol available in the current scope along with the bound name of the symbol.
///
/// For example, assuming `module` is `"functools"` and `member` is `"lru_cache"`, this function
/// could return an [`Edit`] to add `import functools` to the start of the file, alongside with
/// could return an [`Edit`] to add `import functools` to the top of the file, alongside with
/// the name on which the `lru_cache` symbol would be made available (`"functools.lru_cache"`).
fn import_symbol(
&self,
symbol: &ImportRequest,
module: &str,
member: &str,
at: TextSize,
semantic_model: &SemanticModel,
) -> Result<(Edit, String), ResolutionError> {
if let Some(stmt) = self.find_import_from(symbol.module, at) {
) -> Result<(Edit, String)> {
if let Some(stmt) = self.find_import_from(module, at) {
// Case 1: `from functools import lru_cache` is in scope, and we're trying to reference
// `functools.cache`; thus, we add `cache` to the import, and return `"cache"` as the
// bound name.
if semantic_model.is_unbound(symbol.member) {
let Ok(import_edit) = self.add_member(stmt, symbol.member) else {
return Err(ResolutionError::InvalidEdit);
};
Ok((import_edit, symbol.member.to_string()))
if semantic_model
.find_binding(member)
.map_or(true, |binding| binding.kind.is_builtin())
{
let import_edit = self.add_member(stmt, member)?;
Ok((import_edit, member.to_string()))
} else {
Err(ResolutionError::ConflictingName(symbol.member.to_string()))
bail!("Unable to insert `{member}` into scope due to name conflict")
}
} else {
match symbol.style {
ImportStyle::Import => {
// Case 2a: No `functools` import is in scope; thus, we add `import functools`,
// and return `"functools.cache"` as the bound name.
if semantic_model.is_unbound(symbol.module) {
let import_edit =
self.add_import(&AnyImport::Import(Import::module(symbol.module)), at);
Ok((
import_edit,
format!(
"{module}.{member}",
module = symbol.module,
member = symbol.member
),
))
} else {
Err(ResolutionError::ConflictingName(symbol.module.to_string()))
}
}
ImportStyle::ImportFrom => {
// Case 2b: No `functools` import is in scope; thus, we add
// `from functools import cache`, and return `"cache"` as the bound name.
if semantic_model.is_unbound(symbol.member) {
let import_edit = self.add_import(
&AnyImport::ImportFrom(ImportFrom::member(
symbol.module,
symbol.member,
)),
at,
);
Ok((import_edit, symbol.member.to_string()))
} else {
Err(ResolutionError::ConflictingName(symbol.member.to_string()))
}
}
// Case 2: No `functools` import is in scope; thus, we add `import functools`, and
// return `"functools.cache"` as the bound name.
if semantic_model
.find_binding(module)
.map_or(true, |binding| binding.kind.is_builtin())
{
let import_edit = self.add_import(&AnyImport::Import(Import::module(module)), at);
Ok((import_edit, format!("{module}.{member}")))
} else {
bail!("Unable to insert `{module}` into scope due to name conflict")
}
}
}
/// Return the import statement that precedes the given position, if any.
fn preceding_import(&self, at: TextSize) -> Option<&Stmt> {
self.ordered_imports
.partition_point(|stmt| stmt.start() < at)
.checked_sub(1)
.map(|idx| self.ordered_imports[idx])
}
/// Return the top-level [`Stmt`] that imports the given module using `Stmt::ImportFrom`
/// preceding the given position, if any.
fn find_import_from(&self, module: &str, at: TextSize) -> Option<&Stmt> {
let mut import_from = None;
for stmt in &self.runtime_imports {
for stmt in &self.ordered_imports {
if stmt.start() >= at {
break;
}
@@ -325,163 +211,12 @@ impl<'a> Importer<'a> {
asname: None,
comma: aliases.last().and_then(|alias| alias.comma.clone()),
});
Ok(Edit::range_replacement(
statement.codegen_stylist(self.stylist),
stmt.range(),
))
}
/// Add a `TYPE_CHECKING` block to the given module.
fn add_type_checking_block(&self, content: &str, at: TextSize) -> Result<Edit> {
let insertion = if let Some(stmt) = self.preceding_import(at) {
// Insert after the last top-level import.
Insertion::end_of_statement(stmt, self.locator, self.stylist)
} else {
// Insert at the start of the file.
Insertion::start_of_file(self.python_ast, self.locator, self.stylist)
let mut state = CodegenState {
default_newline: &self.stylist.line_ending(),
default_indent: self.stylist.indentation(),
..CodegenState::default()
};
if insertion.is_inline() {
Err(anyhow::anyhow!(
"Cannot insert `TYPE_CHECKING` block inline"
))
} else {
Ok(insertion.into_edit(content))
}
}
/// Add an import statement to an existing `TYPE_CHECKING` block.
fn add_to_type_checking_block(&self, content: &str, at: TextSize) -> Edit {
Insertion::start_of_block(at, self.locator, self.stylist).into_edit(content)
}
/// Return the import statement that precedes the given position, if any.
fn preceding_import(&self, at: TextSize) -> Option<&'a Stmt> {
self.runtime_imports
.partition_point(|stmt| stmt.start() < at)
.checked_sub(1)
.map(|idx| self.runtime_imports[idx])
}
/// Return the `TYPE_CHECKING` block that precedes the given position, if any.
fn preceding_type_checking_block(&self, at: TextSize) -> Option<&'a Stmt> {
let block = self.type_checking_blocks.first()?;
if block.start() <= at {
Some(block)
} else {
None
}
statement.codegen(&mut state);
Ok(Edit::range_replacement(state.to_string(), stmt.range()))
}
}
/// An edit to the top-level of a module, making it available at runtime.
#[derive(Debug)]
pub(crate) struct RuntimeImportEdit {
/// The edit to add the import to the top-level of the module.
add_import_edit: Edit,
}
impl RuntimeImportEdit {
pub(crate) fn into_edits(self) -> Vec<Edit> {
vec![self.add_import_edit]
}
}
/// An edit to an import to a typing-only context.
#[derive(Debug)]
pub(crate) struct TypingImportEdit {
/// The edit to add the `TYPE_CHECKING` symbol to the module.
type_checking_edit: Edit,
/// The edit to add the import to a `TYPE_CHECKING` block.
add_import_edit: Edit,
}
impl TypingImportEdit {
pub(crate) fn into_edits(self) -> Vec<Edit> {
vec![self.type_checking_edit, self.add_import_edit]
}
}
#[derive(Debug)]
enum ImportStyle {
/// Import the symbol using the `import` statement (e.g. `import foo; foo.bar`).
Import,
/// Import the symbol using the `from` statement (e.g. `from foo import bar; bar`).
ImportFrom,
}
#[derive(Debug)]
pub(crate) struct ImportRequest<'a> {
/// The module from which the symbol can be imported (e.g., `foo`, in `from foo import bar`).
module: &'a str,
/// The member to import (e.g., `bar`, in `from foo import bar`).
member: &'a str,
/// The preferred style to use when importing the symbol (e.g., `import foo` or
/// `from foo import bar`), if it's not already in scope.
style: ImportStyle,
}
impl<'a> ImportRequest<'a> {
/// Create a new `ImportRequest` from a module and member. If not present in the scope,
/// the symbol should be imported using the "import" statement.
pub(crate) fn import(module: &'a str, member: &'a str) -> Self {
Self {
module,
member,
style: ImportStyle::Import,
}
}
/// Create a new `ImportRequest` from a module and member. If not present in the scope,
/// the symbol should be imported using the "import from" statement.
pub(crate) fn import_from(module: &'a str, member: &'a str) -> Self {
Self {
module,
member,
style: ImportStyle::ImportFrom,
}
}
}
/// An existing module or member import, located within an import statement.
pub(crate) struct StmtImport<'a> {
/// The import statement.
pub(crate) stmt: &'a Stmt,
/// The "full name" of the imported module or member.
pub(crate) qualified_name: &'a str,
}
/// The result of an [`Importer::get_or_import_symbol`] call.
#[derive(Debug)]
pub(crate) enum ResolutionError {
/// The symbol is imported, but the import came after the current location.
ImportAfterUsage,
/// The symbol is imported, but in an incompatible context (e.g., in typing-only context, while
/// we're in a runtime context).
IncompatibleContext,
/// The symbol can't be imported, because another symbol is bound to the same name.
ConflictingName(String),
/// The symbol can't be imported due to an error in editing an existing import statement.
InvalidEdit,
}
impl std::fmt::Display for ResolutionError {
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ResolutionError::ImportAfterUsage => {
fmt.write_str("Unable to use existing symbol due to late binding")
}
ResolutionError::IncompatibleContext => {
fmt.write_str("Unable to use existing symbol due to incompatible context")
}
ResolutionError::ConflictingName(binding) => std::write!(
fmt,
"Unable to insert `{binding}` into scope due to name conflict"
),
ResolutionError::InvalidEdit => {
fmt.write_str("Unable to modify existing import statement")
}
}
}
}
impl Error for ResolutionError {}

View File

@@ -79,7 +79,7 @@ impl StateMachine {
}
Tok::Lpar | Tok::Lbrace | Tok::Lsqb => {
self.bracket_count = self.bracket_count.saturating_add(1);
self.bracket_count += 1;
if matches!(
self.state,
State::ExpectModuleDocstring
@@ -92,7 +92,7 @@ impl StateMachine {
}
Tok::Rpar | Tok::Rbrace | Tok::Rsqb => {
self.bracket_count = self.bracket_count.saturating_sub(1);
self.bracket_count -= 1;
if matches!(
self.state,
State::ExpectModuleDocstring

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