Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dec1d2e8fc | ||
|
|
3aa91a853e | ||
|
|
59570beb57 | ||
|
|
78e51e8601 | ||
|
|
0006df9292 | ||
|
|
14aa8bb871 | ||
|
|
2882840abc | ||
|
|
4f60f0e925 | ||
|
|
059e90a98f | ||
|
|
a4562ac673 | ||
|
|
021a70d30c | ||
|
|
fddf2f33d2 | ||
|
|
b4c42eb83b | ||
|
|
2a2cc37158 | ||
|
|
8a217e5920 | ||
|
|
f7973ac870 | ||
|
|
3560f86450 | ||
|
|
f22da352db | ||
|
|
cb530a0216 | ||
|
|
90026047f9 | ||
|
|
26f736bc46 | ||
|
|
c9f95e8714 | ||
|
|
3da8b51dc1 | ||
|
|
3cbf2fe82e | ||
|
|
221f3258d4 | ||
|
|
3fae3f248c | ||
|
|
6c73837bc4 | ||
|
|
6f8f38cb68 | ||
|
|
4e2d6f5e45 | ||
|
|
3ed3852c38 | ||
|
|
dca594f89f | ||
|
|
4bc27133a9 | ||
|
|
7154b64248 | ||
|
|
6d01c487a5 | ||
|
|
6660b11422 | ||
|
|
b5c5f710fc | ||
|
|
ee88abf77c | ||
|
|
78bd73f25a | ||
|
|
110765154f | ||
|
|
30ee44770d | ||
|
|
fd69533fe5 | ||
|
|
39c6364545 | ||
|
|
100d765ddf | ||
|
|
6ea231e458 | ||
|
|
c9df4ddf6a | ||
|
|
948463aafa | ||
|
|
729fa12575 | ||
|
|
f14ee9edd5 | ||
|
|
a67630f907 | ||
|
|
5bc81f26c8 | ||
|
|
6908e2682f | ||
|
|
25c4295564 | ||
|
|
426fa4bb12 | ||
|
|
b0b65c24ff | ||
|
|
08bc6d2589 | ||
|
|
f2ae12bab3 | ||
|
|
965f415212 | ||
|
|
83b5bbf004 | ||
|
|
87f6f08ef5 | ||
|
|
59114d0301 |
2
.github/mypy-primer-ty.toml
vendored
2
.github/mypy-primer-ty.toml
vendored
@@ -5,4 +5,4 @@
|
||||
[rules]
|
||||
possibly-unresolved-reference = "warn"
|
||||
unused-ignore-comment = "warn"
|
||||
division-by-zero = "warn"
|
||||
literal-math-error = "warn"
|
||||
|
||||
31
.github/workflows/ci.yaml
vendored
31
.github/workflows/ci.yaml
vendored
@@ -240,11 +240,11 @@ jobs:
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@85c79d00377f0d32cdbae595a46de6f7c2fa6599 # v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@f3a27926ea13d7be3ee2f4cbb925883cf9442b56 # v2.56.7
|
||||
uses: taiki-e/install-action@c07504cae06f832dc8de08911c9a9c5cddb0d2d3 # v2.56.13
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
uses: taiki-e/install-action@f3a27926ea13d7be3ee2f4cbb925883cf9442b56 # v2.56.7
|
||||
uses: taiki-e/install-action@c07504cae06f832dc8de08911c9a9c5cddb0d2d3 # v2.56.13
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- name: ty mdtests (GitHub annotations)
|
||||
@@ -298,11 +298,11 @@ jobs:
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@85c79d00377f0d32cdbae595a46de6f7c2fa6599 # v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@f3a27926ea13d7be3ee2f4cbb925883cf9442b56 # v2.56.7
|
||||
uses: taiki-e/install-action@c07504cae06f832dc8de08911c9a9c5cddb0d2d3 # v2.56.13
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
uses: taiki-e/install-action@f3a27926ea13d7be3ee2f4cbb925883cf9442b56 # v2.56.7
|
||||
uses: taiki-e/install-action@c07504cae06f832dc8de08911c9a9c5cddb0d2d3 # v2.56.13
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- name: "Run tests"
|
||||
@@ -325,7 +325,7 @@ jobs:
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@f3a27926ea13d7be3ee2f4cbb925883cf9442b56 # v2.56.7
|
||||
uses: taiki-e/install-action@c07504cae06f832dc8de08911c9a9c5cddb0d2d3 # v2.56.13
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Run tests"
|
||||
@@ -407,20 +407,11 @@ jobs:
|
||||
run: rustup default "${MSRV}"
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@85c79d00377f0d32cdbae595a46de6f7c2fa6599 # v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@f3a27926ea13d7be3ee2f4cbb925883cf9442b56 # v2.56.7
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
uses: taiki-e/install-action@f3a27926ea13d7be3ee2f4cbb925883cf9442b56 # v2.56.7
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- name: "Run tests"
|
||||
- name: "Build tests"
|
||||
shell: bash
|
||||
env:
|
||||
NEXTEST_PROFILE: "ci"
|
||||
MSRV: ${{ steps.msrv.outputs.value }}
|
||||
run: cargo "+${MSRV}" insta test --all-features --unreferenced reject --test-runner nextest
|
||||
run: cargo "+${MSRV}" test --no-run --all-features
|
||||
|
||||
cargo-fuzz-build:
|
||||
name: "cargo fuzz build"
|
||||
@@ -912,7 +903,7 @@ jobs:
|
||||
run: rustup show
|
||||
|
||||
- name: "Install codspeed"
|
||||
uses: taiki-e/install-action@f3a27926ea13d7be3ee2f4cbb925883cf9442b56 # v2.56.7
|
||||
uses: taiki-e/install-action@c07504cae06f832dc8de08911c9a9c5cddb0d2d3 # v2.56.13
|
||||
with:
|
||||
tool: cargo-codspeed
|
||||
|
||||
@@ -920,7 +911,7 @@ jobs:
|
||||
run: cargo codspeed build --features "codspeed,instrumented" --no-default-features -p ruff_benchmark
|
||||
|
||||
- name: "Run benchmarks"
|
||||
uses: CodSpeedHQ/action@0010eb0ca6e89b80c88e8edaaa07cfe5f3e6664d # v3.5.0
|
||||
uses: CodSpeedHQ/action@c28fe9fbe7d57a3da1b7834ae3761c1d8217612d # v3.7.0
|
||||
with:
|
||||
run: cargo codspeed run
|
||||
token: ${{ secrets.CODSPEED_TOKEN }}
|
||||
@@ -945,7 +936,7 @@ jobs:
|
||||
run: rustup show
|
||||
|
||||
- name: "Install codspeed"
|
||||
uses: taiki-e/install-action@f3a27926ea13d7be3ee2f4cbb925883cf9442b56 # v2.56.7
|
||||
uses: taiki-e/install-action@c07504cae06f832dc8de08911c9a9c5cddb0d2d3 # v2.56.13
|
||||
with:
|
||||
tool: cargo-codspeed
|
||||
|
||||
@@ -953,7 +944,7 @@ jobs:
|
||||
run: cargo codspeed build --features "codspeed,walltime" --no-default-features -p ruff_benchmark
|
||||
|
||||
- name: "Run benchmarks"
|
||||
uses: CodSpeedHQ/action@0010eb0ca6e89b80c88e8edaaa07cfe5f3e6664d # v3.5.0
|
||||
uses: CodSpeedHQ/action@c28fe9fbe7d57a3da1b7834ae3761c1d8217612d # v3.7.0
|
||||
with:
|
||||
run: cargo codspeed run
|
||||
token: ${{ secrets.CODSPEED_TOKEN }}
|
||||
|
||||
31
.github/workflows/sync_typeshed.yaml
vendored
31
.github/workflows/sync_typeshed.yaml
vendored
@@ -36,9 +36,40 @@ jobs:
|
||||
run: |
|
||||
git config --global user.name typeshedbot
|
||||
git config --global user.email '<>'
|
||||
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
|
||||
- name: Sync typeshed
|
||||
id: sync
|
||||
run: |
|
||||
docstring_adder="git+https://github.com/astral-sh/docstring-adder.git@6de51c5f44aea11fe8c8f2d30f9ee0683682c3d2"
|
||||
|
||||
# Run with the full matrix of Python versions supported by typeshed,
|
||||
# so that we codemod in docstrings that only exist on certain versions.
|
||||
#
|
||||
# The codemod will only add docstrings to functions/classes that do not
|
||||
# already have docstrings. We run with Python 3.14 before running with
|
||||
# any other Python version so that we get the Python 3.14 version of the
|
||||
# docstring for a definition that exists on all Python versions: if we
|
||||
# ran with Python 3.9 first, then the later runs with Python 3.10+ would
|
||||
# not modify the docstring that had already been added using the old version of Python.
|
||||
#
|
||||
# TODO: In order to add docstrings for platform-specific APIs, we would also
|
||||
# need to run the codemod on Windows. We get the runtime docstrings by inspecting
|
||||
# the docstrings at runtime, so if an API doesn't exist at runtime (because e.g.
|
||||
# it's Windows-specific and we're running on Linux), then we won't add a docstring to it.
|
||||
#
|
||||
uvx --python=3.14 --force-reinstall --from="${docstring_adder}" add-docstrings --stdlib-path ./typeshed/stdlib
|
||||
uvx --python=3.13 --force-reinstall --from="${docstring_adder}" add-docstrings --stdlib-path ./typeshed/stdlib
|
||||
uvx --python=3.12 --force-reinstall --from="${docstring_adder}" add-docstrings --stdlib-path ./typeshed/stdlib
|
||||
uvx --python=3.11 --force-reinstall --from="${docstring_adder}" add-docstrings --stdlib-path ./typeshed/stdlib
|
||||
uvx --python=3.10 --force-reinstall --from="${docstring_adder}" add-docstrings --stdlib-path ./typeshed/stdlib
|
||||
uvx --python=3.9 --force-reinstall --from="${docstring_adder}" add-docstrings --stdlib-path ./typeshed/stdlib
|
||||
|
||||
# Here we just reformat the codemodded stubs so that they are
|
||||
# consistent with the other typeshed stubs around them.
|
||||
# Typeshed formats code using black in their CI, so we just invoke
|
||||
# black on the stubs the same way that typeshed does.
|
||||
uvx --directory=typeshed pre-commit run -a black || true
|
||||
|
||||
rm -rf ruff/crates/ty_vendored/vendor/typeshed
|
||||
mkdir ruff/crates/ty_vendored/vendor/typeshed
|
||||
cp typeshed/README.md ruff/crates/ty_vendored/vendor/typeshed
|
||||
|
||||
@@ -81,7 +81,7 @@ repos:
|
||||
pass_filenames: false # This makes it a lot faster
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.12.2
|
||||
rev: v0.12.3
|
||||
hooks:
|
||||
- id: ruff-format
|
||||
- id: ruff
|
||||
|
||||
28
CHANGELOG.md
28
CHANGELOG.md
@@ -1,5 +1,33 @@
|
||||
# Changelog
|
||||
|
||||
## 0.12.3
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`flake8-bugbear`\] Support non-context-manager calls in `B017` ([#19063](https://github.com/astral-sh/ruff/pull/19063))
|
||||
- \[`flake8-use-pathlib`\] Add autofixes for `PTH100`, `PTH106`, `PTH107`, `PTH108`, `PTH110`, `PTH111`, `PTH112`, `PTH113`, `PTH114`, `PTH115`, `PTH117`, `PTH119`, `PTH120` ([#19213](https://github.com/astral-sh/ruff/pull/19213))
|
||||
- \[`flake8-use-pathlib`\] Add autofixes for `PTH203`, `PTH204`, `PTH205` ([#18922](https://github.com/astral-sh/ruff/pull/18922))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- \[`flake8-return`\] Fix false-positive for variables used inside nested functions in `RET504` ([#18433](https://github.com/astral-sh/ruff/pull/18433))
|
||||
- Treat form feed as valid whitespace before a line continuation ([#19220](https://github.com/astral-sh/ruff/pull/19220))
|
||||
- \[`flake8-type-checking`\] Fix syntax error introduced by fix (`TC008`) ([#19150](https://github.com/astral-sh/ruff/pull/19150))
|
||||
- \[`pyupgrade`\] Keyword arguments in `super` should suppress the `UP008` fix ([#19131](https://github.com/astral-sh/ruff/pull/19131))
|
||||
|
||||
### Documentation
|
||||
|
||||
- \[`flake8-pyi`\] Make example error out-of-the-box (`PYI007`, `PYI008`) ([#19103](https://github.com/astral-sh/ruff/pull/19103))
|
||||
- \[`flake8-simplify`\] Make example error out-of-the-box (`SIM116`) ([#19111](https://github.com/astral-sh/ruff/pull/19111))
|
||||
- \[`flake8-type-checking`\] Make example error out-of-the-box (`TC001`) ([#19151](https://github.com/astral-sh/ruff/pull/19151))
|
||||
- \[`flake8-use-pathlib`\] Make example error out-of-the-box (`PTH210`) ([#19189](https://github.com/astral-sh/ruff/pull/19189))
|
||||
- \[`pycodestyle`\] Make example error out-of-the-box (`E272`) ([#19191](https://github.com/astral-sh/ruff/pull/19191))
|
||||
- \[`pycodestyle`\] Make example not raise unnecessary `SyntaxError` (`E114`) ([#19190](https://github.com/astral-sh/ruff/pull/19190))
|
||||
- \[`pydoclint`\] Make example error out-of-the-box (`DOC501`) ([#19218](https://github.com/astral-sh/ruff/pull/19218))
|
||||
- \[`pylint`, `pyupgrade`\] Fix syntax errors in examples (`PLW1501`, `UP028`) ([#19127](https://github.com/astral-sh/ruff/pull/19127))
|
||||
- \[`pylint`\] Update `missing-maxsplit-arg` docs and error to suggest proper usage (`PLC0207`) ([#18949](https://github.com/astral-sh/ruff/pull/18949))
|
||||
- \[`flake8-bandit`\] Make example error out-of-the-box (`S412`) ([#19241](https://github.com/astral-sh/ruff/pull/19241))
|
||||
|
||||
## 0.12.2
|
||||
|
||||
### Preview features
|
||||
|
||||
127
Cargo.lock
generated
127
Cargo.lock
generated
@@ -396,9 +396,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.40"
|
||||
version = "4.5.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f"
|
||||
checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -406,9 +406,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.40"
|
||||
version = "4.5.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e"
|
||||
checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -449,9 +449,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.40"
|
||||
version = "4.5.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce"
|
||||
checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
@@ -591,7 +591,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -600,7 +600,7 @@ version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e"
|
||||
dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -933,7 +933,7 @@ dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1013,7 +1013,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1133,9 +1133,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "get-size-derive2"
|
||||
version = "0.5.1"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1aac2af9f9a6a50e31b1e541d05b7925add83d3982c2793193fe9d4ee584323c"
|
||||
checksum = "028f3cfad7c3e3b1d8d04ef0a1c03576f2d62800803fe1301a4cd262849f2dea"
|
||||
dependencies = [
|
||||
"attribute-derive",
|
||||
"quote",
|
||||
@@ -1144,9 +1144,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "get-size2"
|
||||
version = "0.5.1"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "624a0312efd19e1c45922dfcc2d6806d3ffc4bca261f89f31fcc4f63f438d885"
|
||||
checksum = "3a09c2043819a3def7bfbb4927e7df96aab0da4cfd8824484b22d0c94e84458e"
|
||||
dependencies = [
|
||||
"compact_str",
|
||||
"get-size-derive2",
|
||||
@@ -1586,7 +1586,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
|
||||
dependencies = [
|
||||
"hermit-abi 0.5.1",
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1650,7 +1650,7 @@ dependencies = [
|
||||
"portable-atomic",
|
||||
"portable-atomic-util",
|
||||
"serde",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2465,7 +2465,7 @@ dependencies = [
|
||||
"pep508_rs",
|
||||
"serde",
|
||||
"thiserror 2.0.12",
|
||||
"toml",
|
||||
"toml 0.8.23",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2711,7 +2711,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.12.2"
|
||||
version = "0.12.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -2763,7 +2763,7 @@ dependencies = [
|
||||
"test-case",
|
||||
"thiserror 2.0.12",
|
||||
"tikv-jemallocator",
|
||||
"toml",
|
||||
"toml 0.9.2",
|
||||
"tracing",
|
||||
"walkdir",
|
||||
"wild",
|
||||
@@ -2779,7 +2779,7 @@ dependencies = [
|
||||
"ruff_annotate_snippets",
|
||||
"serde",
|
||||
"snapbox",
|
||||
"toml",
|
||||
"toml 0.9.2",
|
||||
"tryfn",
|
||||
"unicode-width 0.2.1",
|
||||
]
|
||||
@@ -2852,6 +2852,7 @@ dependencies = [
|
||||
"salsa",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"thiserror 2.0.12",
|
||||
"tracing",
|
||||
@@ -2894,7 +2895,7 @@ dependencies = [
|
||||
"similar",
|
||||
"strum",
|
||||
"tempfile",
|
||||
"toml",
|
||||
"toml 0.9.2",
|
||||
"tracing",
|
||||
"tracing-indicatif",
|
||||
"tracing-subscriber",
|
||||
@@ -2961,7 +2962,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.12.2"
|
||||
version = "0.12.3"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
@@ -3015,7 +3016,7 @@ dependencies = [
|
||||
"tempfile",
|
||||
"test-case",
|
||||
"thiserror 2.0.12",
|
||||
"toml",
|
||||
"toml 0.9.2",
|
||||
"typed-arena",
|
||||
"unicode-normalization",
|
||||
"unicode-width 0.2.1",
|
||||
@@ -3265,7 +3266,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"shellexpand",
|
||||
"thiserror 2.0.12",
|
||||
"toml",
|
||||
"toml 0.9.2",
|
||||
"tracing",
|
||||
"tracing-log",
|
||||
"tracing-subscriber",
|
||||
@@ -3294,7 +3295,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_wasm"
|
||||
version = "0.12.2"
|
||||
version = "0.12.3"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
@@ -3355,7 +3356,7 @@ dependencies = [
|
||||
"shellexpand",
|
||||
"strum",
|
||||
"tempfile",
|
||||
"toml",
|
||||
"toml 0.9.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3390,7 +3391,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3575,6 +3576,15 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_test"
|
||||
version = "1.0.177"
|
||||
@@ -3780,7 +3790,7 @@ dependencies = [
|
||||
"getrandom 0.3.3",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3980,11 +3990,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"serde_spanned 0.6.9",
|
||||
"toml_datetime 0.6.11",
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed0aee96c12fa71097902e0bb061a5e1ebd766a6636bb605ba401c45c1650eac"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_spanned 1.0.0",
|
||||
"toml_datetime 0.7.0",
|
||||
"toml_parser",
|
||||
"toml_writer",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.11"
|
||||
@@ -3994,6 +4019,15 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.27"
|
||||
@@ -4002,17 +4036,25 @@ checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_write",
|
||||
"serde_spanned 0.6.9",
|
||||
"toml_datetime 0.6.11",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_write"
|
||||
version = "0.1.2"
|
||||
name = "toml_parser"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||
checksum = "97200572db069e74c512a14117b296ba0a80a30123fbbb5aa1f4a348f639ca30"
|
||||
dependencies = [
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_writer"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64"
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
@@ -4135,7 +4177,7 @@ dependencies = [
|
||||
"ruff_python_trivia",
|
||||
"salsa",
|
||||
"tempfile",
|
||||
"toml",
|
||||
"toml 0.9.2",
|
||||
"tracing",
|
||||
"tracing-flame",
|
||||
"tracing-subscriber",
|
||||
@@ -4152,9 +4194,12 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"insta",
|
||||
"regex",
|
||||
"ruff_db",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_parser",
|
||||
"ruff_python_trivia",
|
||||
"ruff_source_file",
|
||||
"ruff_text_size",
|
||||
"rustc-hash",
|
||||
"salsa",
|
||||
@@ -4193,7 +4238,7 @@ dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
"thiserror 2.0.12",
|
||||
"toml",
|
||||
"toml 0.9.2",
|
||||
"tracing",
|
||||
"ty_ide",
|
||||
"ty_python_semantic",
|
||||
@@ -4262,6 +4307,7 @@ dependencies = [
|
||||
"lsp-types",
|
||||
"ruff_db",
|
||||
"ruff_notebook",
|
||||
"ruff_python_ast",
|
||||
"ruff_source_file",
|
||||
"ruff_text_size",
|
||||
"rustc-hash",
|
||||
@@ -4269,6 +4315,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"shellexpand",
|
||||
"thiserror 2.0.12",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"ty_ide",
|
||||
@@ -4309,7 +4356,7 @@ dependencies = [
|
||||
"smallvec",
|
||||
"tempfile",
|
||||
"thiserror 2.0.12",
|
||||
"toml",
|
||||
"toml 0.9.2",
|
||||
"tracing",
|
||||
"ty_python_semantic",
|
||||
"ty_static",
|
||||
@@ -4803,7 +4850,7 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -165,7 +165,7 @@ tempfile = { version = "3.9.0" }
|
||||
test-case = { version = "3.3.1" }
|
||||
thiserror = { version = "2.0.0" }
|
||||
tikv-jemallocator = { version = "0.6.0" }
|
||||
toml = { version = "0.8.11" }
|
||||
toml = { version = "0.9.0" }
|
||||
tracing = { version = "0.1.40" }
|
||||
tracing-flame = { version = "0.2.0" }
|
||||
tracing-indicatif = { version = "0.3.11" }
|
||||
|
||||
@@ -148,8 +148,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
|
||||
|
||||
# For a specific version.
|
||||
curl -LsSf https://astral.sh/ruff/0.12.2/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.12.2/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.12.3/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.12.3/install.ps1 | iex"
|
||||
```
|
||||
|
||||
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
|
||||
@@ -182,7 +182,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.12.2
|
||||
rev: v0.12.3
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff-check
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.12.2"
|
||||
version = "0.12.3"
|
||||
publish = true
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -131,6 +131,7 @@ pub fn run(
|
||||
}: Args,
|
||||
) -> Result<ExitStatus> {
|
||||
{
|
||||
ruff_db::set_program_version(crate::version::version().to_string()).unwrap();
|
||||
let default_panic_hook = std::panic::take_hook();
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
#[expect(clippy::print_stderr)]
|
||||
@@ -439,7 +440,7 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
|
||||
if cli.statistics {
|
||||
printer.write_statistics(&diagnostics, &mut summary_writer)?;
|
||||
} else {
|
||||
printer.write_once(&diagnostics, &mut summary_writer)?;
|
||||
printer.write_once(&diagnostics, &mut summary_writer, preview)?;
|
||||
}
|
||||
|
||||
if !cli.exit_zero {
|
||||
|
||||
@@ -9,13 +9,14 @@ use itertools::{Itertools, iterate};
|
||||
use ruff_linter::linter::FixTable;
|
||||
use serde::Serialize;
|
||||
|
||||
use ruff_db::diagnostic::{Diagnostic, SecondaryCode};
|
||||
use ruff_db::diagnostic::{
|
||||
Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig, DisplayDiagnostics, SecondaryCode,
|
||||
};
|
||||
use ruff_linter::fs::relativize_path;
|
||||
use ruff_linter::logging::LogLevel;
|
||||
use ruff_linter::message::{
|
||||
AzureEmitter, Emitter, EmitterContext, GithubEmitter, GitlabEmitter, GroupedEmitter,
|
||||
JsonEmitter, JsonLinesEmitter, JunitEmitter, PylintEmitter, RdjsonEmitter, SarifEmitter,
|
||||
TextEmitter,
|
||||
Emitter, EmitterContext, GithubEmitter, GitlabEmitter, GroupedEmitter, JunitEmitter,
|
||||
PylintEmitter, RdjsonEmitter, SarifEmitter, TextEmitter,
|
||||
};
|
||||
use ruff_linter::notify_user;
|
||||
use ruff_linter::settings::flags::{self};
|
||||
@@ -202,6 +203,7 @@ impl Printer {
|
||||
&self,
|
||||
diagnostics: &Diagnostics,
|
||||
writer: &mut dyn Write,
|
||||
preview: bool,
|
||||
) -> Result<()> {
|
||||
if matches!(self.log_level, LogLevel::Silent) {
|
||||
return Ok(());
|
||||
@@ -229,13 +231,21 @@ impl Printer {
|
||||
|
||||
match self.format {
|
||||
OutputFormat::Json => {
|
||||
JsonEmitter.emit(writer, &diagnostics.inner, &context)?;
|
||||
let config = DisplayDiagnosticConfig::default()
|
||||
.format(DiagnosticFormat::Json)
|
||||
.preview(preview);
|
||||
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
|
||||
write!(writer, "{value}")?;
|
||||
}
|
||||
OutputFormat::Rdjson => {
|
||||
RdjsonEmitter.emit(writer, &diagnostics.inner, &context)?;
|
||||
}
|
||||
OutputFormat::JsonLines => {
|
||||
JsonLinesEmitter.emit(writer, &diagnostics.inner, &context)?;
|
||||
let config = DisplayDiagnosticConfig::default()
|
||||
.format(DiagnosticFormat::JsonLines)
|
||||
.preview(preview);
|
||||
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
|
||||
write!(writer, "{value}")?;
|
||||
}
|
||||
OutputFormat::Junit => {
|
||||
JunitEmitter.emit(writer, &diagnostics.inner, &context)?;
|
||||
@@ -283,7 +293,11 @@ impl Printer {
|
||||
PylintEmitter.emit(writer, &diagnostics.inner, &context)?;
|
||||
}
|
||||
OutputFormat::Azure => {
|
||||
AzureEmitter.emit(writer, &diagnostics.inner, &context)?;
|
||||
let config = DisplayDiagnosticConfig::default()
|
||||
.format(DiagnosticFormat::Azure)
|
||||
.preview(preview);
|
||||
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
|
||||
write!(writer, "{value}")?;
|
||||
}
|
||||
OutputFormat::Sarif => {
|
||||
SarifEmitter.emit(writer, &diagnostics.inner, &context)?;
|
||||
|
||||
@@ -120,7 +120,7 @@ fn nonexistent_config_file() {
|
||||
#[test]
|
||||
fn config_override_rejected_if_invalid_toml() {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["format", "--config", "foo = bar", "."]), @r#"
|
||||
.args(["format", "--config", "foo = bar", "."]), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
@@ -137,12 +137,11 @@ fn config_override_rejected_if_invalid_toml() {
|
||||
TOML parse error at line 1, column 7
|
||||
|
|
||||
1 | foo = bar
|
||||
| ^
|
||||
invalid string
|
||||
expected `"`, `'`
|
||||
| ^^^
|
||||
string values must be quoted, expected literal string
|
||||
|
||||
For more information, try '--help'.
|
||||
"#);
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -2246,8 +2246,7 @@ fn pyproject_toml_stdin_syntax_error() {
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
pyproject.toml:1:9: RUF200 Failed to parse pyproject.toml: invalid table header
|
||||
expected `.`, `]`
|
||||
pyproject.toml:1:9: RUF200 Failed to parse pyproject.toml: unclosed table, expected `]`
|
||||
|
|
||||
1 | [project
|
||||
| ^ RUF200
|
||||
|
||||
@@ -534,7 +534,7 @@ fn nonexistent_config_file() {
|
||||
fn config_override_rejected_if_invalid_toml() {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.args(["--config", "foo = bar", "."]), @r#"
|
||||
.args(["--config", "foo = bar", "."]), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
@@ -551,12 +551,11 @@ fn config_override_rejected_if_invalid_toml() {
|
||||
TOML parse error at line 1, column 7
|
||||
|
|
||||
1 | foo = bar
|
||||
| ^
|
||||
invalid string
|
||||
expected `"`, `'`
|
||||
| ^^^
|
||||
string values must be quoted, expected literal string
|
||||
|
||||
For more information, try '--help'.
|
||||
"#);
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -733,9 +732,8 @@ select = [E501]
|
||||
Cause: TOML parse error at line 3, column 11
|
||||
|
|
||||
3 | select = [E501]
|
||||
| ^
|
||||
invalid array
|
||||
expected `]`
|
||||
| ^^^^
|
||||
string values must be quoted, expected literal string
|
||||
");
|
||||
});
|
||||
|
||||
@@ -876,7 +874,7 @@ fn each_toml_option_requires_a_new_flag_1() {
|
||||
|
|
||||
1 | extend-select=['F841'], line-length=90
|
||||
| ^
|
||||
expected newline, `#`
|
||||
unexpected key or value, expected newline, `#`
|
||||
|
||||
For more information, try '--help'.
|
||||
");
|
||||
@@ -907,7 +905,7 @@ fn each_toml_option_requires_a_new_flag_2() {
|
||||
|
|
||||
1 | extend-select=['F841'] line-length=90
|
||||
| ^
|
||||
expected newline, `#`
|
||||
unexpected key or value, expected newline, `#`
|
||||
|
||||
For more information, try '--help'.
|
||||
");
|
||||
@@ -5692,3 +5690,57 @@ class Foo:
|
||||
"
|
||||
);
|
||||
}
|
||||
|
||||
#[test_case::test_case("concise")]
|
||||
#[test_case::test_case("full")]
|
||||
#[test_case::test_case("json")]
|
||||
#[test_case::test_case("json-lines")]
|
||||
#[test_case::test_case("junit")]
|
||||
#[test_case::test_case("grouped")]
|
||||
#[test_case::test_case("github")]
|
||||
#[test_case::test_case("gitlab")]
|
||||
#[test_case::test_case("pylint")]
|
||||
#[test_case::test_case("rdjson")]
|
||||
#[test_case::test_case("azure")]
|
||||
#[test_case::test_case("sarif")]
|
||||
fn output_format(output_format: &str) -> Result<()> {
|
||||
const CONTENT: &str = "\
|
||||
import os # F401
|
||||
x = y # F821
|
||||
match 42: # invalid-syntax
|
||||
case _: ...
|
||||
";
|
||||
|
||||
let tempdir = TempDir::new()?;
|
||||
let input = tempdir.path().join("input.py");
|
||||
fs::write(&input, CONTENT)?;
|
||||
|
||||
let snapshot = format!("output_format_{output_format}");
|
||||
|
||||
insta::with_settings!({
|
||||
filters => vec![
|
||||
(tempdir_filter(&tempdir).as_str(), "[TMP]/"),
|
||||
(r#""[^"]+\\?/?input.py"#, r#""[TMP]/input.py"#),
|
||||
(ruff_linter::VERSION, "[VERSION]"),
|
||||
]
|
||||
}, {
|
||||
assert_cmd_snapshot!(
|
||||
snapshot,
|
||||
Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args([
|
||||
"check",
|
||||
"--no-cache",
|
||||
"--output-format",
|
||||
output_format,
|
||||
"--select",
|
||||
"F401,F821",
|
||||
"--target-version",
|
||||
"py39",
|
||||
"input.py",
|
||||
])
|
||||
.current_dir(&tempdir),
|
||||
);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
23
crates/ruff/tests/snapshots/lint__output_format_azure.snap
Normal file
23
crates/ruff/tests/snapshots/lint__output_format_azure.snap
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
source: crates/ruff/tests/lint.rs
|
||||
info:
|
||||
program: ruff
|
||||
args:
|
||||
- check
|
||||
- "--no-cache"
|
||||
- "--output-format"
|
||||
- azure
|
||||
- "--select"
|
||||
- "F401,F821"
|
||||
- "--target-version"
|
||||
- py39
|
||||
- input.py
|
||||
---
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
##vso[task.logissue type=error;sourcepath=[TMP]/input.py;linenumber=1;columnnumber=8;code=F401;]`os` imported but unused
|
||||
##vso[task.logissue type=error;sourcepath=[TMP]/input.py;linenumber=2;columnnumber=5;code=F821;]Undefined name `y`
|
||||
##vso[task.logissue type=error;sourcepath=[TMP]/input.py;linenumber=3;columnnumber=1;]SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
|
||||
|
||||
----- stderr -----
|
||||
25
crates/ruff/tests/snapshots/lint__output_format_concise.snap
Normal file
25
crates/ruff/tests/snapshots/lint__output_format_concise.snap
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
source: crates/ruff/tests/lint.rs
|
||||
info:
|
||||
program: ruff
|
||||
args:
|
||||
- check
|
||||
- "--no-cache"
|
||||
- "--output-format"
|
||||
- concise
|
||||
- "--select"
|
||||
- "F401,F821"
|
||||
- "--target-version"
|
||||
- py39
|
||||
- input.py
|
||||
---
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
input.py:1:8: F401 [*] `os` imported but unused
|
||||
input.py:2:5: F821 Undefined name `y`
|
||||
input.py:3:1: SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
|
||||
Found 3 errors.
|
||||
[*] 1 fixable with the `--fix` option.
|
||||
|
||||
----- stderr -----
|
||||
49
crates/ruff/tests/snapshots/lint__output_format_full.snap
Normal file
49
crates/ruff/tests/snapshots/lint__output_format_full.snap
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
source: crates/ruff/tests/lint.rs
|
||||
info:
|
||||
program: ruff
|
||||
args:
|
||||
- check
|
||||
- "--no-cache"
|
||||
- "--output-format"
|
||||
- full
|
||||
- "--select"
|
||||
- "F401,F821"
|
||||
- "--target-version"
|
||||
- py39
|
||||
- input.py
|
||||
---
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
input.py:1:8: F401 [*] `os` imported but unused
|
||||
|
|
||||
1 | import os # F401
|
||||
| ^^ F401
|
||||
2 | x = y # F821
|
||||
3 | match 42: # invalid-syntax
|
||||
|
|
||||
= help: Remove unused import: `os`
|
||||
|
||||
input.py:2:5: F821 Undefined name `y`
|
||||
|
|
||||
1 | import os # F401
|
||||
2 | x = y # F821
|
||||
| ^ F821
|
||||
3 | match 42: # invalid-syntax
|
||||
4 | case _: ...
|
||||
|
|
||||
|
||||
input.py:3:1: SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
|
||||
|
|
||||
1 | import os # F401
|
||||
2 | x = y # F821
|
||||
3 | match 42: # invalid-syntax
|
||||
| ^^^^^
|
||||
4 | case _: ...
|
||||
|
|
||||
|
||||
Found 3 errors.
|
||||
[*] 1 fixable with the `--fix` option.
|
||||
|
||||
----- stderr -----
|
||||
23
crates/ruff/tests/snapshots/lint__output_format_github.snap
Normal file
23
crates/ruff/tests/snapshots/lint__output_format_github.snap
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
source: crates/ruff/tests/lint.rs
|
||||
info:
|
||||
program: ruff
|
||||
args:
|
||||
- check
|
||||
- "--no-cache"
|
||||
- "--output-format"
|
||||
- github
|
||||
- "--select"
|
||||
- "F401,F821"
|
||||
- "--target-version"
|
||||
- py39
|
||||
- input.py
|
||||
---
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
::error title=Ruff (F401),file=[TMP]/input.py,line=1,col=8,endLine=1,endColumn=10::input.py:1:8: F401 `os` imported but unused
|
||||
::error title=Ruff (F821),file=[TMP]/input.py,line=2,col=5,endLine=2,endColumn=6::input.py:2:5: F821 Undefined name `y`
|
||||
::error title=Ruff,file=[TMP]/input.py,line=3,col=1,endLine=3,endColumn=6::input.py:3:1: SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
|
||||
|
||||
----- stderr -----
|
||||
60
crates/ruff/tests/snapshots/lint__output_format_gitlab.snap
Normal file
60
crates/ruff/tests/snapshots/lint__output_format_gitlab.snap
Normal file
@@ -0,0 +1,60 @@
|
||||
---
|
||||
source: crates/ruff/tests/lint.rs
|
||||
info:
|
||||
program: ruff
|
||||
args:
|
||||
- check
|
||||
- "--no-cache"
|
||||
- "--output-format"
|
||||
- gitlab
|
||||
- "--select"
|
||||
- "F401,F821"
|
||||
- "--target-version"
|
||||
- py39
|
||||
- input.py
|
||||
---
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
[
|
||||
{
|
||||
"check_name": "F401",
|
||||
"description": "`os` imported but unused",
|
||||
"fingerprint": "4dbad37161e65c72",
|
||||
"location": {
|
||||
"lines": {
|
||||
"begin": 1,
|
||||
"end": 1
|
||||
},
|
||||
"path": "input.py"
|
||||
},
|
||||
"severity": "major"
|
||||
},
|
||||
{
|
||||
"check_name": "F821",
|
||||
"description": "Undefined name `y`",
|
||||
"fingerprint": "7af59862a085230",
|
||||
"location": {
|
||||
"lines": {
|
||||
"begin": 2,
|
||||
"end": 2
|
||||
},
|
||||
"path": "input.py"
|
||||
},
|
||||
"severity": "major"
|
||||
},
|
||||
{
|
||||
"check_name": "syntax-error",
|
||||
"description": "Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)",
|
||||
"fingerprint": "e558cec859bb66e8",
|
||||
"location": {
|
||||
"lines": {
|
||||
"begin": 3,
|
||||
"end": 3
|
||||
},
|
||||
"path": "input.py"
|
||||
},
|
||||
"severity": "major"
|
||||
}
|
||||
]
|
||||
----- stderr -----
|
||||
27
crates/ruff/tests/snapshots/lint__output_format_grouped.snap
Normal file
27
crates/ruff/tests/snapshots/lint__output_format_grouped.snap
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
source: crates/ruff/tests/lint.rs
|
||||
info:
|
||||
program: ruff
|
||||
args:
|
||||
- check
|
||||
- "--no-cache"
|
||||
- "--output-format"
|
||||
- grouped
|
||||
- "--select"
|
||||
- "F401,F821"
|
||||
- "--target-version"
|
||||
- py39
|
||||
- input.py
|
||||
---
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
input.py:
|
||||
1:8 F401 [*] `os` imported but unused
|
||||
2:5 F821 Undefined name `y`
|
||||
3:1 SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
|
||||
|
||||
Found 3 errors.
|
||||
[*] 1 fixable with the `--fix` option.
|
||||
|
||||
----- stderr -----
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
source: crates/ruff/tests/lint.rs
|
||||
info:
|
||||
program: ruff
|
||||
args:
|
||||
- check
|
||||
- "--no-cache"
|
||||
- "--output-format"
|
||||
- json-lines
|
||||
- "--select"
|
||||
- "F401,F821"
|
||||
- "--target-version"
|
||||
- py39
|
||||
- input.py
|
||||
---
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
{"cell":null,"code":"F401","end_location":{"column":10,"row":1},"filename":"[TMP]/input.py","fix":{"applicability":"safe","edits":[{"content":"","end_location":{"column":1,"row":2},"location":{"column":1,"row":1}}],"message":"Remove unused import: `os`"},"location":{"column":8,"row":1},"message":"`os` imported but unused","noqa_row":1,"url":"https://docs.astral.sh/ruff/rules/unused-import"}
|
||||
{"cell":null,"code":"F821","end_location":{"column":6,"row":2},"filename":"[TMP]/input.py","fix":null,"location":{"column":5,"row":2},"message":"Undefined name `y`","noqa_row":2,"url":"https://docs.astral.sh/ruff/rules/undefined-name"}
|
||||
{"cell":null,"code":null,"end_location":{"column":6,"row":3},"filename":"[TMP]/input.py","fix":null,"location":{"column":1,"row":3},"message":"SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)","noqa_row":null,"url":null}
|
||||
|
||||
----- stderr -----
|
||||
88
crates/ruff/tests/snapshots/lint__output_format_json.snap
Normal file
88
crates/ruff/tests/snapshots/lint__output_format_json.snap
Normal file
@@ -0,0 +1,88 @@
|
||||
---
|
||||
source: crates/ruff/tests/lint.rs
|
||||
info:
|
||||
program: ruff
|
||||
args:
|
||||
- check
|
||||
- "--no-cache"
|
||||
- "--output-format"
|
||||
- json
|
||||
- "--select"
|
||||
- "F401,F821"
|
||||
- "--target-version"
|
||||
- py39
|
||||
- input.py
|
||||
---
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
[
|
||||
{
|
||||
"cell": null,
|
||||
"code": "F401",
|
||||
"end_location": {
|
||||
"column": 10,
|
||||
"row": 1
|
||||
},
|
||||
"filename": "[TMP]/input.py",
|
||||
"fix": {
|
||||
"applicability": "safe",
|
||||
"edits": [
|
||||
{
|
||||
"content": "",
|
||||
"end_location": {
|
||||
"column": 1,
|
||||
"row": 2
|
||||
},
|
||||
"location": {
|
||||
"column": 1,
|
||||
"row": 1
|
||||
}
|
||||
}
|
||||
],
|
||||
"message": "Remove unused import: `os`"
|
||||
},
|
||||
"location": {
|
||||
"column": 8,
|
||||
"row": 1
|
||||
},
|
||||
"message": "`os` imported but unused",
|
||||
"noqa_row": 1,
|
||||
"url": "https://docs.astral.sh/ruff/rules/unused-import"
|
||||
},
|
||||
{
|
||||
"cell": null,
|
||||
"code": "F821",
|
||||
"end_location": {
|
||||
"column": 6,
|
||||
"row": 2
|
||||
},
|
||||
"filename": "[TMP]/input.py",
|
||||
"fix": null,
|
||||
"location": {
|
||||
"column": 5,
|
||||
"row": 2
|
||||
},
|
||||
"message": "Undefined name `y`",
|
||||
"noqa_row": 2,
|
||||
"url": "https://docs.astral.sh/ruff/rules/undefined-name"
|
||||
},
|
||||
{
|
||||
"cell": null,
|
||||
"code": null,
|
||||
"end_location": {
|
||||
"column": 6,
|
||||
"row": 3
|
||||
},
|
||||
"filename": "[TMP]/input.py",
|
||||
"fix": null,
|
||||
"location": {
|
||||
"column": 1,
|
||||
"row": 3
|
||||
},
|
||||
"message": "SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)",
|
||||
"noqa_row": null,
|
||||
"url": null
|
||||
}
|
||||
]
|
||||
----- stderr -----
|
||||
34
crates/ruff/tests/snapshots/lint__output_format_junit.snap
Normal file
34
crates/ruff/tests/snapshots/lint__output_format_junit.snap
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
source: crates/ruff/tests/lint.rs
|
||||
info:
|
||||
program: ruff
|
||||
args:
|
||||
- check
|
||||
- "--no-cache"
|
||||
- "--output-format"
|
||||
- junit
|
||||
- "--select"
|
||||
- "F401,F821"
|
||||
- "--target-version"
|
||||
- py39
|
||||
- input.py
|
||||
---
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<testsuites name="ruff" tests="3" failures="3" errors="0">
|
||||
<testsuite name="[TMP]/input.py" tests="3" disabled="0" errors="0" failures="3" package="org.ruff">
|
||||
<testcase name="org.ruff.F401" classname="[TMP]/input" line="1" column="8">
|
||||
<failure message="`os` imported but unused">line 1, col 8, `os` imported but unused</failure>
|
||||
</testcase>
|
||||
<testcase name="org.ruff.F821" classname="[TMP]/input" line="2" column="5">
|
||||
<failure message="Undefined name `y`">line 2, col 5, Undefined name `y`</failure>
|
||||
</testcase>
|
||||
<testcase name="org.ruff" classname="[TMP]/input" line="3" column="1">
|
||||
<failure message="SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)">line 3, col 1, SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)</failure>
|
||||
</testcase>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
----- stderr -----
|
||||
23
crates/ruff/tests/snapshots/lint__output_format_pylint.snap
Normal file
23
crates/ruff/tests/snapshots/lint__output_format_pylint.snap
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
source: crates/ruff/tests/lint.rs
|
||||
info:
|
||||
program: ruff
|
||||
args:
|
||||
- check
|
||||
- "--no-cache"
|
||||
- "--output-format"
|
||||
- pylint
|
||||
- "--select"
|
||||
- "F401,F821"
|
||||
- "--target-version"
|
||||
- py39
|
||||
- input.py
|
||||
---
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
input.py:1: [F401] `os` imported but unused
|
||||
input.py:2: [F821] Undefined name `y`
|
||||
input.py:3: SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
|
||||
|
||||
----- stderr -----
|
||||
103
crates/ruff/tests/snapshots/lint__output_format_rdjson.snap
Normal file
103
crates/ruff/tests/snapshots/lint__output_format_rdjson.snap
Normal file
@@ -0,0 +1,103 @@
|
||||
---
|
||||
source: crates/ruff/tests/lint.rs
|
||||
info:
|
||||
program: ruff
|
||||
args:
|
||||
- check
|
||||
- "--no-cache"
|
||||
- "--output-format"
|
||||
- rdjson
|
||||
- "--select"
|
||||
- "F401,F821"
|
||||
- "--target-version"
|
||||
- py39
|
||||
- input.py
|
||||
---
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
{
|
||||
"diagnostics": [
|
||||
{
|
||||
"code": {
|
||||
"url": "https://docs.astral.sh/ruff/rules/unused-import",
|
||||
"value": "F401"
|
||||
},
|
||||
"location": {
|
||||
"path": "[TMP]/input.py",
|
||||
"range": {
|
||||
"end": {
|
||||
"column": 10,
|
||||
"line": 1
|
||||
},
|
||||
"start": {
|
||||
"column": 8,
|
||||
"line": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"message": "`os` imported but unused",
|
||||
"suggestions": [
|
||||
{
|
||||
"range": {
|
||||
"end": {
|
||||
"column": 1,
|
||||
"line": 2
|
||||
},
|
||||
"start": {
|
||||
"column": 1,
|
||||
"line": 1
|
||||
}
|
||||
},
|
||||
"text": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": {
|
||||
"url": "https://docs.astral.sh/ruff/rules/undefined-name",
|
||||
"value": "F821"
|
||||
},
|
||||
"location": {
|
||||
"path": "[TMP]/input.py",
|
||||
"range": {
|
||||
"end": {
|
||||
"column": 6,
|
||||
"line": 2
|
||||
},
|
||||
"start": {
|
||||
"column": 5,
|
||||
"line": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
"message": "Undefined name `y`"
|
||||
},
|
||||
{
|
||||
"code": {
|
||||
"url": null,
|
||||
"value": null
|
||||
},
|
||||
"location": {
|
||||
"path": "[TMP]/input.py",
|
||||
"range": {
|
||||
"end": {
|
||||
"column": 6,
|
||||
"line": 3
|
||||
},
|
||||
"start": {
|
||||
"column": 1,
|
||||
"line": 3
|
||||
}
|
||||
}
|
||||
},
|
||||
"message": "SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)"
|
||||
}
|
||||
],
|
||||
"severity": "warning",
|
||||
"source": {
|
||||
"name": "ruff",
|
||||
"url": "https://docs.astral.sh/ruff"
|
||||
}
|
||||
}
|
||||
----- stderr -----
|
||||
142
crates/ruff/tests/snapshots/lint__output_format_sarif.snap
Normal file
142
crates/ruff/tests/snapshots/lint__output_format_sarif.snap
Normal file
@@ -0,0 +1,142 @@
|
||||
---
|
||||
source: crates/ruff/tests/lint.rs
|
||||
info:
|
||||
program: ruff
|
||||
args:
|
||||
- check
|
||||
- "--no-cache"
|
||||
- "--output-format"
|
||||
- sarif
|
||||
- "--select"
|
||||
- "F401,F821"
|
||||
- "--target-version"
|
||||
- py39
|
||||
- input.py
|
||||
---
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
|
||||
"runs": [
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"level": "error",
|
||||
"locations": [
|
||||
{
|
||||
"physicalLocation": {
|
||||
"artifactLocation": {
|
||||
"uri": "[TMP]/input.py"
|
||||
},
|
||||
"region": {
|
||||
"endColumn": 10,
|
||||
"endLine": 1,
|
||||
"startColumn": 8,
|
||||
"startLine": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"message": {
|
||||
"text": "`os` imported but unused"
|
||||
},
|
||||
"ruleId": "F401"
|
||||
},
|
||||
{
|
||||
"level": "error",
|
||||
"locations": [
|
||||
{
|
||||
"physicalLocation": {
|
||||
"artifactLocation": {
|
||||
"uri": "[TMP]/input.py"
|
||||
},
|
||||
"region": {
|
||||
"endColumn": 6,
|
||||
"endLine": 2,
|
||||
"startColumn": 5,
|
||||
"startLine": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"message": {
|
||||
"text": "Undefined name `y`"
|
||||
},
|
||||
"ruleId": "F821"
|
||||
},
|
||||
{
|
||||
"level": "error",
|
||||
"locations": [
|
||||
{
|
||||
"physicalLocation": {
|
||||
"artifactLocation": {
|
||||
"uri": "[TMP]/input.py"
|
||||
},
|
||||
"region": {
|
||||
"endColumn": 6,
|
||||
"endLine": 3,
|
||||
"startColumn": 1,
|
||||
"startLine": 3
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"message": {
|
||||
"text": "SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)"
|
||||
},
|
||||
"ruleId": null
|
||||
}
|
||||
],
|
||||
"tool": {
|
||||
"driver": {
|
||||
"informationUri": "https://github.com/astral-sh/ruff",
|
||||
"name": "ruff",
|
||||
"rules": [
|
||||
{
|
||||
"fullDescription": {
|
||||
"text": "## What it does\nChecks for unused imports.\n\n## Why is this bad?\nUnused imports add a performance overhead at runtime, and risk creating\nimport cycles. They also increase the cognitive load of reading the code.\n\nIf an import statement is used to check for the availability or existence\nof a module, consider using `importlib.util.find_spec` instead.\n\nIf an import statement is used to re-export a symbol as part of a module's\npublic interface, consider using a \"redundant\" import alias, which\ninstructs Ruff (and other tools) to respect the re-export, and avoid\nmarking it as unused, as in:\n\n```python\nfrom module import member as member\n```\n\nAlternatively, you can use `__all__` to declare a symbol as part of the module's\ninterface, as in:\n\n```python\n# __init__.py\nimport some_module\n\n__all__ = [\"some_module\"]\n```\n\n## Fix safety\n\nFixes to remove unused imports are safe, except in `__init__.py` files.\n\nApplying fixes to `__init__.py` files is currently in preview. The fix offered depends on the\ntype of the unused import. Ruff will suggest a safe fix to export first-party imports with\neither a redundant alias or, if already present in the file, an `__all__` entry. If multiple\n`__all__` declarations are present, Ruff will not offer a fix. Ruff will suggest an unsafe fix\nto remove third-party and standard library imports -- the fix is unsafe because the module's\ninterface changes.\n\n## Example\n\n```python\nimport numpy as np # unused import\n\n\ndef area(radius):\n return 3.14 * radius**2\n```\n\nUse instead:\n\n```python\ndef area(radius):\n return 3.14 * radius**2\n```\n\nTo check the availability of a module, use `importlib.util.find_spec`:\n\n```python\nfrom importlib.util import find_spec\n\nif find_spec(\"numpy\") is not None:\n print(\"numpy is installed\")\nelse:\n print(\"numpy is not installed\")\n```\n\n## Preview\nWhen [preview](https://docs.astral.sh/ruff/preview/) is enabled,\nthe criterion for determining whether an import is first-party\nis stricter, which could affect the suggested fix. See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) for more details.\n\n## Options\n- `lint.ignore-init-module-imports`\n- `lint.pyflakes.allowed-unused-imports`\n\n## References\n- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)\n- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)\n- [Typing documentation: interface conventions](https://typing.python.org/en/latest/source/libraries.html#library-interface-public-and-private-symbols)\n"
|
||||
},
|
||||
"help": {
|
||||
"text": "`{name}` imported but unused; consider using `importlib.util.find_spec` to test for availability"
|
||||
},
|
||||
"helpUri": "https://docs.astral.sh/ruff/rules/unused-import",
|
||||
"id": "F401",
|
||||
"properties": {
|
||||
"id": "F401",
|
||||
"kind": "Pyflakes",
|
||||
"name": "unused-import",
|
||||
"problem.severity": "error"
|
||||
},
|
||||
"shortDescription": {
|
||||
"text": "`{name}` imported but unused; consider using `importlib.util.find_spec` to test for availability"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fullDescription": {
|
||||
"text": "## What it does\nChecks for uses of undefined names.\n\n## Why is this bad?\nAn undefined name is likely to raise `NameError` at runtime.\n\n## Example\n```python\ndef double():\n return n * 2 # raises `NameError` if `n` is undefined when `double` is called\n```\n\nUse instead:\n```python\ndef double(n):\n return n * 2\n```\n\n## Options\n- [`target-version`]: Can be used to configure which symbols Ruff will understand\n as being available in the `builtins` namespace.\n\n## References\n- [Python documentation: Naming and binding](https://docs.python.org/3/reference/executionmodel.html#naming-and-binding)\n"
|
||||
},
|
||||
"help": {
|
||||
"text": "Undefined name `{name}`. {tip}"
|
||||
},
|
||||
"helpUri": "https://docs.astral.sh/ruff/rules/undefined-name",
|
||||
"id": "F821",
|
||||
"properties": {
|
||||
"id": "F821",
|
||||
"kind": "Pyflakes",
|
||||
"name": "undefined-name",
|
||||
"problem.severity": "error"
|
||||
},
|
||||
"shortDescription": {
|
||||
"text": "Undefined name `{name}`. {tip}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version": "[VERSION]"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"version": "2.1.0"
|
||||
}
|
||||
----- stderr -----
|
||||
@@ -60,7 +60,7 @@ fn config_option_ignored_but_validated() {
|
||||
assert_cmd_snapshot!(
|
||||
Command::new(get_cargo_bin(BIN_NAME))
|
||||
.arg("version")
|
||||
.args(["--config", "foo = bar"]), @r#"
|
||||
.args(["--config", "foo = bar"]), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
@@ -77,12 +77,11 @@ fn config_option_ignored_but_validated() {
|
||||
TOML parse error at line 1, column 7
|
||||
|
|
||||
1 | foo = bar
|
||||
| ^
|
||||
invalid string
|
||||
expected `"`, `'`
|
||||
| ^^^
|
||||
string values must be quoted, expected literal string
|
||||
|
||||
For more information, try '--help'.
|
||||
"#
|
||||
"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ rustc-hash = { workspace = true }
|
||||
salsa = { workspace = true }
|
||||
schemars = { workspace = true, optional = true }
|
||||
serde = { workspace = true, optional = true }
|
||||
serde_json = { workspace = true, optional = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true, optional = true }
|
||||
@@ -56,6 +57,6 @@ tempfile = { workspace = true }
|
||||
[features]
|
||||
cache = ["ruff_cache"]
|
||||
os = ["ignore", "dep:etcetera"]
|
||||
serde = ["dep:serde", "camino/serde1"]
|
||||
serde = ["camino/serde1", "dep:serde", "dep:serde_json", "ruff_diagnostics/serde"]
|
||||
# Exposes testing utilities.
|
||||
testing = ["tracing-subscriber"]
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
use std::{fmt::Formatter, sync::Arc};
|
||||
|
||||
use render::{FileResolver, Input};
|
||||
use ruff_diagnostics::Fix;
|
||||
use ruff_source_file::{LineColumn, SourceCode, SourceFile};
|
||||
|
||||
use ruff_annotate_snippets::Level as AnnotateLevel;
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
|
||||
pub use self::render::DisplayDiagnostic;
|
||||
pub use self::render::{DisplayDiagnostic, DisplayDiagnostics, FileResolver, Input};
|
||||
use crate::{Db, files::File};
|
||||
|
||||
mod render;
|
||||
@@ -380,7 +379,7 @@ impl Diagnostic {
|
||||
}
|
||||
|
||||
/// Returns the URL for the rule documentation, if it exists.
|
||||
pub fn to_url(&self) -> Option<String> {
|
||||
pub fn to_ruff_url(&self) -> Option<String> {
|
||||
if self.is_invalid_syntax() {
|
||||
None
|
||||
} else {
|
||||
@@ -432,8 +431,9 @@ impl Diagnostic {
|
||||
/// Returns the [`SourceFile`] which the message belongs to.
|
||||
///
|
||||
/// Panics if the diagnostic has no primary span, or if its file is not a `SourceFile`.
|
||||
pub fn expect_ruff_source_file(&self) -> SourceFile {
|
||||
self.expect_primary_span().expect_ruff_file().clone()
|
||||
pub fn expect_ruff_source_file(&self) -> &SourceFile {
|
||||
self.ruff_source_file()
|
||||
.expect("Expected a ruff source file")
|
||||
}
|
||||
|
||||
/// Returns the [`TextRange`] for the diagnostic.
|
||||
@@ -1174,6 +1174,12 @@ pub struct DisplayDiagnosticConfig {
|
||||
/// here for now as the most "sensible" place for it to live until
|
||||
/// we had more concrete use cases. ---AG
|
||||
context: usize,
|
||||
/// Whether to use preview formatting for Ruff diagnostics.
|
||||
#[allow(
|
||||
dead_code,
|
||||
reason = "This is currently only used for JSON but will be needed soon for other formats"
|
||||
)]
|
||||
preview: bool,
|
||||
}
|
||||
|
||||
impl DisplayDiagnosticConfig {
|
||||
@@ -1194,6 +1200,14 @@ impl DisplayDiagnosticConfig {
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether to enable preview behavior or not.
|
||||
pub fn preview(self, yes: bool) -> DisplayDiagnosticConfig {
|
||||
DisplayDiagnosticConfig {
|
||||
preview: yes,
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DisplayDiagnosticConfig {
|
||||
@@ -1202,6 +1216,7 @@ impl Default for DisplayDiagnosticConfig {
|
||||
format: DiagnosticFormat::default(),
|
||||
color: false,
|
||||
context: 2,
|
||||
preview: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1229,6 +1244,21 @@ pub enum DiagnosticFormat {
|
||||
///
|
||||
/// This may use color when printing to a `tty`.
|
||||
Concise,
|
||||
/// Print diagnostics in the [Azure Pipelines] format.
|
||||
///
|
||||
/// [Azure Pipelines]: https://learn.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands?view=azure-devops&tabs=bash#logissue-log-an-error-or-warning
|
||||
Azure,
|
||||
/// Print diagnostics in JSON format.
|
||||
///
|
||||
/// Unlike `json-lines`, this prints all of the diagnostics as a JSON array.
|
||||
#[cfg(feature = "serde")]
|
||||
Json,
|
||||
/// Print diagnostics in JSON format, one per line.
|
||||
///
|
||||
/// This will print each diagnostic as a separate JSON object on its own line. See the `json`
|
||||
/// format for an array of all diagnostics. See <https://jsonlines.org/> for more details.
|
||||
#[cfg(feature = "serde")]
|
||||
JsonLines,
|
||||
}
|
||||
|
||||
/// A representation of the kinds of messages inside a diagnostic.
|
||||
|
||||
@@ -4,6 +4,7 @@ use ruff_annotate_snippets::{
|
||||
Annotation as AnnotateAnnotation, Level as AnnotateLevel, Message as AnnotateMessage,
|
||||
Renderer as AnnotateRenderer, Snippet as AnnotateSnippet,
|
||||
};
|
||||
use ruff_notebook::{Notebook, NotebookIndex};
|
||||
use ruff_source_file::{LineIndex, OneIndexed, SourceCode};
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
|
||||
@@ -17,9 +18,17 @@ use crate::{
|
||||
|
||||
use super::{
|
||||
Annotation, Diagnostic, DiagnosticFormat, DiagnosticSource, DisplayDiagnosticConfig, Severity,
|
||||
SubDiagnostic,
|
||||
SubDiagnostic, UnifiedFile,
|
||||
};
|
||||
|
||||
use azure::AzureRenderer;
|
||||
|
||||
mod azure;
|
||||
#[cfg(feature = "serde")]
|
||||
mod json;
|
||||
#[cfg(feature = "serde")]
|
||||
mod json_lines;
|
||||
|
||||
/// A type that implements `std::fmt::Display` for diagnostic rendering.
|
||||
///
|
||||
/// It is created via [`Diagnostic::display`].
|
||||
@@ -34,7 +43,6 @@ use super::{
|
||||
pub struct DisplayDiagnostic<'a> {
|
||||
config: &'a DisplayDiagnosticConfig,
|
||||
resolver: &'a dyn FileResolver,
|
||||
annotate_renderer: AnnotateRenderer,
|
||||
diag: &'a Diagnostic,
|
||||
}
|
||||
|
||||
@@ -44,16 +52,9 @@ impl<'a> DisplayDiagnostic<'a> {
|
||||
config: &'a DisplayDiagnosticConfig,
|
||||
diag: &'a Diagnostic,
|
||||
) -> DisplayDiagnostic<'a> {
|
||||
let annotate_renderer = if config.color {
|
||||
AnnotateRenderer::styled()
|
||||
} else {
|
||||
AnnotateRenderer::plain()
|
||||
};
|
||||
|
||||
DisplayDiagnostic {
|
||||
config,
|
||||
resolver,
|
||||
annotate_renderer,
|
||||
diag,
|
||||
}
|
||||
}
|
||||
@@ -61,68 +62,131 @@ impl<'a> DisplayDiagnostic<'a> {
|
||||
|
||||
impl std::fmt::Display for DisplayDiagnostic<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
let stylesheet = if self.config.color {
|
||||
DiagnosticStylesheet::styled()
|
||||
} else {
|
||||
DiagnosticStylesheet::plain()
|
||||
};
|
||||
DisplayDiagnostics::new(self.resolver, self.config, std::slice::from_ref(self.diag)).fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(self.config.format, DiagnosticFormat::Concise) {
|
||||
let (severity, severity_style) = match self.diag.severity() {
|
||||
Severity::Info => ("info", stylesheet.info),
|
||||
Severity::Warning => ("warning", stylesheet.warning),
|
||||
Severity::Error => ("error", stylesheet.error),
|
||||
Severity::Fatal => ("fatal", stylesheet.error),
|
||||
};
|
||||
/// A type that implements `std::fmt::Display` for rendering a collection of diagnostics.
|
||||
///
|
||||
/// It is intended for collections of diagnostics that need to be serialized together, as is the
|
||||
/// case for JSON, for example.
|
||||
///
|
||||
/// See [`DisplayDiagnostic`] for rendering individual `Diagnostic`s and details about the lifetime
|
||||
/// constraints.
|
||||
pub struct DisplayDiagnostics<'a> {
|
||||
config: &'a DisplayDiagnosticConfig,
|
||||
resolver: &'a dyn FileResolver,
|
||||
diagnostics: &'a [Diagnostic],
|
||||
}
|
||||
|
||||
write!(
|
||||
f,
|
||||
"{severity}[{id}]",
|
||||
severity = fmt_styled(severity, severity_style),
|
||||
id = fmt_styled(self.diag.id(), stylesheet.emphasis)
|
||||
)?;
|
||||
impl<'a> DisplayDiagnostics<'a> {
|
||||
pub fn new(
|
||||
resolver: &'a dyn FileResolver,
|
||||
config: &'a DisplayDiagnosticConfig,
|
||||
diagnostics: &'a [Diagnostic],
|
||||
) -> DisplayDiagnostics<'a> {
|
||||
DisplayDiagnostics {
|
||||
config,
|
||||
resolver,
|
||||
diagnostics,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(span) = self.diag.primary_span() {
|
||||
write!(
|
||||
f,
|
||||
" {path}",
|
||||
path = fmt_styled(span.file().path(self.resolver), stylesheet.emphasis)
|
||||
)?;
|
||||
if let Some(range) = span.range() {
|
||||
let diagnostic_source = span.file().diagnostic_source(self.resolver);
|
||||
let start = diagnostic_source
|
||||
.as_source_code()
|
||||
.line_column(range.start());
|
||||
impl std::fmt::Display for DisplayDiagnostics<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self.config.format {
|
||||
DiagnosticFormat::Concise => {
|
||||
let stylesheet = if self.config.color {
|
||||
DiagnosticStylesheet::styled()
|
||||
} else {
|
||||
DiagnosticStylesheet::plain()
|
||||
};
|
||||
|
||||
for diag in self.diagnostics {
|
||||
let (severity, severity_style) = match diag.severity() {
|
||||
Severity::Info => ("info", stylesheet.info),
|
||||
Severity::Warning => ("warning", stylesheet.warning),
|
||||
Severity::Error => ("error", stylesheet.error),
|
||||
Severity::Fatal => ("fatal", stylesheet.error),
|
||||
};
|
||||
write!(
|
||||
f,
|
||||
":{line}:{col}",
|
||||
line = fmt_styled(start.line, stylesheet.emphasis),
|
||||
col = fmt_styled(start.column, stylesheet.emphasis),
|
||||
"{severity}[{id}]",
|
||||
severity = fmt_styled(severity, severity_style),
|
||||
id = fmt_styled(diag.id(), stylesheet.emphasis)
|
||||
)?;
|
||||
if let Some(span) = diag.primary_span() {
|
||||
write!(
|
||||
f,
|
||||
" {path}",
|
||||
path = fmt_styled(span.file().path(self.resolver), stylesheet.emphasis)
|
||||
)?;
|
||||
if let Some(range) = span.range() {
|
||||
let diagnostic_source = span.file().diagnostic_source(self.resolver);
|
||||
let start = diagnostic_source
|
||||
.as_source_code()
|
||||
.line_column(range.start());
|
||||
|
||||
write!(
|
||||
f,
|
||||
":{line}:{col}",
|
||||
line = fmt_styled(start.line, stylesheet.emphasis),
|
||||
col = fmt_styled(start.column, stylesheet.emphasis),
|
||||
)?;
|
||||
}
|
||||
write!(f, ":")?;
|
||||
}
|
||||
writeln!(f, " {message}", message = diag.concise_message())?;
|
||||
}
|
||||
write!(f, ":")?;
|
||||
}
|
||||
return writeln!(f, " {message}", message = self.diag.concise_message());
|
||||
DiagnosticFormat::Full => {
|
||||
let stylesheet = if self.config.color {
|
||||
DiagnosticStylesheet::styled()
|
||||
} else {
|
||||
DiagnosticStylesheet::plain()
|
||||
};
|
||||
|
||||
let mut renderer = if self.config.color {
|
||||
AnnotateRenderer::styled()
|
||||
} else {
|
||||
AnnotateRenderer::plain()
|
||||
};
|
||||
|
||||
renderer = renderer
|
||||
.error(stylesheet.error)
|
||||
.warning(stylesheet.warning)
|
||||
.info(stylesheet.info)
|
||||
.note(stylesheet.note)
|
||||
.help(stylesheet.help)
|
||||
.line_no(stylesheet.line_no)
|
||||
.emphasis(stylesheet.emphasis)
|
||||
.none(stylesheet.none);
|
||||
|
||||
for diag in self.diagnostics {
|
||||
let resolved = Resolved::new(self.resolver, diag);
|
||||
let renderable = resolved.to_renderable(self.config.context);
|
||||
for diag in renderable.diagnostics.iter() {
|
||||
writeln!(f, "{}", renderer.render(diag.to_annotate()))?;
|
||||
}
|
||||
writeln!(f)?;
|
||||
}
|
||||
}
|
||||
DiagnosticFormat::Azure => {
|
||||
AzureRenderer::new(self.resolver).render(f, self.diagnostics)?;
|
||||
}
|
||||
#[cfg(feature = "serde")]
|
||||
DiagnosticFormat::Json => {
|
||||
json::JsonRenderer::new(self.resolver, self.config).render(f, self.diagnostics)?;
|
||||
}
|
||||
#[cfg(feature = "serde")]
|
||||
DiagnosticFormat::JsonLines => {
|
||||
json_lines::JsonLinesRenderer::new(self.resolver, self.config)
|
||||
.render(f, self.diagnostics)?;
|
||||
}
|
||||
}
|
||||
|
||||
let mut renderer = self.annotate_renderer.clone();
|
||||
renderer = renderer
|
||||
.error(stylesheet.error)
|
||||
.warning(stylesheet.warning)
|
||||
.info(stylesheet.info)
|
||||
.note(stylesheet.note)
|
||||
.help(stylesheet.help)
|
||||
.line_no(stylesheet.line_no)
|
||||
.emphasis(stylesheet.emphasis)
|
||||
.none(stylesheet.none);
|
||||
|
||||
let resolved = Resolved::new(self.resolver, self.diag);
|
||||
let renderable = resolved.to_renderable(self.config.context);
|
||||
for diag in renderable.diagnostics.iter() {
|
||||
writeln!(f, "{}", renderer.render(diag.to_annotate()))?;
|
||||
}
|
||||
writeln!(f)
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -635,6 +699,12 @@ pub trait FileResolver {
|
||||
|
||||
/// Returns the input contents associated with the file given.
|
||||
fn input(&self, file: File) -> Input;
|
||||
|
||||
/// Returns the [`NotebookIndex`] associated with the file given, if it's a Jupyter notebook.
|
||||
fn notebook_index(&self, file: &UnifiedFile) -> Option<NotebookIndex>;
|
||||
|
||||
/// Returns whether the file given is a Jupyter notebook.
|
||||
fn is_notebook(&self, file: &UnifiedFile) -> bool;
|
||||
}
|
||||
|
||||
impl<T> FileResolver for T
|
||||
@@ -651,6 +721,25 @@ where
|
||||
line_index: line_index(self, file),
|
||||
}
|
||||
}
|
||||
|
||||
fn notebook_index(&self, file: &UnifiedFile) -> Option<NotebookIndex> {
|
||||
match file {
|
||||
UnifiedFile::Ty(file) => self
|
||||
.input(*file)
|
||||
.text
|
||||
.as_notebook()
|
||||
.map(Notebook::index)
|
||||
.cloned(),
|
||||
UnifiedFile::Ruff(_) => unimplemented!("Expected an interned ty file"),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_notebook(&self, file: &UnifiedFile) -> bool {
|
||||
match file {
|
||||
UnifiedFile::Ty(file) => self.input(*file).text.as_notebook().is_some(),
|
||||
UnifiedFile::Ruff(_) => unimplemented!("Expected an interned ty file"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FileResolver for &dyn Db {
|
||||
@@ -664,6 +753,25 @@ impl FileResolver for &dyn Db {
|
||||
line_index: line_index(*self, file),
|
||||
}
|
||||
}
|
||||
|
||||
fn notebook_index(&self, file: &UnifiedFile) -> Option<NotebookIndex> {
|
||||
match file {
|
||||
UnifiedFile::Ty(file) => self
|
||||
.input(*file)
|
||||
.text
|
||||
.as_notebook()
|
||||
.map(Notebook::index)
|
||||
.cloned(),
|
||||
UnifiedFile::Ruff(_) => unimplemented!("Expected an interned ty file"),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_notebook(&self, file: &UnifiedFile) -> bool {
|
||||
match file {
|
||||
UnifiedFile::Ty(file) => self.input(*file).text.as_notebook().is_some(),
|
||||
UnifiedFile::Ruff(_) => unimplemented!("Expected an interned ty file"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An abstraction over a unit of user input.
|
||||
@@ -724,7 +832,9 @@ fn relativize_path<'p>(cwd: &SystemPath, path: &'p str) -> &'p str {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use crate::diagnostic::{Annotation, DiagnosticId, Severity, Span};
|
||||
use ruff_diagnostics::{Edit, Fix};
|
||||
|
||||
use crate::diagnostic::{Annotation, DiagnosticId, SecondaryCode, Severity, Span};
|
||||
use crate::files::system_path_to_file;
|
||||
use crate::system::{DbWithWritableSystem, SystemPath};
|
||||
use crate::tests::TestDb;
|
||||
@@ -2121,7 +2231,7 @@ watermelon
|
||||
|
||||
/// A small harness for setting up an environment specifically for testing
|
||||
/// diagnostic rendering.
|
||||
struct TestEnvironment {
|
||||
pub(super) struct TestEnvironment {
|
||||
db: TestDb,
|
||||
config: DisplayDiagnosticConfig,
|
||||
}
|
||||
@@ -2130,7 +2240,7 @@ watermelon
|
||||
/// Create a new test harness.
|
||||
///
|
||||
/// This uses the default diagnostic rendering configuration.
|
||||
fn new() -> TestEnvironment {
|
||||
pub(super) fn new() -> TestEnvironment {
|
||||
TestEnvironment {
|
||||
db: TestDb::new(),
|
||||
config: DisplayDiagnosticConfig::default(),
|
||||
@@ -2149,8 +2259,26 @@ watermelon
|
||||
self.config = config;
|
||||
}
|
||||
|
||||
/// Set the output format to use in diagnostic rendering.
|
||||
pub(super) fn format(&mut self, format: DiagnosticFormat) {
|
||||
let mut config = std::mem::take(&mut self.config);
|
||||
config = config.format(format);
|
||||
self.config = config;
|
||||
}
|
||||
|
||||
/// Enable preview functionality for diagnostic rendering.
|
||||
#[allow(
|
||||
dead_code,
|
||||
reason = "This is currently only used for JSON but will be needed soon for other formats"
|
||||
)]
|
||||
pub(super) fn preview(&mut self, yes: bool) {
|
||||
let mut config = std::mem::take(&mut self.config);
|
||||
config = config.preview(yes);
|
||||
self.config = config;
|
||||
}
|
||||
|
||||
/// Add a file with the given path and contents to this environment.
|
||||
fn add(&mut self, path: &str, contents: &str) {
|
||||
pub(super) fn add(&mut self, path: &str, contents: &str) {
|
||||
let path = SystemPath::new(path);
|
||||
self.db.write_file(path, contents).unwrap();
|
||||
}
|
||||
@@ -2200,7 +2328,7 @@ watermelon
|
||||
/// A convenience function for returning a builder for a diagnostic
|
||||
/// with "error" severity and canned values for its identifier
|
||||
/// and message.
|
||||
fn err(&mut self) -> DiagnosticBuilder<'_> {
|
||||
pub(super) fn err(&mut self) -> DiagnosticBuilder<'_> {
|
||||
self.builder(
|
||||
"test-diagnostic",
|
||||
Severity::Error,
|
||||
@@ -2226,6 +2354,12 @@ watermelon
|
||||
DiagnosticBuilder { env: self, diag }
|
||||
}
|
||||
|
||||
/// A convenience function for returning a builder for an invalid syntax diagnostic.
|
||||
fn invalid_syntax(&mut self, message: &str) -> DiagnosticBuilder<'_> {
|
||||
let diag = Diagnostic::new(DiagnosticId::InvalidSyntax, Severity::Error, message);
|
||||
DiagnosticBuilder { env: self, diag }
|
||||
}
|
||||
|
||||
/// Returns a builder for tersely constructing sub-diagnostics.
|
||||
fn sub_builder(&mut self, severity: Severity, message: &str) -> SubDiagnosticBuilder<'_> {
|
||||
let subdiag = SubDiagnostic::new(severity, message);
|
||||
@@ -2235,9 +2369,18 @@ watermelon
|
||||
/// Render the given diagnostic into a `String`.
|
||||
///
|
||||
/// (This will set the "printed" flag on `Diagnostic`.)
|
||||
fn render(&self, diag: &Diagnostic) -> String {
|
||||
pub(super) fn render(&self, diag: &Diagnostic) -> String {
|
||||
diag.display(&self.db, &self.config).to_string()
|
||||
}
|
||||
|
||||
/// Render the given diagnostics into a `String`.
|
||||
///
|
||||
/// See `render` for rendering a single diagnostic.
|
||||
///
|
||||
/// (This will set the "printed" flag on `Diagnostic`.)
|
||||
pub(super) fn render_diagnostics(&self, diagnostics: &[Diagnostic]) -> String {
|
||||
DisplayDiagnostics::new(&self.db, &self.config, diagnostics).to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// A helper builder for tersely populating a `Diagnostic`.
|
||||
@@ -2246,14 +2389,14 @@ watermelon
|
||||
/// supported by this builder, and this only needs to be done
|
||||
/// infrequently, consider doing it more verbosely on `diag`
|
||||
/// itself.
|
||||
struct DiagnosticBuilder<'e> {
|
||||
pub(super) struct DiagnosticBuilder<'e> {
|
||||
env: &'e mut TestEnvironment,
|
||||
diag: Diagnostic,
|
||||
}
|
||||
|
||||
impl<'e> DiagnosticBuilder<'e> {
|
||||
/// Return the built diagnostic.
|
||||
fn build(self) -> Diagnostic {
|
||||
pub(super) fn build(self) -> Diagnostic {
|
||||
self.diag
|
||||
}
|
||||
|
||||
@@ -2302,6 +2445,25 @@ watermelon
|
||||
self.diag.annotate(ann);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the secondary code on the diagnostic.
|
||||
fn secondary_code(mut self, secondary_code: &str) -> DiagnosticBuilder<'e> {
|
||||
self.diag
|
||||
.set_secondary_code(SecondaryCode::new(secondary_code.to_string()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the fix on the diagnostic.
|
||||
pub(super) fn fix(mut self, fix: Fix) -> DiagnosticBuilder<'e> {
|
||||
self.diag.set_fix(fix);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the noqa offset on the diagnostic.
|
||||
fn noqa_offset(mut self, noqa_offset: TextSize) -> DiagnosticBuilder<'e> {
|
||||
self.diag.set_noqa_offset(noqa_offset);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A helper builder for tersely populating a `SubDiagnostic`.
|
||||
@@ -2381,4 +2543,199 @@ watermelon
|
||||
let offset = TextSize::from(offset.parse::<u32>().unwrap());
|
||||
(line_number, Some(offset))
|
||||
}
|
||||
|
||||
/// Create Ruff-style diagnostics for testing the various output formats.
|
||||
pub(crate) fn create_diagnostics(
|
||||
format: DiagnosticFormat,
|
||||
) -> (TestEnvironment, Vec<Diagnostic>) {
|
||||
let mut env = TestEnvironment::new();
|
||||
env.add(
|
||||
"fib.py",
|
||||
r#"import os
|
||||
|
||||
|
||||
def fibonacci(n):
|
||||
"""Compute the nth number in the Fibonacci sequence."""
|
||||
x = 1
|
||||
if n == 0:
|
||||
return 0
|
||||
elif n == 1:
|
||||
return 1
|
||||
else:
|
||||
return fibonacci(n - 1) + fibonacci(n - 2)
|
||||
"#,
|
||||
);
|
||||
env.add("undef.py", r"if a == 1: pass");
|
||||
env.format(format);
|
||||
|
||||
let diagnostics = vec![
|
||||
env.builder("unused-import", Severity::Error, "`os` imported but unused")
|
||||
.primary("fib.py", "1:7", "1:9", "Remove unused import: `os`")
|
||||
.secondary_code("F401")
|
||||
.fix(Fix::unsafe_edit(Edit::range_deletion(TextRange::new(
|
||||
TextSize::from(0),
|
||||
TextSize::from(10),
|
||||
))))
|
||||
.noqa_offset(TextSize::from(7))
|
||||
.build(),
|
||||
env.builder(
|
||||
"unused-variable",
|
||||
Severity::Error,
|
||||
"Local variable `x` is assigned to but never used",
|
||||
)
|
||||
.primary(
|
||||
"fib.py",
|
||||
"6:4",
|
||||
"6:5",
|
||||
"Remove assignment to unused variable `x`",
|
||||
)
|
||||
.secondary_code("F841")
|
||||
.fix(Fix::unsafe_edit(Edit::deletion(
|
||||
TextSize::from(94),
|
||||
TextSize::from(99),
|
||||
)))
|
||||
.noqa_offset(TextSize::from(94))
|
||||
.build(),
|
||||
env.builder("undefined-name", Severity::Error, "Undefined name `a`")
|
||||
.primary("undef.py", "1:3", "1:4", "")
|
||||
.secondary_code("F821")
|
||||
.noqa_offset(TextSize::from(3))
|
||||
.build(),
|
||||
];
|
||||
|
||||
(env, diagnostics)
|
||||
}
|
||||
|
||||
/// Create Ruff-style syntax error diagnostics for testing the various output formats.
|
||||
pub(crate) fn create_syntax_error_diagnostics(
|
||||
format: DiagnosticFormat,
|
||||
) -> (TestEnvironment, Vec<Diagnostic>) {
|
||||
let mut env = TestEnvironment::new();
|
||||
env.add(
|
||||
"syntax_errors.py",
|
||||
r"from os import
|
||||
|
||||
if call(foo
|
||||
def bar():
|
||||
pass
|
||||
",
|
||||
);
|
||||
env.format(format);
|
||||
|
||||
let diagnostics = vec![
|
||||
env.invalid_syntax("SyntaxError: Expected one or more symbol names after import")
|
||||
.primary("syntax_errors.py", "1:14", "1:15", "")
|
||||
.build(),
|
||||
env.invalid_syntax("SyntaxError: Expected ')', found newline")
|
||||
.primary("syntax_errors.py", "3:11", "3:12", "")
|
||||
.build(),
|
||||
];
|
||||
|
||||
(env, diagnostics)
|
||||
}
|
||||
|
||||
/// Create Ruff-style diagnostics for testing the various output formats for a notebook.
|
||||
#[allow(
|
||||
dead_code,
|
||||
reason = "This is currently only used for JSON but will be needed soon for other formats"
|
||||
)]
|
||||
pub(crate) fn create_notebook_diagnostics(
|
||||
format: DiagnosticFormat,
|
||||
) -> (TestEnvironment, Vec<Diagnostic>) {
|
||||
let mut env = TestEnvironment::new();
|
||||
env.add(
|
||||
"notebook.ipynb",
|
||||
r##"
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# cell 1\n",
|
||||
"import os"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# cell 2\n",
|
||||
"import math\n",
|
||||
"\n",
|
||||
"print('hello world')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# cell 3\n",
|
||||
"def foo():\n",
|
||||
" print()\n",
|
||||
" x = 1\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
"##,
|
||||
);
|
||||
env.format(format);
|
||||
|
||||
let diagnostics = vec![
|
||||
env.builder("unused-import", Severity::Error, "`os` imported but unused")
|
||||
.primary("notebook.ipynb", "2:7", "2:9", "Remove unused import: `os`")
|
||||
.secondary_code("F401")
|
||||
.fix(Fix::safe_edit(Edit::range_deletion(TextRange::new(
|
||||
TextSize::from(9),
|
||||
TextSize::from(19),
|
||||
))))
|
||||
.noqa_offset(TextSize::from(16))
|
||||
.build(),
|
||||
env.builder(
|
||||
"unused-import",
|
||||
Severity::Error,
|
||||
"`math` imported but unused",
|
||||
)
|
||||
.primary(
|
||||
"notebook.ipynb",
|
||||
"4:7",
|
||||
"4:11",
|
||||
"Remove unused import: `math`",
|
||||
)
|
||||
.secondary_code("F401")
|
||||
.fix(Fix::safe_edit(Edit::range_deletion(TextRange::new(
|
||||
TextSize::from(28),
|
||||
TextSize::from(40),
|
||||
))))
|
||||
.noqa_offset(TextSize::from(35))
|
||||
.build(),
|
||||
env.builder(
|
||||
"unused-variable",
|
||||
Severity::Error,
|
||||
"Local variable `x` is assigned to but never used",
|
||||
)
|
||||
.primary(
|
||||
"notebook.ipynb",
|
||||
"10:4",
|
||||
"10:5",
|
||||
"Remove assignment to unused variable `x`",
|
||||
)
|
||||
.secondary_code("F841")
|
||||
.fix(Fix::unsafe_edit(Edit::range_deletion(TextRange::new(
|
||||
TextSize::from(94),
|
||||
TextSize::from(104),
|
||||
))))
|
||||
.noqa_offset(TextSize::from(98))
|
||||
.build(),
|
||||
];
|
||||
|
||||
(env, diagnostics)
|
||||
}
|
||||
}
|
||||
|
||||
83
crates/ruff_db/src/diagnostic/render/azure.rs
Normal file
83
crates/ruff_db/src/diagnostic/render/azure.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
use ruff_source_file::LineColumn;
|
||||
|
||||
use crate::diagnostic::{Diagnostic, Severity};
|
||||
|
||||
use super::FileResolver;
|
||||
|
||||
pub(super) struct AzureRenderer<'a> {
|
||||
resolver: &'a dyn FileResolver,
|
||||
}
|
||||
|
||||
impl<'a> AzureRenderer<'a> {
|
||||
pub(super) fn new(resolver: &'a dyn FileResolver) -> Self {
|
||||
Self { resolver }
|
||||
}
|
||||
}
|
||||
|
||||
impl AzureRenderer<'_> {
|
||||
pub(super) fn render(
|
||||
&self,
|
||||
f: &mut std::fmt::Formatter,
|
||||
diagnostics: &[Diagnostic],
|
||||
) -> std::fmt::Result {
|
||||
for diag in diagnostics {
|
||||
let severity = match diag.severity() {
|
||||
Severity::Info | Severity::Warning => "warning",
|
||||
Severity::Error | Severity::Fatal => "error",
|
||||
};
|
||||
write!(f, "##vso[task.logissue type={severity};")?;
|
||||
if let Some(span) = diag.primary_span() {
|
||||
let filename = span.file().path(self.resolver);
|
||||
write!(f, "sourcepath={filename};")?;
|
||||
if let Some(range) = span.range() {
|
||||
let location = if self.resolver.notebook_index(span.file()).is_some() {
|
||||
// We can't give a reasonable location for the structured formats,
|
||||
// so we show one that's clearly a fallback
|
||||
LineColumn::default()
|
||||
} else {
|
||||
span.file()
|
||||
.diagnostic_source(self.resolver)
|
||||
.as_source_code()
|
||||
.line_column(range.start())
|
||||
};
|
||||
write!(
|
||||
f,
|
||||
"linenumber={line};columnnumber={col};",
|
||||
line = location.line,
|
||||
col = location.column,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
writeln!(
|
||||
f,
|
||||
"{code}]{body}",
|
||||
code = diag
|
||||
.secondary_code()
|
||||
.map_or_else(String::new, |code| format!("code={code};")),
|
||||
body = diag.body(),
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::diagnostic::{
|
||||
DiagnosticFormat,
|
||||
render::tests::{create_diagnostics, create_syntax_error_diagnostics},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn output() {
|
||||
let (env, diagnostics) = create_diagnostics(DiagnosticFormat::Azure);
|
||||
insta::assert_snapshot!(env.render_diagnostics(&diagnostics));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn syntax_errors() {
|
||||
let (env, diagnostics) = create_syntax_error_diagnostics(DiagnosticFormat::Azure);
|
||||
insta::assert_snapshot!(env.render_diagnostics(&diagnostics));
|
||||
}
|
||||
}
|
||||
393
crates/ruff_db/src/diagnostic/render/json.rs
Normal file
393
crates/ruff_db/src/diagnostic/render/json.rs
Normal file
@@ -0,0 +1,393 @@
|
||||
use serde::{Serialize, Serializer, ser::SerializeSeq};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use ruff_diagnostics::{Applicability, Edit};
|
||||
use ruff_notebook::NotebookIndex;
|
||||
use ruff_source_file::{LineColumn, OneIndexed};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::diagnostic::{Diagnostic, DiagnosticSource, DisplayDiagnosticConfig, SecondaryCode};
|
||||
|
||||
use super::FileResolver;
|
||||
|
||||
pub(super) struct JsonRenderer<'a> {
|
||||
resolver: &'a dyn FileResolver,
|
||||
config: &'a DisplayDiagnosticConfig,
|
||||
}
|
||||
|
||||
impl<'a> JsonRenderer<'a> {
|
||||
pub(super) fn new(resolver: &'a dyn FileResolver, config: &'a DisplayDiagnosticConfig) -> Self {
|
||||
Self { resolver, config }
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonRenderer<'_> {
|
||||
pub(super) fn render(
|
||||
&self,
|
||||
f: &mut std::fmt::Formatter,
|
||||
diagnostics: &[Diagnostic],
|
||||
) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{:#}",
|
||||
diagnostics_to_json_value(diagnostics, self.resolver, self.config)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn diagnostics_to_json_value<'a>(
|
||||
diagnostics: impl IntoIterator<Item = &'a Diagnostic>,
|
||||
resolver: &dyn FileResolver,
|
||||
config: &DisplayDiagnosticConfig,
|
||||
) -> Value {
|
||||
let values: Vec<_> = diagnostics
|
||||
.into_iter()
|
||||
.map(|diag| diagnostic_to_json(diag, resolver, config))
|
||||
.collect();
|
||||
json!(values)
|
||||
}
|
||||
|
||||
pub(super) fn diagnostic_to_json<'a>(
|
||||
diagnostic: &'a Diagnostic,
|
||||
resolver: &'a dyn FileResolver,
|
||||
config: &'a DisplayDiagnosticConfig,
|
||||
) -> JsonDiagnostic<'a> {
|
||||
let span = diagnostic.primary_span_ref();
|
||||
let filename = span.map(|span| span.file().path(resolver));
|
||||
let range = span.and_then(|span| span.range());
|
||||
let diagnostic_source = span.map(|span| span.file().diagnostic_source(resolver));
|
||||
let source_code = diagnostic_source
|
||||
.as_ref()
|
||||
.map(|diagnostic_source| diagnostic_source.as_source_code());
|
||||
let notebook_index = span.and_then(|span| resolver.notebook_index(span.file()));
|
||||
|
||||
let mut start_location = None;
|
||||
let mut end_location = None;
|
||||
let mut noqa_location = None;
|
||||
let mut notebook_cell_index = None;
|
||||
if let Some(source_code) = source_code {
|
||||
noqa_location = diagnostic
|
||||
.noqa_offset()
|
||||
.map(|offset| source_code.line_column(offset));
|
||||
if let Some(range) = range {
|
||||
let mut start = source_code.line_column(range.start());
|
||||
let mut end = source_code.line_column(range.end());
|
||||
if let Some(notebook_index) = ¬ebook_index {
|
||||
notebook_cell_index =
|
||||
Some(notebook_index.cell(start.line).unwrap_or(OneIndexed::MIN));
|
||||
start = notebook_index.translate_line_column(&start);
|
||||
end = notebook_index.translate_line_column(&end);
|
||||
noqa_location =
|
||||
noqa_location.map(|location| notebook_index.translate_line_column(&location));
|
||||
}
|
||||
start_location = Some(start);
|
||||
end_location = Some(end);
|
||||
}
|
||||
}
|
||||
|
||||
let fix = diagnostic.fix().map(|fix| JsonFix {
|
||||
applicability: fix.applicability(),
|
||||
message: diagnostic.suggestion(),
|
||||
edits: ExpandedEdits {
|
||||
edits: fix.edits(),
|
||||
notebook_index,
|
||||
config,
|
||||
diagnostic_source,
|
||||
},
|
||||
});
|
||||
|
||||
// In preview, the locations and filename can be optional.
|
||||
if config.preview {
|
||||
JsonDiagnostic {
|
||||
code: diagnostic.secondary_code(),
|
||||
url: diagnostic.to_ruff_url(),
|
||||
message: diagnostic.body(),
|
||||
fix,
|
||||
cell: notebook_cell_index,
|
||||
location: start_location.map(JsonLocation::from),
|
||||
end_location: end_location.map(JsonLocation::from),
|
||||
filename,
|
||||
noqa_row: noqa_location.map(|location| location.line),
|
||||
}
|
||||
} else {
|
||||
JsonDiagnostic {
|
||||
code: diagnostic.secondary_code(),
|
||||
url: diagnostic.to_ruff_url(),
|
||||
message: diagnostic.body(),
|
||||
fix,
|
||||
cell: notebook_cell_index,
|
||||
location: Some(start_location.unwrap_or_default().into()),
|
||||
end_location: Some(end_location.unwrap_or_default().into()),
|
||||
filename: Some(filename.unwrap_or_default()),
|
||||
noqa_row: noqa_location.map(|location| location.line),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ExpandedEdits<'a> {
|
||||
edits: &'a [Edit],
|
||||
notebook_index: Option<NotebookIndex>,
|
||||
config: &'a DisplayDiagnosticConfig,
|
||||
diagnostic_source: Option<DiagnosticSource>,
|
||||
}
|
||||
|
||||
impl Serialize for ExpandedEdits<'_> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut s = serializer.serialize_seq(Some(self.edits.len()))?;
|
||||
|
||||
for edit in self.edits {
|
||||
let (location, end_location) = if let Some(diagnostic_source) = &self.diagnostic_source
|
||||
{
|
||||
let source_code = diagnostic_source.as_source_code();
|
||||
let mut location = source_code.line_column(edit.start());
|
||||
let mut end_location = source_code.line_column(edit.end());
|
||||
|
||||
if let Some(notebook_index) = &self.notebook_index {
|
||||
// There exists a newline between each cell's source code in the
|
||||
// concatenated source code in Ruff. This newline doesn't actually
|
||||
// exists in the JSON source field.
|
||||
//
|
||||
// Now, certain edits may try to remove this newline, which means
|
||||
// the edit will spill over to the first character of the next cell.
|
||||
// If it does, we need to translate the end location to the last
|
||||
// character of the previous cell.
|
||||
match (
|
||||
notebook_index.cell(location.line),
|
||||
notebook_index.cell(end_location.line),
|
||||
) {
|
||||
(Some(start_cell), Some(end_cell)) if start_cell != end_cell => {
|
||||
debug_assert_eq!(end_location.column.get(), 1);
|
||||
|
||||
let prev_row = end_location.line.saturating_sub(1);
|
||||
end_location = LineColumn {
|
||||
line: notebook_index.cell_row(prev_row).unwrap_or(OneIndexed::MIN),
|
||||
column: source_code
|
||||
.line_column(source_code.line_end_exclusive(prev_row))
|
||||
.column,
|
||||
};
|
||||
}
|
||||
(Some(_), None) => {
|
||||
debug_assert_eq!(end_location.column.get(), 1);
|
||||
|
||||
let prev_row = end_location.line.saturating_sub(1);
|
||||
end_location = LineColumn {
|
||||
line: notebook_index.cell_row(prev_row).unwrap_or(OneIndexed::MIN),
|
||||
column: source_code
|
||||
.line_column(source_code.line_end_exclusive(prev_row))
|
||||
.column,
|
||||
};
|
||||
}
|
||||
_ => {
|
||||
end_location = notebook_index.translate_line_column(&end_location);
|
||||
}
|
||||
}
|
||||
location = notebook_index.translate_line_column(&location);
|
||||
}
|
||||
|
||||
(Some(location), Some(end_location))
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
// In preview, the locations can be optional.
|
||||
let value = if self.config.preview {
|
||||
JsonEdit {
|
||||
content: edit.content().unwrap_or_default(),
|
||||
location: location.map(JsonLocation::from),
|
||||
end_location: end_location.map(JsonLocation::from),
|
||||
}
|
||||
} else {
|
||||
JsonEdit {
|
||||
content: edit.content().unwrap_or_default(),
|
||||
location: Some(location.unwrap_or_default().into()),
|
||||
end_location: Some(end_location.unwrap_or_default().into()),
|
||||
}
|
||||
};
|
||||
|
||||
s.serialize_element(&value)?;
|
||||
}
|
||||
|
||||
s.end()
|
||||
}
|
||||
}
|
||||
|
||||
/// A serializable version of `Diagnostic`.
|
||||
///
|
||||
/// The `Old` variant only exists to preserve backwards compatibility. Both this and `JsonEdit`
|
||||
/// should become structs with the `New` definitions in a future Ruff release.
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct JsonDiagnostic<'a> {
|
||||
cell: Option<OneIndexed>,
|
||||
code: Option<&'a SecondaryCode>,
|
||||
end_location: Option<JsonLocation>,
|
||||
filename: Option<&'a str>,
|
||||
fix: Option<JsonFix<'a>>,
|
||||
location: Option<JsonLocation>,
|
||||
message: &'a str,
|
||||
noqa_row: Option<OneIndexed>,
|
||||
url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct JsonFix<'a> {
|
||||
applicability: Applicability,
|
||||
edits: ExpandedEdits<'a>,
|
||||
message: Option<&'a str>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct JsonLocation {
|
||||
column: OneIndexed,
|
||||
row: OneIndexed,
|
||||
}
|
||||
|
||||
impl From<LineColumn> for JsonLocation {
|
||||
fn from(location: LineColumn) -> Self {
|
||||
JsonLocation {
|
||||
row: location.line,
|
||||
column: location.column,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct JsonEdit<'a> {
|
||||
content: &'a str,
|
||||
end_location: Option<JsonLocation>,
|
||||
location: Option<JsonLocation>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruff_diagnostics::{Edit, Fix};
|
||||
use ruff_text_size::TextSize;
|
||||
|
||||
use crate::diagnostic::{
|
||||
DiagnosticFormat,
|
||||
render::tests::{
|
||||
TestEnvironment, create_diagnostics, create_notebook_diagnostics,
|
||||
create_syntax_error_diagnostics,
|
||||
},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn output() {
|
||||
let (env, diagnostics) = create_diagnostics(DiagnosticFormat::Json);
|
||||
insta::assert_snapshot!(env.render_diagnostics(&diagnostics));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn syntax_errors() {
|
||||
let (env, diagnostics) = create_syntax_error_diagnostics(DiagnosticFormat::Json);
|
||||
insta::assert_snapshot!(env.render_diagnostics(&diagnostics));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn notebook_output() {
|
||||
let (env, diagnostics) = create_notebook_diagnostics(DiagnosticFormat::Json);
|
||||
insta::assert_snapshot!(env.render_diagnostics(&diagnostics));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_file_stable() {
|
||||
let mut env = TestEnvironment::new();
|
||||
env.format(DiagnosticFormat::Json);
|
||||
env.preview(false);
|
||||
|
||||
let diag = env
|
||||
.err()
|
||||
.fix(Fix::safe_edit(Edit::insertion(
|
||||
"edit".to_string(),
|
||||
TextSize::from(0),
|
||||
)))
|
||||
.build();
|
||||
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r#"
|
||||
[
|
||||
{
|
||||
"cell": null,
|
||||
"code": null,
|
||||
"end_location": {
|
||||
"column": 1,
|
||||
"row": 1
|
||||
},
|
||||
"filename": "",
|
||||
"fix": {
|
||||
"applicability": "safe",
|
||||
"edits": [
|
||||
{
|
||||
"content": "edit",
|
||||
"end_location": {
|
||||
"column": 1,
|
||||
"row": 1
|
||||
},
|
||||
"location": {
|
||||
"column": 1,
|
||||
"row": 1
|
||||
}
|
||||
}
|
||||
],
|
||||
"message": null
|
||||
},
|
||||
"location": {
|
||||
"column": 1,
|
||||
"row": 1
|
||||
},
|
||||
"message": "main diagnostic message",
|
||||
"noqa_row": null,
|
||||
"url": "https://docs.astral.sh/ruff/rules/test-diagnostic"
|
||||
}
|
||||
]
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_file_preview() {
|
||||
let mut env = TestEnvironment::new();
|
||||
env.format(DiagnosticFormat::Json);
|
||||
env.preview(true);
|
||||
|
||||
let diag = env
|
||||
.err()
|
||||
.fix(Fix::safe_edit(Edit::insertion(
|
||||
"edit".to_string(),
|
||||
TextSize::from(0),
|
||||
)))
|
||||
.build();
|
||||
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@r#"
|
||||
[
|
||||
{
|
||||
"cell": null,
|
||||
"code": null,
|
||||
"end_location": null,
|
||||
"filename": null,
|
||||
"fix": {
|
||||
"applicability": "safe",
|
||||
"edits": [
|
||||
{
|
||||
"content": "edit",
|
||||
"end_location": null,
|
||||
"location": null
|
||||
}
|
||||
],
|
||||
"message": null
|
||||
},
|
||||
"location": null,
|
||||
"message": "main diagnostic message",
|
||||
"noqa_row": null,
|
||||
"url": "https://docs.astral.sh/ruff/rules/test-diagnostic"
|
||||
}
|
||||
]
|
||||
"#,
|
||||
);
|
||||
}
|
||||
}
|
||||
59
crates/ruff_db/src/diagnostic/render/json_lines.rs
Normal file
59
crates/ruff_db/src/diagnostic/render/json_lines.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use crate::diagnostic::{Diagnostic, DisplayDiagnosticConfig, render::json::diagnostic_to_json};
|
||||
|
||||
use super::FileResolver;
|
||||
|
||||
pub(super) struct JsonLinesRenderer<'a> {
|
||||
resolver: &'a dyn FileResolver,
|
||||
config: &'a DisplayDiagnosticConfig,
|
||||
}
|
||||
|
||||
impl<'a> JsonLinesRenderer<'a> {
|
||||
pub(super) fn new(resolver: &'a dyn FileResolver, config: &'a DisplayDiagnosticConfig) -> Self {
|
||||
Self { resolver, config }
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonLinesRenderer<'_> {
|
||||
pub(super) fn render(
|
||||
&self,
|
||||
f: &mut std::fmt::Formatter,
|
||||
diagnostics: &[Diagnostic],
|
||||
) -> std::fmt::Result {
|
||||
for diag in diagnostics {
|
||||
writeln!(
|
||||
f,
|
||||
"{}",
|
||||
serde_json::json!(diagnostic_to_json(diag, self.resolver, self.config))
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::diagnostic::{
|
||||
DiagnosticFormat,
|
||||
render::tests::{
|
||||
create_diagnostics, create_notebook_diagnostics, create_syntax_error_diagnostics,
|
||||
},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn output() {
|
||||
let (env, diagnostics) = create_diagnostics(DiagnosticFormat::JsonLines);
|
||||
insta::assert_snapshot!(env.render_diagnostics(&diagnostics));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn syntax_errors() {
|
||||
let (env, diagnostics) = create_syntax_error_diagnostics(DiagnosticFormat::JsonLines);
|
||||
insta::assert_snapshot!(env.render_diagnostics(&diagnostics));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn notebook_output() {
|
||||
let (env, diagnostics) = create_notebook_diagnostics(DiagnosticFormat::JsonLines);
|
||||
insta::assert_snapshot!(env.render_diagnostics(&diagnostics));
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/message/azure.rs
|
||||
expression: content
|
||||
snapshot_kind: text
|
||||
source: crates/ruff_db/src/diagnostic/render/azure.rs
|
||||
expression: env.render_diagnostics(&diagnostics)
|
||||
---
|
||||
##vso[task.logissue type=error;sourcepath=fib.py;linenumber=1;columnnumber=8;code=F401;]`os` imported but unused
|
||||
##vso[task.logissue type=error;sourcepath=fib.py;linenumber=6;columnnumber=5;code=F841;]Local variable `x` is assigned to but never used
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/message/azure.rs
|
||||
expression: content
|
||||
snapshot_kind: text
|
||||
source: crates/ruff_db/src/diagnostic/render/azure.rs
|
||||
expression: env.render_diagnostics(&diagnostics)
|
||||
---
|
||||
##vso[task.logissue type=error;sourcepath=syntax_errors.py;linenumber=1;columnnumber=15;]SyntaxError: Expected one or more symbol names after import
|
||||
##vso[task.logissue type=error;sourcepath=syntax_errors.py;linenumber=3;columnnumber=12;]SyntaxError: Expected ')', found newline
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/message/json.rs
|
||||
expression: content
|
||||
snapshot_kind: text
|
||||
source: crates/ruff_db/src/diagnostic/render/json.rs
|
||||
expression: env.render_diagnostics(&diagnostics)
|
||||
---
|
||||
[
|
||||
{
|
||||
@@ -84,8 +83,8 @@ snapshot_kind: text
|
||||
{
|
||||
"content": "",
|
||||
"end_location": {
|
||||
"column": 10,
|
||||
"row": 4
|
||||
"column": 1,
|
||||
"row": 5
|
||||
},
|
||||
"location": {
|
||||
"column": 1,
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/message/json.rs
|
||||
expression: content
|
||||
snapshot_kind: text
|
||||
source: crates/ruff_db/src/diagnostic/render/json.rs
|
||||
expression: env.render_diagnostics(&diagnostics)
|
||||
---
|
||||
[
|
||||
{
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/message/json.rs
|
||||
expression: content
|
||||
snapshot_kind: text
|
||||
source: crates/ruff_db/src/diagnostic/render/json.rs
|
||||
expression: env.render_diagnostics(&diagnostics)
|
||||
---
|
||||
[
|
||||
{
|
||||
@@ -1,8 +1,7 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/message/json_lines.rs
|
||||
expression: content
|
||||
snapshot_kind: text
|
||||
source: crates/ruff_db/src/diagnostic/render/json_lines.rs
|
||||
expression: env.render_diagnostics(&diagnostics)
|
||||
---
|
||||
{"cell":1,"code":"F401","end_location":{"column":10,"row":2},"filename":"notebook.ipynb","fix":{"applicability":"safe","edits":[{"content":"","end_location":{"column":10,"row":2},"location":{"column":1,"row":2}}],"message":"Remove unused import: `os`"},"location":{"column":8,"row":2},"message":"`os` imported but unused","noqa_row":2,"url":"https://docs.astral.sh/ruff/rules/unused-import"}
|
||||
{"cell":2,"code":"F401","end_location":{"column":12,"row":2},"filename":"notebook.ipynb","fix":{"applicability":"safe","edits":[{"content":"","end_location":{"column":1,"row":3},"location":{"column":1,"row":2}}],"message":"Remove unused import: `math`"},"location":{"column":8,"row":2},"message":"`math` imported but unused","noqa_row":2,"url":"https://docs.astral.sh/ruff/rules/unused-import"}
|
||||
{"cell":3,"code":"F841","end_location":{"column":6,"row":4},"filename":"notebook.ipynb","fix":{"applicability":"unsafe","edits":[{"content":"","end_location":{"column":10,"row":4},"location":{"column":1,"row":4}}],"message":"Remove assignment to unused variable `x`"},"location":{"column":5,"row":4},"message":"Local variable `x` is assigned to but never used","noqa_row":4,"url":"https://docs.astral.sh/ruff/rules/unused-variable"}
|
||||
{"cell":3,"code":"F841","end_location":{"column":6,"row":4},"filename":"notebook.ipynb","fix":{"applicability":"unsafe","edits":[{"content":"","end_location":{"column":1,"row":5},"location":{"column":1,"row":4}}],"message":"Remove assignment to unused variable `x`"},"location":{"column":5,"row":4},"message":"Local variable `x` is assigned to but never used","noqa_row":4,"url":"https://docs.astral.sh/ruff/rules/unused-variable"}
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/message/json_lines.rs
|
||||
expression: content
|
||||
snapshot_kind: text
|
||||
source: crates/ruff_db/src/diagnostic/render/json_lines.rs
|
||||
expression: env.render_diagnostics(&diagnostics)
|
||||
---
|
||||
{"cell":null,"code":"F401","end_location":{"column":10,"row":1},"filename":"fib.py","fix":{"applicability":"unsafe","edits":[{"content":"","end_location":{"column":1,"row":2},"location":{"column":1,"row":1}}],"message":"Remove unused import: `os`"},"location":{"column":8,"row":1},"message":"`os` imported but unused","noqa_row":1,"url":"https://docs.astral.sh/ruff/rules/unused-import"}
|
||||
{"cell":null,"code":"F841","end_location":{"column":6,"row":6},"filename":"fib.py","fix":{"applicability":"unsafe","edits":[{"content":"","end_location":{"column":10,"row":6},"location":{"column":5,"row":6}}],"message":"Remove assignment to unused variable `x`"},"location":{"column":5,"row":6},"message":"Local variable `x` is assigned to but never used","noqa_row":6,"url":"https://docs.astral.sh/ruff/rules/unused-variable"}
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/message/json_lines.rs
|
||||
expression: content
|
||||
snapshot_kind: text
|
||||
source: crates/ruff_db/src/diagnostic/render/json_lines.rs
|
||||
expression: env.render_diagnostics(&diagnostics)
|
||||
---
|
||||
{"cell":null,"code":null,"end_location":{"column":1,"row":2},"filename":"syntax_errors.py","fix":null,"location":{"column":15,"row":1},"message":"SyntaxError: Expected one or more symbol names after import","noqa_row":null,"url":null}
|
||||
{"cell":null,"code":null,"end_location":{"column":1,"row":4},"filename":"syntax_errors.py","fix":null,"location":{"column":12,"row":3},"message":"SyntaxError: Expected ')', found newline","noqa_row":null,"url":null}
|
||||
@@ -28,6 +28,21 @@ pub use web_time::{Instant, SystemTime, SystemTimeError};
|
||||
pub type FxDashMap<K, V> = dashmap::DashMap<K, V, BuildHasherDefault<FxHasher>>;
|
||||
pub type FxDashSet<K> = dashmap::DashSet<K, BuildHasherDefault<FxHasher>>;
|
||||
|
||||
static VERSION: std::sync::OnceLock<String> = std::sync::OnceLock::new();
|
||||
|
||||
/// Returns the version of the executing program if set.
|
||||
pub fn program_version() -> Option<&'static str> {
|
||||
VERSION.get().map(|version| version.as_str())
|
||||
}
|
||||
|
||||
/// Sets the version of the executing program.
|
||||
///
|
||||
/// ## Errors
|
||||
/// If the version has already been initialized (can only be set once).
|
||||
pub fn set_program_version(version: String) -> Result<(), String> {
|
||||
VERSION.set(version)
|
||||
}
|
||||
|
||||
/// Most basic database that gives access to files, the host system, source code, and parsed AST.
|
||||
#[salsa::db]
|
||||
pub trait Db: salsa::Database {
|
||||
|
||||
@@ -21,6 +21,19 @@ type LockedZipArchive<'a> = MutexGuard<'a, VendoredZipArchive>;
|
||||
///
|
||||
/// "Files" in the `VendoredFileSystem` are read-only and immutable.
|
||||
/// Directories are supported, but symlinks and hardlinks cannot exist.
|
||||
///
|
||||
/// # Path separators
|
||||
///
|
||||
/// At time of writing (2025-07-11), this implementation always uses `/` as a
|
||||
/// path separator, even in Windows environments where `\` is traditionally
|
||||
/// used as a file path separator. Namely, this is only currently used with zip
|
||||
/// files built by `crates/ty_vendored/build.rs`.
|
||||
///
|
||||
/// Callers using this may provide paths that use a `\` as a separator. It will
|
||||
/// be transparently normalized to `/`.
|
||||
///
|
||||
/// This is particularly important because the presence of a trailing separator
|
||||
/// in a zip file is conventionally used to indicate a directory entry.
|
||||
#[derive(Clone)]
|
||||
pub struct VendoredFileSystem {
|
||||
inner: Arc<Mutex<VendoredZipArchive>>,
|
||||
@@ -115,6 +128,68 @@ impl VendoredFileSystem {
|
||||
read_to_string(self, path.as_ref())
|
||||
}
|
||||
|
||||
/// Read the direct children of the directory
|
||||
/// identified by `path`.
|
||||
///
|
||||
/// If `path` is not a directory, then this will
|
||||
/// return an empty `Vec`.
|
||||
pub fn read_directory(&self, dir: impl AsRef<VendoredPath>) -> Vec<DirectoryEntry> {
|
||||
// N.B. We specifically do not return an iterator here to avoid
|
||||
// holding a lock for the lifetime of the iterator returned.
|
||||
// That is, it seems like a footgun to keep the zip archive
|
||||
// locked during iteration, since the unit of work for each
|
||||
// item in the iterator could be arbitrarily long. Allocating
|
||||
// up front and stuffing all entries into it is probably the
|
||||
// simplest solution and what we do here. If this becomes
|
||||
// a problem, there are other strategies we could pursue.
|
||||
// (Amortizing allocs, using a different synchronization
|
||||
// behavior or even exposing additional APIs.) ---AG
|
||||
|
||||
fn read_directory(fs: &VendoredFileSystem, dir: &VendoredPath) -> Vec<DirectoryEntry> {
|
||||
let mut normalized = NormalizedVendoredPath::from(dir);
|
||||
if !normalized.as_str().ends_with('/') {
|
||||
normalized = normalized.with_trailing_slash();
|
||||
}
|
||||
let archive = fs.lock_archive();
|
||||
let mut entries = vec![];
|
||||
for name in archive.0.file_names() {
|
||||
// Any entry that doesn't have the `path` (with a
|
||||
// trailing slash) as a prefix cannot possibly be in
|
||||
// the directory referenced by `path`.
|
||||
let Some(without_dir_prefix) = name.strip_prefix(normalized.as_str()) else {
|
||||
continue;
|
||||
};
|
||||
// Filter out an entry equivalent to the path given
|
||||
// since we only want children of the directory.
|
||||
if without_dir_prefix.is_empty() {
|
||||
continue;
|
||||
}
|
||||
// We only want *direct* children. Files that are
|
||||
// direct children cannot have any slashes (or else
|
||||
// they are not direct children). Directories that
|
||||
// are direct children can only have one slash and
|
||||
// it must be at the end.
|
||||
//
|
||||
// (We do this manually ourselves to avoid doing a
|
||||
// full file lookup and metadata retrieval via the
|
||||
// `zip` crate.)
|
||||
let file_type = FileType::from_zip_file_name(without_dir_prefix);
|
||||
let slash_count = without_dir_prefix.matches('/').count();
|
||||
match file_type {
|
||||
FileType::File if slash_count > 0 => continue,
|
||||
FileType::Directory if slash_count > 1 => continue,
|
||||
_ => {}
|
||||
}
|
||||
entries.push(DirectoryEntry {
|
||||
path: VendoredPathBuf::from(name),
|
||||
file_type,
|
||||
});
|
||||
}
|
||||
entries
|
||||
}
|
||||
read_directory(self, dir.as_ref())
|
||||
}
|
||||
|
||||
/// Acquire a lock on the underlying zip archive.
|
||||
/// The call will block until it is able to acquire the lock.
|
||||
///
|
||||
@@ -206,6 +281,14 @@ pub enum FileType {
|
||||
}
|
||||
|
||||
impl FileType {
|
||||
fn from_zip_file_name(name: &str) -> FileType {
|
||||
if name.ends_with('/') {
|
||||
FileType::Directory
|
||||
} else {
|
||||
FileType::File
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn is_file(self) -> bool {
|
||||
matches!(self, Self::File)
|
||||
}
|
||||
@@ -244,6 +327,30 @@ impl Metadata {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct DirectoryEntry {
|
||||
path: VendoredPathBuf,
|
||||
file_type: FileType,
|
||||
}
|
||||
|
||||
impl DirectoryEntry {
|
||||
pub fn new(path: VendoredPathBuf, file_type: FileType) -> Self {
|
||||
Self { path, file_type }
|
||||
}
|
||||
|
||||
pub fn into_path(self) -> VendoredPathBuf {
|
||||
self.path
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &VendoredPath {
|
||||
&self.path
|
||||
}
|
||||
|
||||
pub fn file_type(&self) -> FileType {
|
||||
self.file_type
|
||||
}
|
||||
}
|
||||
|
||||
/// Newtype wrapper around a ZipArchive.
|
||||
#[derive(Debug)]
|
||||
struct VendoredZipArchive(ZipArchive<io::Cursor<Cow<'static, [u8]>>>);
|
||||
@@ -498,6 +605,60 @@ pub(crate) mod tests {
|
||||
test_directory("./stdlib/asyncio/../asyncio/")
|
||||
}
|
||||
|
||||
fn readdir_snapshot(fs: &VendoredFileSystem, path: &str) -> String {
|
||||
let mut paths = fs
|
||||
.read_directory(VendoredPath::new(path))
|
||||
.into_iter()
|
||||
.map(|entry| entry.path().to_string())
|
||||
.collect::<Vec<String>>();
|
||||
paths.sort();
|
||||
paths.join("\n")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_directory_stdlib() {
|
||||
let mock_typeshed = mock_typeshed();
|
||||
|
||||
assert_snapshot!(readdir_snapshot(&mock_typeshed, "stdlib"), @r"
|
||||
vendored://stdlib/asyncio/
|
||||
vendored://stdlib/functools.pyi
|
||||
");
|
||||
assert_snapshot!(readdir_snapshot(&mock_typeshed, "stdlib/"), @r"
|
||||
vendored://stdlib/asyncio/
|
||||
vendored://stdlib/functools.pyi
|
||||
");
|
||||
assert_snapshot!(readdir_snapshot(&mock_typeshed, "./stdlib"), @r"
|
||||
vendored://stdlib/asyncio/
|
||||
vendored://stdlib/functools.pyi
|
||||
");
|
||||
assert_snapshot!(readdir_snapshot(&mock_typeshed, "./stdlib/"), @r"
|
||||
vendored://stdlib/asyncio/
|
||||
vendored://stdlib/functools.pyi
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_directory_asyncio() {
|
||||
let mock_typeshed = mock_typeshed();
|
||||
|
||||
assert_snapshot!(
|
||||
readdir_snapshot(&mock_typeshed, "stdlib/asyncio"),
|
||||
@"vendored://stdlib/asyncio/tasks.pyi",
|
||||
);
|
||||
assert_snapshot!(
|
||||
readdir_snapshot(&mock_typeshed, "./stdlib/asyncio"),
|
||||
@"vendored://stdlib/asyncio/tasks.pyi",
|
||||
);
|
||||
assert_snapshot!(
|
||||
readdir_snapshot(&mock_typeshed, "stdlib/asyncio/"),
|
||||
@"vendored://stdlib/asyncio/tasks.pyi",
|
||||
);
|
||||
assert_snapshot!(
|
||||
readdir_snapshot(&mock_typeshed, "./stdlib/asyncio/"),
|
||||
@"vendored://stdlib/asyncio/tasks.pyi",
|
||||
);
|
||||
}
|
||||
|
||||
fn test_nonexistent_path(path: &str) {
|
||||
let mock_typeshed = mock_typeshed();
|
||||
let path = VendoredPath::new(path);
|
||||
|
||||
@@ -17,6 +17,10 @@ impl VendoredPath {
|
||||
unsafe { &*(path as *const Utf8Path as *const VendoredPath) }
|
||||
}
|
||||
|
||||
pub fn file_name(&self) -> Option<&str> {
|
||||
self.0.file_name()
|
||||
}
|
||||
|
||||
pub fn to_path_buf(&self) -> VendoredPathBuf {
|
||||
VendoredPathBuf(self.0.to_path_buf())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.12.2"
|
||||
version = "0.12.3"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""
|
||||
Should emit:
|
||||
B017 - on lines 24, 28, 46, 49, 52, and 58
|
||||
B017 - on lines 24, 28, 46, 49, 52, 58, 62, 68, and 71
|
||||
"""
|
||||
import asyncio
|
||||
import unittest
|
||||
@@ -56,3 +56,17 @@ def test_pytest_raises():
|
||||
|
||||
with contextlib.nullcontext(), pytest.raises(Exception):
|
||||
raise ValueError("Multiple context managers")
|
||||
|
||||
|
||||
def test_pytest_raises_keyword():
|
||||
with pytest.raises(expected_exception=Exception):
|
||||
raise ValueError("Should be flagged")
|
||||
|
||||
def test_assert_raises_keyword():
|
||||
class TestKwargs(unittest.TestCase):
|
||||
def test_method(self):
|
||||
with self.assertRaises(exception=Exception):
|
||||
raise ValueError("Should be flagged")
|
||||
|
||||
with self.assertRaises(exception=BaseException):
|
||||
raise ValueError("Should be flagged")
|
||||
|
||||
@@ -181,3 +181,51 @@ class SubclassTestModel2(TestModel4):
|
||||
# Subclass without __str__
|
||||
class SubclassTestModel3(TestModel1):
|
||||
pass
|
||||
|
||||
|
||||
# Test cases for type-annotated abstract models - these should NOT trigger DJ008
|
||||
from typing import ClassVar
|
||||
from django_stubs_ext.db.models import TypedModelMeta
|
||||
|
||||
|
||||
class TypeAnnotatedAbstractModel1(models.Model):
|
||||
"""Model with type-annotated abstract = True - should not trigger DJ008"""
|
||||
new_field = models.CharField(max_length=10)
|
||||
|
||||
class Meta(TypedModelMeta):
|
||||
abstract: ClassVar[bool] = True
|
||||
|
||||
|
||||
class TypeAnnotatedAbstractModel2(models.Model):
|
||||
"""Model with type-annotated abstract = True using regular Meta - should not trigger DJ008"""
|
||||
new_field = models.CharField(max_length=10)
|
||||
|
||||
class Meta:
|
||||
abstract: ClassVar[bool] = True
|
||||
|
||||
|
||||
class TypeAnnotatedAbstractModel3(models.Model):
|
||||
"""Model with type-annotated abstract = True but without ClassVar - should not trigger DJ008"""
|
||||
new_field = models.CharField(max_length=10)
|
||||
|
||||
class Meta:
|
||||
abstract: bool = True
|
||||
|
||||
|
||||
class TypeAnnotatedNonAbstractModel(models.Model):
|
||||
"""Model with type-annotated abstract = False - should trigger DJ008"""
|
||||
new_field = models.CharField(max_length=10)
|
||||
|
||||
class Meta:
|
||||
abstract: ClassVar[bool] = False
|
||||
|
||||
|
||||
class TypeAnnotatedAbstractModelWithStr(models.Model):
|
||||
"""Model with type-annotated abstract = True and __str__ method - should not trigger DJ008"""
|
||||
new_field = models.CharField(max_length=10)
|
||||
|
||||
class Meta(TypedModelMeta):
|
||||
abstract: ClassVar[bool] = True
|
||||
|
||||
def __str__(self):
|
||||
return self.new_field
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
_(f"{'value'}")
|
||||
|
||||
# Don't trigger for t-strings
|
||||
_(t"{'value'}")
|
||||
|
||||
@@ -13,3 +13,7 @@ from logging import info
|
||||
|
||||
info(f"{name}")
|
||||
info(f"{__name__}")
|
||||
|
||||
# Don't trigger for t-strings
|
||||
info(t"{name}")
|
||||
info(t"{__name__}")
|
||||
|
||||
@@ -47,3 +47,7 @@ def test_error_match_is_empty():
|
||||
|
||||
with pytest.raises(ValueError, match=f""):
|
||||
raise ValueError("Can't divide 1 by 0")
|
||||
|
||||
def test_ok_t_string_match():
|
||||
with pytest.raises(ValueError, match=t""):
|
||||
raise ValueError("Can't divide 1 by 0")
|
||||
|
||||
@@ -23,3 +23,9 @@ def f():
|
||||
pytest.fail(msg=f"")
|
||||
pytest.fail(reason="")
|
||||
pytest.fail(reason=f"")
|
||||
|
||||
# Skip for t-strings
|
||||
def g():
|
||||
pytest.fail(t"")
|
||||
pytest.fail(msg=t"")
|
||||
pytest.fail(reason=t"")
|
||||
|
||||
@@ -32,3 +32,7 @@ def test_error_match_is_empty():
|
||||
|
||||
with pytest.warns(UserWarning, match=f""):
|
||||
pass
|
||||
|
||||
def test_ok_match_t_string():
|
||||
with pytest.warns(UserWarning, match=t""):
|
||||
pass
|
||||
|
||||
@@ -422,6 +422,35 @@ def func(a: dict[str, int]) -> list[dict[str, int]]:
|
||||
services = a["services"]
|
||||
return services
|
||||
|
||||
|
||||
# See: https://github.com/astral-sh/ruff/issues/14052
|
||||
def outer() -> list[object]:
|
||||
@register
|
||||
async def inner() -> None:
|
||||
print(layout)
|
||||
|
||||
layout = [...]
|
||||
return layout
|
||||
|
||||
def outer() -> list[object]:
|
||||
with open("") as f:
|
||||
async def inner() -> None:
|
||||
print(layout)
|
||||
|
||||
layout = [...]
|
||||
return layout
|
||||
|
||||
|
||||
def outer() -> list[object]:
|
||||
def inner():
|
||||
with open("") as f:
|
||||
async def inner_inner() -> None:
|
||||
print(layout)
|
||||
|
||||
layout = [...]
|
||||
return layout
|
||||
|
||||
|
||||
# See: https://github.com/astral-sh/ruff/issues/18411
|
||||
def f():
|
||||
(#=
|
||||
|
||||
6
crates/ruff_linter/resources/test/fixtures/flake8_type_checking/whitespace.py
vendored
Normal file
6
crates/ruff_linter/resources/test/fixtures/flake8_type_checking/whitespace.py
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
# Regression test for: https://github.com/astral-sh/ruff/issues/19175
|
||||
# there is a (potentially invisible) unicode formfeed character (000C) between `TYPE_CHECKING` and the backslash
|
||||
from typing import TYPE_CHECKING\
|
||||
|
||||
if TYPE_CHECKING: import builtins
|
||||
builtins.print("!")
|
||||
@@ -245,3 +245,14 @@ def f(bar: str):
|
||||
class C:
|
||||
def __init__(self, x) -> None:
|
||||
print(locals())
|
||||
|
||||
###
|
||||
# Should trigger for t-string here
|
||||
# even though the corresponding f-string
|
||||
# does not trigger (since it is common in stubs)
|
||||
###
|
||||
class C:
|
||||
def f(self, x, y):
|
||||
"""Docstring."""
|
||||
msg = t"{x}..."
|
||||
raise NotImplementedError(msg)
|
||||
|
||||
5
crates/ruff_linter/resources/test/fixtures/isort/required_imports/multiple_strings.py
vendored
Normal file
5
crates/ruff_linter/resources/test/fixtures/isort/required_imports/multiple_strings.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
"""This is a docstring."""
|
||||
"This is not a docstring."
|
||||
"This is also not a docstring."
|
||||
|
||||
x = 1
|
||||
@@ -48,6 +48,39 @@ from typing import override, overload
|
||||
def BAD_FUNC():
|
||||
pass
|
||||
|
||||
|
||||
@overload
|
||||
def BAD_FUNC():
|
||||
pass
|
||||
|
||||
|
||||
import ast
|
||||
from ast import NodeTransformer
|
||||
|
||||
|
||||
class Visitor(ast.NodeVisitor):
|
||||
def visit_Constant(self, node):
|
||||
pass
|
||||
|
||||
def bad_Name(self):
|
||||
pass
|
||||
|
||||
class ExtendsVisitor(Visitor):
|
||||
def visit_Constant(self, node):
|
||||
pass
|
||||
|
||||
class Transformer(NodeTransformer):
|
||||
def visit_Constant(self, node):
|
||||
pass
|
||||
|
||||
|
||||
from http.server import BaseHTTPRequestHandler
|
||||
|
||||
|
||||
class MyRequestHandler(BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
pass
|
||||
|
||||
def dont_GET(self):
|
||||
pass
|
||||
|
||||
|
||||
@@ -22,3 +22,10 @@ assert b"hello" # [assert-on-string-literal]
|
||||
assert "", b"hi" # [assert-on-string-literal]
|
||||
assert "WhyNotHere?", "HereIsOk" # [assert-on-string-literal]
|
||||
assert 12, "ok here"
|
||||
|
||||
|
||||
# t-strings are always True even when "empty"
|
||||
# skip lint in this case
|
||||
assert t""
|
||||
assert t"hey"
|
||||
assert t"{a}"
|
||||
|
||||
@@ -140,3 +140,15 @@ class Foo:
|
||||
def unused_message_2(self, x):
|
||||
msg = ""
|
||||
raise NotImplementedError(x)
|
||||
|
||||
class TPerson:
|
||||
def developer_greeting(self, name): # [no-self-use]
|
||||
print(t"Greetings {name}!")
|
||||
|
||||
def greeting_1(self):
|
||||
print(t"Hello from {self.name} !")
|
||||
|
||||
def tstring(self, x):
|
||||
msg = t"{x}"
|
||||
raise NotImplementedError(msg)
|
||||
|
||||
|
||||
@@ -33,3 +33,11 @@ class Foo:
|
||||
|
||||
def __init__(self, bar):
|
||||
self.bar = bar
|
||||
|
||||
# This is a type error, out of scope for the rule
|
||||
class Foo:
|
||||
__slots__ = t"bar{baz}"
|
||||
|
||||
def __init__(self, bar):
|
||||
self.bar = bar
|
||||
|
||||
|
||||
@@ -84,3 +84,7 @@ def _match_ignore(line):
|
||||
# Not a valid type annotation but this test shouldn't result in a panic.
|
||||
# Refer: https://github.com/astral-sh/ruff/issues/11736
|
||||
x: '"foo".encode("utf-8")'
|
||||
|
||||
# AttributeError for t-strings so skip lint
|
||||
(t"foo{bar}").encode("utf-8")
|
||||
(t"foo{bar}").encode(encoding="utf-8")
|
||||
|
||||
@@ -90,3 +90,7 @@ bool(True)and None
|
||||
int(1)and None
|
||||
float(1.)and None
|
||||
bool(True)and()
|
||||
|
||||
|
||||
# t-strings are not native literals
|
||||
str(t"hey")
|
||||
|
||||
@@ -4,8 +4,8 @@ use crate::Fix;
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::codes::Rule;
|
||||
use crate::rules::{
|
||||
flake8_import_conventions, flake8_pyi, flake8_pytest_style, flake8_type_checking, pyflakes,
|
||||
pylint, pyupgrade, refurb, ruff,
|
||||
flake8_import_conventions, flake8_pyi, flake8_pytest_style, flake8_return,
|
||||
flake8_type_checking, pyflakes, pylint, pyupgrade, refurb, ruff,
|
||||
};
|
||||
|
||||
/// Run lint rules over the [`Binding`]s.
|
||||
@@ -25,11 +25,20 @@ pub(crate) fn bindings(checker: &Checker) {
|
||||
Rule::ForLoopWrites,
|
||||
Rule::CustomTypeVarForSelf,
|
||||
Rule::PrivateTypeParameter,
|
||||
Rule::UnnecessaryAssign,
|
||||
]) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (binding_id, binding) in checker.semantic.bindings.iter_enumerated() {
|
||||
if checker.is_rule_enabled(Rule::UnnecessaryAssign) {
|
||||
if binding.kind.is_function_definition() {
|
||||
flake8_return::rules::unnecessary_assign(
|
||||
checker,
|
||||
binding.statement(checker.semantic()).unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::UnusedVariable) {
|
||||
if binding.kind.is_bound_exception()
|
||||
&& binding.is_unused()
|
||||
|
||||
@@ -207,7 +207,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
Rule::UnnecessaryReturnNone,
|
||||
Rule::ImplicitReturnValue,
|
||||
Rule::ImplicitReturn,
|
||||
Rule::UnnecessaryAssign,
|
||||
Rule::SuperfluousElseReturn,
|
||||
Rule::SuperfluousElseRaise,
|
||||
Rule::SuperfluousElseContinue,
|
||||
|
||||
@@ -670,7 +670,11 @@ impl SemanticSyntaxContext for Checker<'_> {
|
||||
| SemanticSyntaxErrorKind::InvalidStarExpression
|
||||
| SemanticSyntaxErrorKind::AsyncComprehensionInSyncComprehension(_)
|
||||
| SemanticSyntaxErrorKind::DuplicateParameter(_)
|
||||
| SemanticSyntaxErrorKind::NonlocalDeclarationAtModuleLevel => {
|
||||
| SemanticSyntaxErrorKind::NonlocalDeclarationAtModuleLevel
|
||||
| SemanticSyntaxErrorKind::LoadBeforeNonlocalDeclaration { .. }
|
||||
| SemanticSyntaxErrorKind::NonlocalAndGlobal(_)
|
||||
| SemanticSyntaxErrorKind::AnnotatedGlobal(_)
|
||||
| SemanticSyntaxErrorKind::AnnotatedNonlocal(_) => {
|
||||
self.semantic_errors.borrow_mut().push(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use ruff_python_ast::Stmt;
|
||||
use ruff_python_ast::helpers::is_docstring_stmt;
|
||||
use ruff_python_codegen::Stylist;
|
||||
use ruff_python_parser::{TokenKind, Tokens};
|
||||
use ruff_python_trivia::is_python_whitespace;
|
||||
use ruff_python_trivia::{PythonWhitespace, textwrap::indent};
|
||||
use ruff_source_file::{LineRanges, UniversalNewlineIterator};
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
@@ -274,19 +275,12 @@ impl<'a> Insertion<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the end of the last docstring.
|
||||
/// Find the end of the docstring (first string statement).
|
||||
fn match_docstring_end(body: &[Stmt]) -> Option<TextSize> {
|
||||
let mut iter = body.iter();
|
||||
let mut stmt = iter.next()?;
|
||||
let stmt = body.first()?;
|
||||
if !is_docstring_stmt(stmt) {
|
||||
return None;
|
||||
}
|
||||
for next in iter {
|
||||
if !is_docstring_stmt(next) {
|
||||
break;
|
||||
}
|
||||
stmt = next;
|
||||
}
|
||||
Some(stmt.end())
|
||||
}
|
||||
|
||||
@@ -306,7 +300,7 @@ fn match_semicolon(s: &str) -> Option<TextSize> {
|
||||
fn match_continuation(s: &str) -> Option<TextSize> {
|
||||
for (offset, c) in s.char_indices() {
|
||||
match c {
|
||||
' ' | '\t' => continue,
|
||||
_ if is_python_whitespace(c) => continue,
|
||||
'\\' => return Some(TextSize::try_from(offset).unwrap()),
|
||||
_ => break,
|
||||
}
|
||||
@@ -366,7 +360,7 @@ mod tests {
|
||||
.trim_start();
|
||||
assert_eq!(
|
||||
insert(contents)?,
|
||||
Insertion::own_line("", TextSize::from(40), "\n")
|
||||
Insertion::own_line("", TextSize::from(20), "\n")
|
||||
);
|
||||
|
||||
let contents = r"
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
use std::io::Write;
|
||||
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_source_file::LineColumn;
|
||||
|
||||
use crate::message::{Emitter, EmitterContext};
|
||||
|
||||
/// Generate error logging commands for Azure Pipelines format.
|
||||
/// See [documentation](https://learn.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands?view=azure-devops&tabs=bash#logissue-log-an-error-or-warning)
|
||||
#[derive(Default)]
|
||||
pub struct AzureEmitter;
|
||||
|
||||
impl Emitter for AzureEmitter {
|
||||
fn emit(
|
||||
&mut self,
|
||||
writer: &mut dyn Write,
|
||||
diagnostics: &[Diagnostic],
|
||||
context: &EmitterContext,
|
||||
) -> anyhow::Result<()> {
|
||||
for diagnostic in diagnostics {
|
||||
let filename = diagnostic.expect_ruff_filename();
|
||||
let location = if context.is_notebook(&filename) {
|
||||
// We can't give a reasonable location for the structured formats,
|
||||
// so we show one that's clearly a fallback
|
||||
LineColumn::default()
|
||||
} else {
|
||||
diagnostic.expect_ruff_start_location()
|
||||
};
|
||||
|
||||
writeln!(
|
||||
writer,
|
||||
"##vso[task.logissue type=error\
|
||||
;sourcepath={filename};linenumber={line};columnnumber={col};{code}]{body}",
|
||||
line = location.line,
|
||||
col = location.column,
|
||||
code = diagnostic
|
||||
.secondary_code()
|
||||
.map_or_else(String::new, |code| format!("code={code};")),
|
||||
body = diagnostic.body(),
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use insta::assert_snapshot;
|
||||
|
||||
use crate::message::AzureEmitter;
|
||||
use crate::message::tests::{
|
||||
capture_emitter_output, create_diagnostics, create_syntax_error_diagnostics,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn output() {
|
||||
let mut emitter = AzureEmitter;
|
||||
let content = capture_emitter_output(&mut emitter, &create_diagnostics());
|
||||
|
||||
assert_snapshot!(content);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn syntax_errors() {
|
||||
let mut emitter = AzureEmitter;
|
||||
let content = capture_emitter_output(&mut emitter, &create_syntax_error_diagnostics());
|
||||
|
||||
assert_snapshot!(content);
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ use crate::{Applicability, Fix};
|
||||
/// * Compute the diff from the [`Edit`] because diff calculation is expensive.
|
||||
pub(super) struct Diff<'a> {
|
||||
fix: &'a Fix,
|
||||
source_code: SourceFile,
|
||||
source_code: &'a SourceFile,
|
||||
}
|
||||
|
||||
impl<'a> Diff<'a> {
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
use std::io::Write;
|
||||
|
||||
use serde::ser::SerializeSeq;
|
||||
use serde::{Serialize, Serializer};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_notebook::NotebookIndex;
|
||||
use ruff_source_file::{LineColumn, OneIndexed, SourceCode};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::Edit;
|
||||
use crate::message::{Emitter, EmitterContext};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct JsonEmitter;
|
||||
|
||||
impl Emitter for JsonEmitter {
|
||||
fn emit(
|
||||
&mut self,
|
||||
writer: &mut dyn Write,
|
||||
diagnostics: &[Diagnostic],
|
||||
context: &EmitterContext,
|
||||
) -> anyhow::Result<()> {
|
||||
serde_json::to_writer_pretty(
|
||||
writer,
|
||||
&ExpandedMessages {
|
||||
diagnostics,
|
||||
context,
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct ExpandedMessages<'a> {
|
||||
diagnostics: &'a [Diagnostic],
|
||||
context: &'a EmitterContext<'a>,
|
||||
}
|
||||
|
||||
impl Serialize for ExpandedMessages<'_> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut s = serializer.serialize_seq(Some(self.diagnostics.len()))?;
|
||||
|
||||
for message in self.diagnostics {
|
||||
let value = message_to_json_value(message, self.context);
|
||||
s.serialize_element(&value)?;
|
||||
}
|
||||
|
||||
s.end()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn message_to_json_value(message: &Diagnostic, context: &EmitterContext) -> Value {
|
||||
let source_file = message.expect_ruff_source_file();
|
||||
let source_code = source_file.to_source_code();
|
||||
let filename = message.expect_ruff_filename();
|
||||
let notebook_index = context.notebook_index(&filename);
|
||||
|
||||
let fix = message.fix().map(|fix| {
|
||||
json!({
|
||||
"applicability": fix.applicability(),
|
||||
"message": message.suggestion(),
|
||||
"edits": &ExpandedEdits { edits: fix.edits(), source_code: &source_code, notebook_index },
|
||||
})
|
||||
});
|
||||
|
||||
let mut start_location = source_code.line_column(message.expect_range().start());
|
||||
let mut end_location = source_code.line_column(message.expect_range().end());
|
||||
let mut noqa_location = message
|
||||
.noqa_offset()
|
||||
.map(|offset| source_code.line_column(offset));
|
||||
let mut notebook_cell_index = None;
|
||||
|
||||
if let Some(notebook_index) = notebook_index {
|
||||
notebook_cell_index = Some(
|
||||
notebook_index
|
||||
.cell(start_location.line)
|
||||
.unwrap_or(OneIndexed::MIN),
|
||||
);
|
||||
start_location = notebook_index.translate_line_column(&start_location);
|
||||
end_location = notebook_index.translate_line_column(&end_location);
|
||||
noqa_location =
|
||||
noqa_location.map(|location| notebook_index.translate_line_column(&location));
|
||||
}
|
||||
|
||||
json!({
|
||||
"code": message.secondary_code(),
|
||||
"url": message.to_url(),
|
||||
"message": message.body(),
|
||||
"fix": fix,
|
||||
"cell": notebook_cell_index,
|
||||
"location": location_to_json(start_location),
|
||||
"end_location": location_to_json(end_location),
|
||||
"filename": filename,
|
||||
"noqa_row": noqa_location.map(|location| location.line)
|
||||
})
|
||||
}
|
||||
|
||||
fn location_to_json(location: LineColumn) -> serde_json::Value {
|
||||
json!({
|
||||
"row": location.line,
|
||||
"column": location.column
|
||||
})
|
||||
}
|
||||
|
||||
struct ExpandedEdits<'a> {
|
||||
edits: &'a [Edit],
|
||||
source_code: &'a SourceCode<'a, 'a>,
|
||||
notebook_index: Option<&'a NotebookIndex>,
|
||||
}
|
||||
|
||||
impl Serialize for ExpandedEdits<'_> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut s = serializer.serialize_seq(Some(self.edits.len()))?;
|
||||
|
||||
for edit in self.edits {
|
||||
let mut location = self.source_code.line_column(edit.start());
|
||||
let mut end_location = self.source_code.line_column(edit.end());
|
||||
|
||||
if let Some(notebook_index) = self.notebook_index {
|
||||
// There exists a newline between each cell's source code in the
|
||||
// concatenated source code in Ruff. This newline doesn't actually
|
||||
// exists in the JSON source field.
|
||||
//
|
||||
// Now, certain edits may try to remove this newline, which means
|
||||
// the edit will spill over to the first character of the next cell.
|
||||
// If it does, we need to translate the end location to the last
|
||||
// character of the previous cell.
|
||||
match (
|
||||
notebook_index.cell(location.line),
|
||||
notebook_index.cell(end_location.line),
|
||||
) {
|
||||
(Some(start_cell), Some(end_cell)) if start_cell != end_cell => {
|
||||
debug_assert_eq!(end_location.column.get(), 1);
|
||||
|
||||
let prev_row = end_location.line.saturating_sub(1);
|
||||
end_location = LineColumn {
|
||||
line: notebook_index.cell_row(prev_row).unwrap_or(OneIndexed::MIN),
|
||||
column: self
|
||||
.source_code
|
||||
.line_column(self.source_code.line_end_exclusive(prev_row))
|
||||
.column,
|
||||
};
|
||||
}
|
||||
(Some(_), None) => {
|
||||
debug_assert_eq!(end_location.column.get(), 1);
|
||||
|
||||
let prev_row = end_location.line.saturating_sub(1);
|
||||
end_location = LineColumn {
|
||||
line: notebook_index.cell_row(prev_row).unwrap_or(OneIndexed::MIN),
|
||||
column: self
|
||||
.source_code
|
||||
.line_column(self.source_code.line_end_exclusive(prev_row))
|
||||
.column,
|
||||
};
|
||||
}
|
||||
_ => {
|
||||
end_location = notebook_index.translate_line_column(&end_location);
|
||||
}
|
||||
}
|
||||
location = notebook_index.translate_line_column(&location);
|
||||
}
|
||||
|
||||
let value = json!({
|
||||
"content": edit.content().unwrap_or_default(),
|
||||
"location": location_to_json(location),
|
||||
"end_location": location_to_json(end_location)
|
||||
});
|
||||
|
||||
s.serialize_element(&value)?;
|
||||
}
|
||||
|
||||
s.end()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use insta::assert_snapshot;
|
||||
|
||||
use crate::message::JsonEmitter;
|
||||
use crate::message::tests::{
|
||||
capture_emitter_notebook_output, capture_emitter_output, create_diagnostics,
|
||||
create_notebook_diagnostics, create_syntax_error_diagnostics,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn output() {
|
||||
let mut emitter = JsonEmitter;
|
||||
let content = capture_emitter_output(&mut emitter, &create_diagnostics());
|
||||
|
||||
assert_snapshot!(content);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn syntax_errors() {
|
||||
let mut emitter = JsonEmitter;
|
||||
let content = capture_emitter_output(&mut emitter, &create_syntax_error_diagnostics());
|
||||
|
||||
assert_snapshot!(content);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn notebook_output() {
|
||||
let mut emitter = JsonEmitter;
|
||||
let (diagnostics, notebook_indexes) = create_notebook_diagnostics();
|
||||
let content =
|
||||
capture_emitter_notebook_output(&mut emitter, &diagnostics, ¬ebook_indexes);
|
||||
|
||||
assert_snapshot!(content);
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
use std::io::Write;
|
||||
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
|
||||
use crate::message::json::message_to_json_value;
|
||||
use crate::message::{Emitter, EmitterContext};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct JsonLinesEmitter;
|
||||
|
||||
impl Emitter for JsonLinesEmitter {
|
||||
fn emit(
|
||||
&mut self,
|
||||
writer: &mut dyn Write,
|
||||
diagnostics: &[Diagnostic],
|
||||
context: &EmitterContext,
|
||||
) -> anyhow::Result<()> {
|
||||
for diagnostic in diagnostics {
|
||||
serde_json::to_writer(&mut *writer, &message_to_json_value(diagnostic, context))?;
|
||||
writer.write_all(b"\n")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use insta::assert_snapshot;
|
||||
|
||||
use crate::message::json_lines::JsonLinesEmitter;
|
||||
use crate::message::tests::{
|
||||
capture_emitter_notebook_output, capture_emitter_output, create_diagnostics,
|
||||
create_notebook_diagnostics, create_syntax_error_diagnostics,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn output() {
|
||||
let mut emitter = JsonLinesEmitter;
|
||||
let content = capture_emitter_output(&mut emitter, &create_diagnostics());
|
||||
|
||||
assert_snapshot!(content);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn syntax_errors() {
|
||||
let mut emitter = JsonLinesEmitter;
|
||||
let content = capture_emitter_output(&mut emitter, &create_syntax_error_diagnostics());
|
||||
|
||||
assert_snapshot!(content);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn notebook_output() {
|
||||
let mut emitter = JsonLinesEmitter;
|
||||
let (messages, notebook_indexes) = create_notebook_diagnostics();
|
||||
let content = capture_emitter_notebook_output(&mut emitter, &messages, ¬ebook_indexes);
|
||||
|
||||
assert_snapshot!(content);
|
||||
}
|
||||
}
|
||||
@@ -3,17 +3,17 @@ use std::fmt::Display;
|
||||
use std::io::Write;
|
||||
use std::ops::Deref;
|
||||
|
||||
use ruff_db::diagnostic::{
|
||||
Annotation, Diagnostic, DiagnosticId, LintName, SecondaryCode, Severity, Span,
|
||||
};
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
pub use azure::AzureEmitter;
|
||||
use ruff_db::diagnostic::{
|
||||
Annotation, Diagnostic, DiagnosticId, FileResolver, Input, LintName, SecondaryCode, Severity,
|
||||
Span, UnifiedFile,
|
||||
};
|
||||
use ruff_db::files::File;
|
||||
|
||||
pub use github::GithubEmitter;
|
||||
pub use gitlab::GitlabEmitter;
|
||||
pub use grouped::GroupedEmitter;
|
||||
pub use json::JsonEmitter;
|
||||
pub use json_lines::JsonLinesEmitter;
|
||||
pub use junit::JunitEmitter;
|
||||
pub use pylint::PylintEmitter;
|
||||
pub use rdjson::RdjsonEmitter;
|
||||
@@ -26,13 +26,10 @@ pub use text::TextEmitter;
|
||||
use crate::Fix;
|
||||
use crate::registry::Rule;
|
||||
|
||||
mod azure;
|
||||
mod diff;
|
||||
mod github;
|
||||
mod gitlab;
|
||||
mod grouped;
|
||||
mod json;
|
||||
mod json_lines;
|
||||
mod junit;
|
||||
mod pylint;
|
||||
mod rdjson;
|
||||
@@ -107,6 +104,34 @@ where
|
||||
diagnostic
|
||||
}
|
||||
|
||||
impl FileResolver for EmitterContext<'_> {
|
||||
fn path(&self, _file: File) -> &str {
|
||||
unimplemented!("Expected a Ruff file for rendering a Ruff diagnostic");
|
||||
}
|
||||
|
||||
fn input(&self, _file: File) -> Input {
|
||||
unimplemented!("Expected a Ruff file for rendering a Ruff diagnostic");
|
||||
}
|
||||
|
||||
fn notebook_index(&self, file: &UnifiedFile) -> Option<NotebookIndex> {
|
||||
match file {
|
||||
UnifiedFile::Ty(_) => {
|
||||
unimplemented!("Expected a Ruff file for rendering a Ruff diagnostic")
|
||||
}
|
||||
UnifiedFile::Ruff(file) => self.notebook_indexes.get(file.name()).cloned(),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_notebook(&self, file: &UnifiedFile) -> bool {
|
||||
match file {
|
||||
UnifiedFile::Ty(_) => {
|
||||
unimplemented!("Expected a Ruff file for rendering a Ruff diagnostic")
|
||||
}
|
||||
UnifiedFile::Ruff(file) => self.notebook_indexes.get(file.name()).is_some(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MessageWithLocation<'a> {
|
||||
message: &'a Diagnostic,
|
||||
start_location: LineColumn,
|
||||
|
||||
@@ -73,7 +73,7 @@ fn message_to_rdjson_value(message: &Diagnostic) -> Value {
|
||||
},
|
||||
"code": {
|
||||
"value": message.secondary_code(),
|
||||
"url": message.to_url(),
|
||||
"url": message.to_ruff_url(),
|
||||
},
|
||||
"suggestions": rdjson_suggestions(fix.edits(), &source_code),
|
||||
})
|
||||
@@ -86,7 +86,7 @@ fn message_to_rdjson_value(message: &Diagnostic) -> Value {
|
||||
},
|
||||
"code": {
|
||||
"value": message.secondary_code(),
|
||||
"url": message.to_url(),
|
||||
"url": message.to_ruff_url(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -87,9 +87,14 @@ fn detect_blind_exception(
|
||||
}
|
||||
}
|
||||
|
||||
let first_arg = arguments.args.first()?;
|
||||
let exception_argument_name = if is_pytest_raises {
|
||||
"expected_exception"
|
||||
} else {
|
||||
"exception"
|
||||
};
|
||||
|
||||
let builtin_symbol = semantic.resolve_builtin_symbol(first_arg)?;
|
||||
let exception_expr = arguments.find_argument_value(exception_argument_name, 0)?;
|
||||
let builtin_symbol = semantic.resolve_builtin_symbol(exception_expr)?;
|
||||
|
||||
match builtin_symbol {
|
||||
"Exception" => Some(ExceptionKind::Exception),
|
||||
|
||||
@@ -43,3 +43,29 @@ B017_0.py:57:36: B017 Do not assert blind exception: `Exception`
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ B017
|
||||
58 | raise ValueError("Multiple context managers")
|
||||
|
|
||||
|
||||
B017_0.py:62:10: B017 Do not assert blind exception: `Exception`
|
||||
|
|
||||
61 | def test_pytest_raises_keyword():
|
||||
62 | with pytest.raises(expected_exception=Exception):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B017
|
||||
63 | raise ValueError("Should be flagged")
|
||||
|
|
||||
|
||||
B017_0.py:68:18: B017 Do not assert blind exception: `Exception`
|
||||
|
|
||||
66 | class TestKwargs(unittest.TestCase):
|
||||
67 | def test_method(self):
|
||||
68 | with self.assertRaises(exception=Exception):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B017
|
||||
69 | raise ValueError("Should be flagged")
|
||||
|
|
||||
|
||||
B017_0.py:71:18: B017 Do not assert blind exception: `BaseException`
|
||||
|
|
||||
69 | raise ValueError("Should be flagged")
|
||||
70 |
|
||||
71 | with self.assertRaises(exception=BaseException):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B017
|
||||
72 | raise ValueError("Should be flagged")
|
||||
|
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
B018.py:11:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
||||
|
|
||||
|
||||
@@ -43,3 +43,29 @@ B017_0.py:57:36: B017 Do not assert blind exception: `Exception`
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ B017
|
||||
58 | raise ValueError("Multiple context managers")
|
||||
|
|
||||
|
||||
B017_0.py:62:10: B017 Do not assert blind exception: `Exception`
|
||||
|
|
||||
61 | def test_pytest_raises_keyword():
|
||||
62 | with pytest.raises(expected_exception=Exception):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B017
|
||||
63 | raise ValueError("Should be flagged")
|
||||
|
|
||||
|
||||
B017_0.py:68:18: B017 Do not assert blind exception: `Exception`
|
||||
|
|
||||
66 | class TestKwargs(unittest.TestCase):
|
||||
67 | def test_method(self):
|
||||
68 | with self.assertRaises(exception=Exception):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B017
|
||||
69 | raise ValueError("Should be flagged")
|
||||
|
|
||||
|
||||
B017_0.py:71:18: B017 Do not assert blind exception: `BaseException`
|
||||
|
|
||||
69 | raise ValueError("Should be flagged")
|
||||
70 |
|
||||
71 | with self.assertRaises(exception=BaseException):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B017
|
||||
72 | raise ValueError("Should be flagged")
|
||||
|
|
||||
|
||||
@@ -96,22 +96,43 @@ fn is_model_abstract(class_def: &ast::StmtClassDef) -> bool {
|
||||
continue;
|
||||
}
|
||||
for element in body {
|
||||
let Stmt::Assign(ast::StmtAssign { targets, value, .. }) = element else {
|
||||
continue;
|
||||
};
|
||||
for target in targets {
|
||||
let Expr::Name(ast::ExprName { id, .. }) = target else {
|
||||
continue;
|
||||
};
|
||||
if id != "abstract" {
|
||||
continue;
|
||||
match element {
|
||||
Stmt::Assign(ast::StmtAssign { targets, value, .. }) => {
|
||||
if targets
|
||||
.iter()
|
||||
.any(|target| is_abstract_true_assignment(target, Some(value)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if !is_const_true(value) {
|
||||
continue;
|
||||
Stmt::AnnAssign(ast::StmtAnnAssign { target, value, .. }) => {
|
||||
if is_abstract_true_assignment(target, value.as_deref()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn is_abstract_true_assignment(target: &Expr, value: Option<&Expr>) -> bool {
|
||||
let Expr::Name(ast::ExprName { id, .. }) = target else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if id != "abstract" {
|
||||
return false;
|
||||
}
|
||||
|
||||
let Some(value) = value else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if !is_const_true(value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_django/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
DJ008.py:6:7: DJ008 Model does not define `__str__` method
|
||||
|
|
||||
@@ -31,3 +30,11 @@ DJ008.py:182:7: DJ008 Model does not define `__str__` method
|
||||
| ^^^^^^^^^^^^^^^^^^ DJ008
|
||||
183 | pass
|
||||
|
|
||||
|
||||
DJ008.py:215:7: DJ008 Model does not define `__str__` method
|
||||
|
|
||||
215 | class TypeAnnotatedNonAbstractModel(models.Model):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ DJ008
|
||||
216 | """Model with type-annotated abstract = False - should trigger DJ008"""
|
||||
217 | new_field = models.CharField(max_length=10)
|
||||
|
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_gettext/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
INT001.py:1:3: INT001 f-string is resolved before function call; consider `_("string %s") % arg`
|
||||
|
|
||||
1 | _(f"{'value'}")
|
||||
| ^^^^^^^^^^^^ INT001
|
||||
2 |
|
||||
3 | # Don't trigger for t-strings
|
||||
|
|
||||
|
||||
@@ -52,4 +52,6 @@ G004.py:15:6: G004 Logging statement uses f-string
|
||||
14 | info(f"{name}")
|
||||
15 | info(f"{__name__}")
|
||||
| ^^^^^^^^^^^^^ G004
|
||||
16 |
|
||||
17 | # Don't trigger for t-strings
|
||||
|
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
PT016.py:19:5: PT016 No message passed to `pytest.fail()`
|
||||
|
|
||||
@@ -67,4 +66,6 @@ PT016.py:25:5: PT016 No message passed to `pytest.fail()`
|
||||
24 | pytest.fail(reason="")
|
||||
25 | pytest.fail(reason=f"")
|
||||
| ^^^^^^^^^^^ PT016
|
||||
26 |
|
||||
27 | # Skip for t-strings
|
||||
|
|
||||
|
||||
@@ -539,7 +539,21 @@ fn implicit_return(checker: &Checker, function_def: &ast::StmtFunctionDef, stmt:
|
||||
}
|
||||
|
||||
/// RET504
|
||||
fn unnecessary_assign(checker: &Checker, stack: &Stack) {
|
||||
pub(crate) fn unnecessary_assign(checker: &Checker, function_stmt: &Stmt) {
|
||||
let Stmt::FunctionDef(function_def) = function_stmt else {
|
||||
return;
|
||||
};
|
||||
let Some(stack) = create_stack(checker, function_def) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if !result_exists(&stack.returns) {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(function_scope) = checker.semantic().function_scope(function_def) else {
|
||||
return;
|
||||
};
|
||||
for (assign, return_, stmt) in &stack.assignment_return {
|
||||
// Identify, e.g., `return x`.
|
||||
let Some(value) = return_.value.as_ref() else {
|
||||
@@ -583,6 +597,22 @@ fn unnecessary_assign(checker: &Checker, stack: &Stack) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(assigned_binding) = function_scope
|
||||
.get(assigned_id)
|
||||
.map(|binding_id| checker.semantic().binding(binding_id))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
// Check if there's any reference made to `assigned_binding` in another scope, e.g, nested
|
||||
// functions. If there is, ignore them.
|
||||
if assigned_binding
|
||||
.references()
|
||||
.map(|reference_id| checker.semantic().reference(reference_id))
|
||||
.any(|reference| reference.scope_id() != assigned_binding.scope)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut diagnostic = checker.report_diagnostic(
|
||||
UnnecessaryAssign {
|
||||
name: assigned_id.to_string(),
|
||||
@@ -665,24 +695,21 @@ fn superfluous_elif_else(checker: &Checker, stack: &Stack) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Run all checks from the `flake8-return` plugin.
|
||||
pub(crate) fn function(checker: &Checker, function_def: &ast::StmtFunctionDef) {
|
||||
let ast::StmtFunctionDef {
|
||||
decorator_list,
|
||||
returns,
|
||||
body,
|
||||
..
|
||||
} = function_def;
|
||||
fn create_stack<'a>(
|
||||
checker: &'a Checker,
|
||||
function_def: &'a ast::StmtFunctionDef,
|
||||
) -> Option<Stack<'a>> {
|
||||
let ast::StmtFunctionDef { body, .. } = function_def;
|
||||
|
||||
// Find the last statement in the function.
|
||||
let Some(last_stmt) = body.last() else {
|
||||
// Skip empty functions.
|
||||
return;
|
||||
return None;
|
||||
};
|
||||
|
||||
// Skip functions that consist of a single return statement.
|
||||
if body.len() == 1 && matches!(last_stmt, Stmt::Return(_)) {
|
||||
return;
|
||||
return None;
|
||||
}
|
||||
|
||||
// Traverse the function body, to collect the stack.
|
||||
@@ -696,9 +723,29 @@ pub(crate) fn function(checker: &Checker, function_def: &ast::StmtFunctionDef) {
|
||||
|
||||
// Avoid false positives for generators.
|
||||
if stack.is_generator {
|
||||
return;
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(stack)
|
||||
}
|
||||
|
||||
/// Run all checks from the `flake8-return` plugin, but `RET504` which is ran
|
||||
/// after the semantic model is fully built.
|
||||
pub(crate) fn function(checker: &Checker, function_def: &ast::StmtFunctionDef) {
|
||||
let ast::StmtFunctionDef {
|
||||
decorator_list,
|
||||
returns,
|
||||
body,
|
||||
..
|
||||
} = function_def;
|
||||
|
||||
let Some(stack) = create_stack(checker, function_def) else {
|
||||
return;
|
||||
};
|
||||
let Some(last_stmt) = body.last() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if checker.any_rule_enabled(&[
|
||||
Rule::SuperfluousElseReturn,
|
||||
Rule::SuperfluousElseRaise,
|
||||
@@ -721,10 +768,6 @@ pub(crate) fn function(checker: &Checker, function_def: &ast::StmtFunctionDef) {
|
||||
if checker.is_rule_enabled(Rule::ImplicitReturn) {
|
||||
implicit_return(checker, function_def, last_stmt);
|
||||
}
|
||||
|
||||
if checker.is_rule_enabled(Rule::UnnecessaryAssign) {
|
||||
unnecessary_assign(checker, &stack);
|
||||
}
|
||||
} else {
|
||||
if checker.is_rule_enabled(Rule::UnnecessaryReturnNone) {
|
||||
// Skip functions that have a return annotation that is not `None`.
|
||||
|
||||
@@ -247,8 +247,6 @@ RET504.py:423:16: RET504 [*] Unnecessary assignment to `services` before `return
|
||||
422 | services = a["services"]
|
||||
423 | return services
|
||||
| ^^^^^^^^ RET504
|
||||
424 |
|
||||
425 | # See: https://github.com/astral-sh/ruff/issues/18411
|
||||
|
|
||||
= help: Remove unnecessary assignment
|
||||
|
||||
@@ -260,46 +258,46 @@ RET504.py:423:16: RET504 [*] Unnecessary assignment to `services` before `return
|
||||
423 |- return services
|
||||
422 |+ return a["services"]
|
||||
424 423 |
|
||||
425 424 | # See: https://github.com/astral-sh/ruff/issues/18411
|
||||
426 425 | def f():
|
||||
425 424 |
|
||||
426 425 | # See: https://github.com/astral-sh/ruff/issues/14052
|
||||
|
||||
RET504.py:429:12: RET504 [*] Unnecessary assignment to `x` before `return` statement
|
||||
RET504.py:458:12: RET504 [*] Unnecessary assignment to `x` before `return` statement
|
||||
|
|
||||
427 | (#=
|
||||
428 | x) = 1
|
||||
429 | return x
|
||||
456 | (#=
|
||||
457 | x) = 1
|
||||
458 | return x
|
||||
| ^ RET504
|
||||
430 |
|
||||
431 | def f():
|
||||
459 |
|
||||
460 | def f():
|
||||
|
|
||||
= help: Remove unnecessary assignment
|
||||
|
||||
ℹ Unsafe fix
|
||||
424 424 |
|
||||
425 425 | # See: https://github.com/astral-sh/ruff/issues/18411
|
||||
426 426 | def f():
|
||||
427 |- (#=
|
||||
428 |- x) = 1
|
||||
429 |- return x
|
||||
427 |+ return 1
|
||||
430 428 |
|
||||
431 429 | def f():
|
||||
432 430 | x = (1
|
||||
453 453 |
|
||||
454 454 | # See: https://github.com/astral-sh/ruff/issues/18411
|
||||
455 455 | def f():
|
||||
456 |- (#=
|
||||
457 |- x) = 1
|
||||
458 |- return x
|
||||
456 |+ return 1
|
||||
459 457 |
|
||||
460 458 | def f():
|
||||
461 459 | x = (1
|
||||
|
||||
RET504.py:434:12: RET504 [*] Unnecessary assignment to `x` before `return` statement
|
||||
RET504.py:463:12: RET504 [*] Unnecessary assignment to `x` before `return` statement
|
||||
|
|
||||
432 | x = (1
|
||||
433 | )
|
||||
434 | return x
|
||||
461 | x = (1
|
||||
462 | )
|
||||
463 | return x
|
||||
| ^ RET504
|
||||
|
|
||||
= help: Remove unnecessary assignment
|
||||
|
||||
ℹ Unsafe fix
|
||||
429 429 | return x
|
||||
430 430 |
|
||||
431 431 | def f():
|
||||
432 |- x = (1
|
||||
432 |+ return (1
|
||||
433 433 | )
|
||||
434 |- return x
|
||||
458 458 | return x
|
||||
459 459 |
|
||||
460 460 | def f():
|
||||
461 |- x = (1
|
||||
461 |+ return (1
|
||||
462 462 | )
|
||||
463 |- return x
|
||||
|
||||
@@ -36,6 +36,7 @@ mod tests {
|
||||
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TC004_8.py"))]
|
||||
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TC004_9.py"))]
|
||||
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("quote.py"))]
|
||||
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("whitespace.py"))]
|
||||
#[test_case(Rule::RuntimeStringUnion, Path::new("TC010_1.py"))]
|
||||
#[test_case(Rule::RuntimeStringUnion, Path::new("TC010_2.py"))]
|
||||
#[test_case(Rule::TypingOnlyFirstPartyImport, Path::new("TC001.py"))]
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
|
||||
---
|
||||
whitespace.py:5:26: TC004 [*] Move import `builtins` out of type-checking block. Import is used for more than type hinting.
|
||||
|
|
||||
3 | from typing import TYPE_CHECKING\
|
||||
4 |
|
||||
5 | if TYPE_CHECKING: import builtins
|
||||
| ^^^^^^^^ TC004
|
||||
6 | builtins.print("!")
|
||||
|
|
||||
= help: Move out of type-checking block
|
||||
|
||||
ℹ Unsafe fix
|
||||
1 1 | # Regression test for: https://github.com/astral-sh/ruff/issues/19175
|
||||
2 2 | # there is a (potentially invisible) unicode formfeed character (000C) between `TYPE_CHECKING` and the backslash
|
||||
3 |-from typing import TYPE_CHECKING\
|
||||
3 |+from typing import TYPE_CHECKING; import builtins\
|
||||
4 4 |
|
||||
5 |-if TYPE_CHECKING: import builtins
|
||||
5 |+if TYPE_CHECKING: pass
|
||||
6 6 | builtins.print("!")
|
||||
@@ -66,3 +66,13 @@ ARG.py:216:24: ARG002 Unused method argument: `x`
|
||||
| ^ ARG002
|
||||
217 | print("Hello, world!")
|
||||
|
|
||||
|
||||
ARG.py:255:20: ARG002 Unused method argument: `y`
|
||||
|
|
||||
253 | ###
|
||||
254 | class C:
|
||||
255 | def f(self, x, y):
|
||||
| ^ ARG002
|
||||
256 | """Docstring."""
|
||||
257 | msg = t"{x}..."
|
||||
|
|
||||
|
||||
@@ -912,6 +912,7 @@ mod tests {
|
||||
#[test_case(Path::new("docstring.pyi"))]
|
||||
#[test_case(Path::new("docstring_only.py"))]
|
||||
#[test_case(Path::new("empty.py"))]
|
||||
#[test_case(Path::new("multiple_strings.py"))]
|
||||
fn required_imports(path: &Path) -> Result<()> {
|
||||
let snapshot = format!("required_imports_{}", path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/isort/mod.rs
|
||||
---
|
||||
multiple_strings.py:1:1: I002 [*] Missing required import: `from __future__ import annotations`
|
||||
ℹ Safe fix
|
||||
1 1 | """This is a docstring."""
|
||||
2 |+from __future__ import annotations
|
||||
2 3 | "This is not a docstring."
|
||||
3 4 | "This is also not a docstring."
|
||||
4 5 |
|
||||
|
||||
multiple_strings.py:1:1: I002 [*] Missing required import: `from __future__ import generator_stop`
|
||||
ℹ Safe fix
|
||||
1 1 | """This is a docstring."""
|
||||
2 |+from __future__ import generator_stop
|
||||
2 3 | "This is not a docstring."
|
||||
3 4 | "This is also not a docstring."
|
||||
4 5 |
|
||||
@@ -1,9 +1,7 @@
|
||||
use ruff_python_ast::{Decorator, Stmt};
|
||||
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::identifier::Identifier;
|
||||
use ruff_python_ast::{Decorator, Stmt, identifier::Identifier};
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_python_semantic::analyze::visibility;
|
||||
use ruff_python_semantic::analyze::{class::any_base_class, visibility};
|
||||
use ruff_python_stdlib::str;
|
||||
|
||||
use crate::Violation;
|
||||
@@ -84,6 +82,41 @@ pub(crate) fn invalid_function_name(
|
||||
return;
|
||||
}
|
||||
|
||||
let parent_class = semantic
|
||||
.current_statement_parent()
|
||||
.and_then(|parent| parent.as_class_def_stmt());
|
||||
|
||||
// Ignore the visit_* methods of the ast.NodeVisitor and ast.NodeTransformer classes.
|
||||
if name.starts_with("visit_")
|
||||
&& parent_class.is_some_and(|class| {
|
||||
any_base_class(class, semantic, &mut |superclass| {
|
||||
let qualified = semantic.resolve_qualified_name(superclass);
|
||||
qualified.is_some_and(|name| {
|
||||
matches!(name.segments(), ["ast", "NodeVisitor" | "NodeTransformer"])
|
||||
})
|
||||
})
|
||||
})
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore the do_* methods of the http.server.BaseHTTPRequestHandler class
|
||||
if name.starts_with("do_")
|
||||
&& parent_class.is_some_and(|class| {
|
||||
any_base_class(class, semantic, &mut |superclass| {
|
||||
let qualified = semantic.resolve_qualified_name(superclass);
|
||||
qualified.is_some_and(|name| {
|
||||
matches!(
|
||||
name.segments(),
|
||||
["http", "server", "BaseHTTPRequestHandler"]
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
checker.report_diagnostic(
|
||||
InvalidFunctionName {
|
||||
name: name.to_string(),
|
||||
|
||||
@@ -37,3 +37,21 @@ N802.py:40:9: N802 Function name `testTest` should be lowercase
|
||||
| ^^^^^^^^ N802
|
||||
41 | assert True
|
||||
|
|
||||
|
||||
N802.py:65:9: N802 Function name `bad_Name` should be lowercase
|
||||
|
|
||||
63 | pass
|
||||
64 |
|
||||
65 | def bad_Name(self):
|
||||
| ^^^^^^^^ N802
|
||||
66 | pass
|
||||
|
|
||||
|
||||
N802.py:84:9: N802 Function name `dont_GET` should be lowercase
|
||||
|
|
||||
82 | pass
|
||||
83 |
|
||||
84 | def dont_GET(self):
|
||||
| ^^^^^^^^ N802
|
||||
85 | pass
|
||||
|
|
||||
|
||||
@@ -18,11 +18,15 @@ use crate::checkers::ast::Checker;
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import os
|
||||
///
|
||||
/// os.getenv(1)
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// import os
|
||||
///
|
||||
/// os.getenv("1")
|
||||
/// ```
|
||||
#[derive(ViolationMetadata)]
|
||||
|
||||
@@ -75,3 +75,21 @@ no_self_use.py:140:9: PLR6301 Method `unused_message_2` could be a function, cla
|
||||
141 | msg = ""
|
||||
142 | raise NotImplementedError(x)
|
||||
|
|
||||
|
||||
no_self_use.py:145:9: PLR6301 Method `developer_greeting` could be a function, class method, or static method
|
||||
|
|
||||
144 | class TPerson:
|
||||
145 | def developer_greeting(self, name): # [no-self-use]
|
||||
| ^^^^^^^^^^^^^^^^^^ PLR6301
|
||||
146 | print(t"Greetings {name}!")
|
||||
|
|
||||
|
||||
no_self_use.py:151:9: PLR6301 Method `tstring` could be a function, class method, or static method
|
||||
|
|
||||
149 | print(t"Hello from {self.name} !")
|
||||
150 |
|
||||
151 | def tstring(self, x):
|
||||
| ^^^^^^^ PLR6301
|
||||
152 | msg = t"{x}"
|
||||
153 | raise NotImplementedError(msg)
|
||||
|
|
||||
|
||||
@@ -14,12 +14,12 @@ use crate::{AlwaysFixableViolation, Edit, Fix};
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// from xml.etree import cElementTree
|
||||
/// from xml.etree import cElementTree as ET
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// from xml.etree import ElementTree
|
||||
/// from xml.etree import ElementTree as ET
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
|
||||
@@ -43,7 +43,7 @@ use super::{
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// from typing import TypeVar
|
||||
/// from typing import Generic, TypeVar
|
||||
///
|
||||
/// T = TypeVar("T")
|
||||
///
|
||||
|
||||
@@ -40,12 +40,18 @@ use super::{
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// from typing import Annotated, TypeAlias, TypeAliasType
|
||||
/// from annotated_types import Gt
|
||||
///
|
||||
/// ListOfInt: TypeAlias = list[int]
|
||||
/// PositiveInt = TypeAliasType("PositiveInt", Annotated[int, Gt(0)])
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// from typing import Annotated
|
||||
/// from annotated_types import Gt
|
||||
///
|
||||
/// type ListOfInt = list[int]
|
||||
/// type PositiveInt = Annotated[int, Gt(0)]
|
||||
/// ```
|
||||
|
||||
@@ -27,6 +27,8 @@ use crate::{AlwaysFixableViolation, Edit, Fix};
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import asyncio
|
||||
///
|
||||
/// raise asyncio.TimeoutError
|
||||
/// ```
|
||||
///
|
||||
|
||||
@@ -576,6 +576,8 @@ UP012.py:86:5: UP012 [*] Unnecessary call to `encode` as UTF-8
|
||||
85 | # Refer: https://github.com/astral-sh/ruff/issues/11736
|
||||
86 | x: '"foo".encode("utf-8")'
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ UP012
|
||||
87 |
|
||||
88 | # AttributeError for t-strings so skip lint
|
||||
|
|
||||
= help: Rewrite as bytes literal
|
||||
|
||||
@@ -585,3 +587,6 @@ UP012.py:86:5: UP012 [*] Unnecessary call to `encode` as UTF-8
|
||||
85 85 | # Refer: https://github.com/astral-sh/ruff/issues/11736
|
||||
86 |-x: '"foo".encode("utf-8")'
|
||||
86 |+x: 'b"foo"'
|
||||
87 87 |
|
||||
88 88 | # AttributeError for t-strings so skip lint
|
||||
89 89 | (t"foo{bar}").encode("utf-8")
|
||||
|
||||
@@ -660,6 +660,7 @@ UP018.py:90:1: UP018 [*] Unnecessary `int` call (rewrite as a literal)
|
||||
90 |+1 and None
|
||||
91 91 | float(1.)and None
|
||||
92 92 | bool(True)and()
|
||||
93 93 |
|
||||
|
||||
UP018.py:91:1: UP018 [*] Unnecessary `float` call (rewrite as a literal)
|
||||
|
|
||||
@@ -678,6 +679,8 @@ UP018.py:91:1: UP018 [*] Unnecessary `float` call (rewrite as a literal)
|
||||
91 |-float(1.)and None
|
||||
91 |+1. and None
|
||||
92 92 | bool(True)and()
|
||||
93 93 |
|
||||
94 94 |
|
||||
|
||||
UP018.py:92:1: UP018 [*] Unnecessary `bool` call (rewrite as a literal)
|
||||
|
|
||||
@@ -694,3 +697,6 @@ UP018.py:92:1: UP018 [*] Unnecessary `bool` call (rewrite as a literal)
|
||||
91 91 | float(1.)and None
|
||||
92 |-bool(True)and()
|
||||
92 |+True and()
|
||||
93 93 |
|
||||
94 94 |
|
||||
95 95 | # t-strings are not native literals
|
||||
|
||||
@@ -19,22 +19,26 @@ use crate::rules::refurb::helpers::parenthesize_loop_iter_if_necessary;
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// from pathlib import Path
|
||||
///
|
||||
/// with Path("file").open("w") as f:
|
||||
/// for line in lines:
|
||||
/// f.write(line)
|
||||
///
|
||||
/// with Path("file").open("wb") as f:
|
||||
/// for line in lines:
|
||||
/// f.write(line.encode())
|
||||
/// with Path("file").open("wb") as f_b:
|
||||
/// for line_b in lines_b:
|
||||
/// f_b.write(line_b.encode())
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// from pathlib import Path
|
||||
///
|
||||
/// with Path("file").open("w") as f:
|
||||
/// f.writelines(lines)
|
||||
///
|
||||
/// with Path("file").open("wb") as f:
|
||||
/// f.writelines(line.encode() for line in lines)
|
||||
/// with Path("file").open("wb") as f_b:
|
||||
/// f_b.writelines(line_b.encode() for line_b in lines_b)
|
||||
/// ```
|
||||
///
|
||||
/// ## Fix safety
|
||||
|
||||
@@ -14,11 +14,15 @@ use crate::{checkers::ast::Checker, importer::ImportRequest};
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// from pathlib import Path
|
||||
///
|
||||
/// cwd = Path().resolve()
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// from pathlib import Path
|
||||
///
|
||||
/// cwd = Path.cwd()
|
||||
/// ```
|
||||
///
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user