Compare commits
90 Commits
0.14.2
...
alex/dont-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80f045a261 | ||
|
|
349e24db9a | ||
|
|
bff32a41dc | ||
|
|
17c7b3cde1 | ||
|
|
921f409ee8 | ||
|
|
a151f9746d | ||
|
|
521217bb90 | ||
|
|
a32d5b8dc4 | ||
|
|
6337e22f0c | ||
|
|
827d8ae5d4 | ||
|
|
1734ddfb3e | ||
|
|
ff3a6a8fbd | ||
|
|
9664474c51 | ||
|
|
69b4c29924 | ||
|
|
bb40c34361 | ||
|
|
b93d8f2b9f | ||
|
|
1d111c8780 | ||
|
|
9d7da914b9 | ||
|
|
0c2cf75869 | ||
|
|
1d6ae8596a | ||
|
|
cf4e82d4b0 | ||
|
|
1baf98aab3 | ||
|
|
3179b05221 | ||
|
|
172e8d4ae0 | ||
|
|
735ec0c1f9 | ||
|
|
3585c96ea5 | ||
|
|
4b026c2a55 | ||
|
|
4b758b3746 | ||
|
|
8737a2d5f5 | ||
|
|
3be3a10a2f | ||
|
|
13375d0e42 | ||
|
|
c0b04d4b7c | ||
|
|
1c7ea690a8 | ||
|
|
9bacd19c5a | ||
|
|
f0fe6d62fb | ||
|
|
10bda3df00 | ||
|
|
e55bc943e5 | ||
|
|
1b0ee4677e | ||
|
|
1ebedf6df5 | ||
|
|
980b4c55b2 | ||
|
|
5139f76d1f | ||
|
|
aca8ba76a4 | ||
|
|
7045898ffa | ||
|
|
d38a5292d2 | ||
|
|
83a00c0ac8 | ||
|
|
8b22fd1a5f | ||
|
|
765257bdce | ||
|
|
2d4e0edee4 | ||
|
|
9ce3fa3fe3 | ||
|
|
196a68e4c8 | ||
|
|
349061117c | ||
|
|
d0aebaa253 | ||
|
|
17850eee4b | ||
|
|
4d2ee41e24 | ||
|
|
7b959ef44b | ||
|
|
4c4ddc8c29 | ||
|
|
ae0343f848 | ||
|
|
29462ea1d4 | ||
|
|
7fee62b2de | ||
|
|
96b60c11d9 | ||
|
|
fffbe5a879 | ||
|
|
116611bd39 | ||
|
|
db0e921db1 | ||
|
|
3c7f56f582 | ||
|
|
8a73519b25 | ||
|
|
fa12fd0184 | ||
|
|
fdb8ea487c | ||
|
|
d846a0319a | ||
|
|
bca5d33385 | ||
|
|
c83c4d52a4 | ||
|
|
e692b7f1ee | ||
|
|
8e51db3ecd | ||
|
|
64ab79e572 | ||
|
|
1ade9a5943 | ||
|
|
304ac22e74 | ||
|
|
c3de8847d5 | ||
|
|
f17ddd62ad | ||
|
|
adbf05802a | ||
|
|
eb8c0ad87c | ||
|
|
a2d0d39853 | ||
|
|
f36fa7d6c1 | ||
|
|
6f0982d2d6 | ||
|
|
3e8685d2ec | ||
|
|
7576669297 | ||
|
|
bf74c824eb | ||
|
|
e196c2ab37 | ||
|
|
4522f35ea7 | ||
|
|
be5a62f7e5 | ||
|
|
28aed61a22 | ||
|
|
05cde8bd19 |
13
.github/workflows/ci.yaml
vendored
13
.github/workflows/ci.yaml
vendored
@@ -277,8 +277,8 @@ jobs:
|
||||
run: cargo test -p ty_python_semantic --test mdtest || true
|
||||
- name: "Run tests"
|
||||
run: cargo insta test --all-features --unreferenced reject --test-runner nextest
|
||||
# Dogfood ty on py-fuzzer
|
||||
- run: uv run --project=./python/py-fuzzer cargo run -p ty check --project=./python/py-fuzzer
|
||||
- name: Dogfood ty on py-fuzzer
|
||||
run: uv run --project=./python/py-fuzzer cargo run -p ty check --project=./python/py-fuzzer
|
||||
# Check for broken links in the documentation.
|
||||
- run: cargo doc --all --no-deps
|
||||
env:
|
||||
@@ -438,7 +438,7 @@ jobs:
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install cargo-binstall"
|
||||
uses: cargo-bins/cargo-binstall@a66119fbb1c952daba62640c2609111fe0803621 # v1.15.7
|
||||
uses: cargo-bins/cargo-binstall@afcf9780305558bcc9e4bc94b7589ab2bb8b6106 # v1.15.9
|
||||
- name: "Install cargo-fuzz"
|
||||
# Download the latest version from quick install and not the github releases because github releases only has MUSL targets.
|
||||
run: cargo binstall cargo-fuzz --force --disable-strategies crate-meta-data --no-confirm
|
||||
@@ -531,8 +531,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
|
||||
with:
|
||||
# TODO: figure out why `ruff-ecosystem` crashes on Python 3.14
|
||||
python-version: "3.13"
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
activate-environment: true
|
||||
|
||||
- uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
@@ -650,7 +649,7 @@ jobs:
|
||||
- determine_changes
|
||||
# Only runs on pull requests, since that is the only we way we can find the base version for comparison.
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && github.event_name == 'pull_request' && (needs.determine_changes.outputs.ty == 'true' || needs.determine_changes.outputs.py-fuzzer == 'true') }}
|
||||
timeout-minutes: ${{ github.repository == 'astral-sh/ruff' && 5 || 20 }}
|
||||
timeout-minutes: ${{ github.repository == 'astral-sh/ruff' && 10 || 20 }}
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
@@ -699,7 +698,7 @@ jobs:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: cargo-bins/cargo-binstall@a66119fbb1c952daba62640c2609111fe0803621 # v1.15.7
|
||||
- uses: cargo-bins/cargo-binstall@afcf9780305558bcc9e4bc94b7589ab2bb8b6106 # v1.15.9
|
||||
- run: cargo binstall --no-confirm cargo-shear
|
||||
- run: cargo shear
|
||||
|
||||
|
||||
18
.github/workflows/release.yml
vendored
18
.github/workflows/release.yml
vendored
@@ -70,7 +70,7 @@ jobs:
|
||||
shell: bash
|
||||
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.30.0/cargo-dist-installer.sh | sh"
|
||||
- name: Cache dist
|
||||
uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
|
||||
with:
|
||||
name: cargo-dist-cache
|
||||
path: ~/.cargo/bin/dist
|
||||
@@ -86,7 +86,7 @@ jobs:
|
||||
cat plan-dist-manifest.json
|
||||
echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT"
|
||||
- name: "Upload dist-manifest.json"
|
||||
uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
|
||||
with:
|
||||
name: artifacts-plan-dist-manifest
|
||||
path: plan-dist-manifest.json
|
||||
@@ -128,14 +128,14 @@ jobs:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Install cached dist
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53
|
||||
with:
|
||||
name: cargo-dist-cache
|
||||
path: ~/.cargo/bin/
|
||||
- run: chmod +x ~/.cargo/bin/dist
|
||||
# Get all the local artifacts for the global tasks to use (for e.g. checksums)
|
||||
- name: Fetch local artifacts
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53
|
||||
with:
|
||||
pattern: artifacts-*
|
||||
path: target/distrib/
|
||||
@@ -153,7 +153,7 @@ jobs:
|
||||
|
||||
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
|
||||
- name: "Upload artifacts"
|
||||
uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
|
||||
with:
|
||||
name: artifacts-build-global
|
||||
path: |
|
||||
@@ -179,14 +179,14 @@ jobs:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Install cached dist
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53
|
||||
with:
|
||||
name: cargo-dist-cache
|
||||
path: ~/.cargo/bin/
|
||||
- run: chmod +x ~/.cargo/bin/dist
|
||||
# Fetch artifacts from scratch-storage
|
||||
- name: Fetch artifacts
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53
|
||||
with:
|
||||
pattern: artifacts-*
|
||||
path: target/distrib/
|
||||
@@ -200,7 +200,7 @@ jobs:
|
||||
cat dist-manifest.json
|
||||
echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
|
||||
- name: "Upload dist-manifest.json"
|
||||
uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
|
||||
with:
|
||||
# Overwrite the previous copy
|
||||
name: artifacts-dist-manifest
|
||||
@@ -256,7 +256,7 @@ jobs:
|
||||
submodules: recursive
|
||||
# Create a GitHub Release while uploading all files to it
|
||||
- name: "Download GitHub Artifacts"
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53
|
||||
with:
|
||||
pattern: artifacts-*
|
||||
path: artifacts
|
||||
|
||||
2
.github/zizmor.yml
vendored
2
.github/zizmor.yml
vendored
@@ -1,5 +1,5 @@
|
||||
# Configuration for the zizmor static analysis tool, run via pre-commit in CI
|
||||
# https://woodruffw.github.io/zizmor/configuration/
|
||||
# https://docs.zizmor.sh/configuration/
|
||||
#
|
||||
# TODO: can we remove the ignores here so that our workflows are more secure?
|
||||
rules:
|
||||
|
||||
@@ -102,7 +102,7 @@ repos:
|
||||
# zizmor detects security vulnerabilities in GitHub Actions workflows.
|
||||
# Additional configuration for the tool is found in `.github/zizmor.yml`
|
||||
- repo: https://github.com/zizmorcore/zizmor-pre-commit
|
||||
rev: v1.15.2
|
||||
rev: v1.16.0
|
||||
hooks:
|
||||
- id: zizmor
|
||||
|
||||
|
||||
55
CHANGELOG.md
55
CHANGELOG.md
@@ -1,5 +1,60 @@
|
||||
# Changelog
|
||||
|
||||
## 0.14.3
|
||||
|
||||
Released on 2025-10-30.
|
||||
|
||||
### Preview features
|
||||
|
||||
- Respect `--output-format` with `--watch` ([#21097](https://github.com/astral-sh/ruff/pull/21097))
|
||||
- \[`pydoclint`\] Fix false positive on explicit exception re-raising (`DOC501`, `DOC502`) ([#21011](https://github.com/astral-sh/ruff/pull/21011))
|
||||
- \[`pyflakes`\] Revert to stable behavior if imports for module lie in alternate branches for `F401` ([#20878](https://github.com/astral-sh/ruff/pull/20878))
|
||||
- \[`pylint`\] Implement `stop-iteration-return` (`PLR1708`) ([#20733](https://github.com/astral-sh/ruff/pull/20733))
|
||||
- \[`ruff`\] Add support for additional eager conversion patterns (`RUF065`) ([#20657](https://github.com/astral-sh/ruff/pull/20657))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Fix finding keyword range for clause header after statement ending with semicolon ([#21067](https://github.com/astral-sh/ruff/pull/21067))
|
||||
- Fix syntax error false positive on nested alternative patterns ([#21104](https://github.com/astral-sh/ruff/pull/21104))
|
||||
- \[`ISC001`\] Fix panic when string literals are unclosed ([#21034](https://github.com/astral-sh/ruff/pull/21034))
|
||||
- \[`flake8-django`\] Apply `DJ001` to annotated fields ([#20907](https://github.com/astral-sh/ruff/pull/20907))
|
||||
- \[`flake8-pyi`\] Fix `PYI034` to not trigger on metaclasses (`PYI034`) ([#20881](https://github.com/astral-sh/ruff/pull/20881))
|
||||
- \[`flake8-type-checking`\] Fix `TC003` false positive with `future-annotations` ([#21125](https://github.com/astral-sh/ruff/pull/21125))
|
||||
- \[`pyflakes`\] Fix false positive for `__class__` in lambda expressions within class definitions (`F821`) ([#20564](https://github.com/astral-sh/ruff/pull/20564))
|
||||
- \[`pyupgrade`\] Fix false positive for `TypeVar` with default on Python \<3.13 (`UP046`,`UP047`) ([#21045](https://github.com/astral-sh/ruff/pull/21045))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- Add missing docstring sections to the numpy list ([#20931](https://github.com/astral-sh/ruff/pull/20931))
|
||||
- \[`airflow`\] Extend `airflow.models..Param` check (`AIR311`) ([#21043](https://github.com/astral-sh/ruff/pull/21043))
|
||||
- \[`airflow`\] Warn that `airflow....DAG.create_dagrun` has been removed (`AIR301`) ([#21093](https://github.com/astral-sh/ruff/pull/21093))
|
||||
- \[`refurb`\] Preserve digit separators in `Decimal` constructor (`FURB157`) ([#20588](https://github.com/astral-sh/ruff/pull/20588))
|
||||
|
||||
### Server
|
||||
|
||||
- Avoid sending an unnecessary "clear diagnostics" message for clients supporting pull diagnostics ([#21105](https://github.com/astral-sh/ruff/pull/21105))
|
||||
|
||||
### Documentation
|
||||
|
||||
- \[`flake8-bandit`\] Fix correct example for `S308` ([#21128](https://github.com/astral-sh/ruff/pull/21128))
|
||||
|
||||
### Other changes
|
||||
|
||||
- Clearer error message when `line-length` goes beyond threshold ([#21072](https://github.com/astral-sh/ruff/pull/21072))
|
||||
|
||||
### Contributors
|
||||
|
||||
- [@danparizher](https://github.com/danparizher)
|
||||
- [@jvacek](https://github.com/jvacek)
|
||||
- [@ntBre](https://github.com/ntBre)
|
||||
- [@augustelalande](https://github.com/augustelalande)
|
||||
- [@prakhar1144](https://github.com/prakhar1144)
|
||||
- [@TaKO8Ki](https://github.com/TaKO8Ki)
|
||||
- [@dylwil3](https://github.com/dylwil3)
|
||||
- [@fatelei](https://github.com/fatelei)
|
||||
- [@ShaharNaveh](https://github.com/ShaharNaveh)
|
||||
- [@Lee-W](https://github.com/Lee-W)
|
||||
|
||||
## 0.14.2
|
||||
|
||||
Released on 2025-10-23.
|
||||
|
||||
75
Cargo.lock
generated
75
Cargo.lock
generated
@@ -243,7 +243,7 @@ dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"itertools 0.10.5",
|
||||
"itertools 0.13.0",
|
||||
"log",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
@@ -295,9 +295,9 @@ checksum = "36f64beae40a84da1b4b26ff2761a5b895c12adc41dc25aaee1c4f2bbfe97a6e"
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "1.12.0"
|
||||
version = "1.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4"
|
||||
checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
@@ -433,9 +433,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.49"
|
||||
version = "4.5.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4512b90fa68d3a9932cea5184017c5d200f5921df706d45e853537dea51508f"
|
||||
checksum = "0c2cfd7bf8a6017ddaa4e32ffe7403d547790db06bd171c1c53926faab501623"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -443,9 +443,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.49"
|
||||
version = "4.5.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0025e98baa12e766c67ba13ff4695a887a1eba19569aad00a472546795bd6730"
|
||||
checksum = "0a4c05b9e80c5ccd3a7ef080ad7b6ba7d6fc00a985b8b157197075677c82c7a0"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -633,7 +633,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]]
|
||||
@@ -642,7 +642,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]]
|
||||
@@ -1007,7 +1007,7 @@ dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1093,7 +1093,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1115,13 +1115,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "etcetera"
|
||||
version = "0.10.0"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26c7b13d0780cb82722fd59f6f57f925e143427e4a75313a6c77243bf5326ae6"
|
||||
checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"home",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1224,9 +1223,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "get-size-derive2"
|
||||
version = "0.7.0"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3814abc7da8ab18d2fd820f5b540b5e39b6af0a32de1bdd7c47576693074843"
|
||||
checksum = "46b134aa084df7c3a513a1035c52f623e4b3065dfaf3d905a4f28a2e79b5bb3f"
|
||||
dependencies = [
|
||||
"attribute-derive",
|
||||
"quote",
|
||||
@@ -1235,9 +1234,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "get-size2"
|
||||
version = "0.7.0"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5dfe2cec5b5ce8fb94dcdb16a1708baa4d0609cc3ce305ca5d3f6f2ffb59baed"
|
||||
checksum = "c0d51c9f2e956a517619ad9e7eaebc7a573f9c49b38152e12eade750f89156f9"
|
||||
dependencies = [
|
||||
"compact_str",
|
||||
"get-size-derive2",
|
||||
@@ -1366,15 +1365,6 @@ version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
||||
|
||||
[[package]]
|
||||
name = "home"
|
||||
version = "0.5.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "html-escape"
|
||||
version = "0.2.13"
|
||||
@@ -1523,9 +1513,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ignore"
|
||||
version = "0.4.23"
|
||||
version = "0.4.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b"
|
||||
checksum = "81776e6f9464432afcc28d03e52eb101c93b6f0566f52aef2427663e700f0403"
|
||||
dependencies = [
|
||||
"crossbeam-deque",
|
||||
"globset",
|
||||
@@ -1563,7 +1553,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.15.5",
|
||||
"hashbrown 0.16.0",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
@@ -1690,7 +1680,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1754,7 +1744,7 @@ dependencies = [
|
||||
"portable-atomic",
|
||||
"portable-atomic-util",
|
||||
"serde",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2835,7 +2825,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.14.2"
|
||||
version = "0.14.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -3092,7 +3082,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.14.2"
|
||||
version = "0.14.3"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
@@ -3447,7 +3437,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_wasm"
|
||||
version = "0.14.2"
|
||||
version = "0.14.3"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
@@ -3545,7 +3535,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3563,7 +3553,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||
[[package]]
|
||||
name = "salsa"
|
||||
version = "0.24.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=d38145c29574758de7ffbe8a13cd4584c3b09161#d38145c29574758de7ffbe8a13cd4584c3b09161"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=cdd0b85516a52c18b8a6d17a2279a96ed6c3e198#cdd0b85516a52c18b8a6d17a2279a96ed6c3e198"
|
||||
dependencies = [
|
||||
"boxcar",
|
||||
"compact_str",
|
||||
@@ -3587,12 +3577,12 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "salsa-macro-rules"
|
||||
version = "0.24.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=d38145c29574758de7ffbe8a13cd4584c3b09161#d38145c29574758de7ffbe8a13cd4584c3b09161"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=cdd0b85516a52c18b8a6d17a2279a96ed6c3e198#cdd0b85516a52c18b8a6d17a2279a96ed6c3e198"
|
||||
|
||||
[[package]]
|
||||
name = "salsa-macros"
|
||||
version = "0.24.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=d38145c29574758de7ffbe8a13cd4584c3b09161#d38145c29574758de7ffbe8a13cd4584c3b09161"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=cdd0b85516a52c18b8a6d17a2279a96ed6c3e198#cdd0b85516a52c18b8a6d17a2279a96ed6c3e198"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3941,7 +3931,7 @@ dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4521,7 +4511,6 @@ name = "ty_test"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.9.4",
|
||||
"camino",
|
||||
"colored 3.0.0",
|
||||
"insta",
|
||||
@@ -5022,7 +5011,7 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -5,7 +5,7 @@ resolver = "2"
|
||||
[workspace.package]
|
||||
# Please update rustfmt.toml when bumping the Rust edition
|
||||
edition = "2024"
|
||||
rust-version = "1.88"
|
||||
rust-version = "1.89"
|
||||
homepage = "https://docs.astral.sh/ruff"
|
||||
documentation = "https://docs.astral.sh/ruff"
|
||||
repository = "https://github.com/astral-sh/ruff"
|
||||
@@ -84,7 +84,7 @@ dashmap = { version = "6.0.1" }
|
||||
dir-test = { version = "0.4.0" }
|
||||
dunce = { version = "1.0.5" }
|
||||
drop_bomb = { version = "0.1.5" }
|
||||
etcetera = { version = "0.10.0" }
|
||||
etcetera = { version = "0.11.0" }
|
||||
fern = { version = "0.7.0" }
|
||||
filetime = { version = "0.2.23" }
|
||||
getrandom = { version = "0.3.1" }
|
||||
@@ -146,7 +146,7 @@ regex-automata = { version = "0.4.9" }
|
||||
rustc-hash = { version = "2.0.0" }
|
||||
rustc-stable-hash = { version = "0.1.2" }
|
||||
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "d38145c29574758de7ffbe8a13cd4584c3b09161", default-features = false, features = [
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "cdd0b85516a52c18b8a6d17a2279a96ed6c3e198", default-features = false, features = [
|
||||
"compact_str",
|
||||
"macros",
|
||||
"salsa_unstable",
|
||||
|
||||
@@ -147,8 +147,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.14.2/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.14.2/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.14.3/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.14.3/install.ps1 | iex"
|
||||
```
|
||||
|
||||
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
|
||||
@@ -181,7 +181,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.14.2
|
||||
rev: v0.14.3
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff-check
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.14.2"
|
||||
version = "0.14.3"
|
||||
publish = true
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -7,6 +7,7 @@ use path_absolutize::CWD;
|
||||
use ruff_db::system::{SystemPath, SystemPathBuf};
|
||||
use ruff_graph::{Direction, ImportMap, ModuleDb, ModuleImports};
|
||||
use ruff_linter::package::PackageRoot;
|
||||
use ruff_linter::source_kind::SourceKind;
|
||||
use ruff_linter::{warn_user, warn_user_once};
|
||||
use ruff_python_ast::{PySourceType, SourceType};
|
||||
use ruff_workspace::resolver::{ResolvedFile, match_exclusion, python_files_in_path};
|
||||
@@ -127,10 +128,6 @@ pub(crate) fn analyze_graph(
|
||||
},
|
||||
Some(language) => PySourceType::from(language),
|
||||
};
|
||||
if matches!(source_type, PySourceType::Ipynb) {
|
||||
debug!("Ignoring Jupyter notebook: {}", path.display());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Convert to system paths.
|
||||
let Ok(package) = package.map(SystemPathBuf::from_path_buf).transpose() else {
|
||||
@@ -147,13 +144,34 @@ pub(crate) fn analyze_graph(
|
||||
let root = root.clone();
|
||||
let result = inner_result.clone();
|
||||
scope.spawn(move |_| {
|
||||
// Extract source code (handles both .py and .ipynb files)
|
||||
let source_kind = match SourceKind::from_path(path.as_std_path(), source_type) {
|
||||
Ok(Some(source_kind)) => source_kind,
|
||||
Ok(None) => {
|
||||
debug!("Skipping non-Python notebook: {path}");
|
||||
return;
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("Failed to read source for {path}: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let source_code = source_kind.source_code();
|
||||
|
||||
// Identify any imports via static analysis.
|
||||
let mut imports =
|
||||
ModuleImports::detect(&db, &path, package.as_deref(), string_imports)
|
||||
.unwrap_or_else(|err| {
|
||||
warn!("Failed to generate import map for {path}: {err}");
|
||||
ModuleImports::default()
|
||||
});
|
||||
let mut imports = ModuleImports::detect(
|
||||
&db,
|
||||
source_code,
|
||||
source_type,
|
||||
&path,
|
||||
package.as_deref(),
|
||||
string_imports,
|
||||
)
|
||||
.unwrap_or_else(|err| {
|
||||
warn!("Failed to generate import map for {path}: {err}");
|
||||
ModuleImports::default()
|
||||
});
|
||||
|
||||
debug!("Discovered {} imports for {}", imports.len(), path);
|
||||
|
||||
|
||||
@@ -370,7 +370,7 @@ pub(crate) fn format_source(
|
||||
let line_index = LineIndex::from_source_text(unformatted);
|
||||
let byte_range = range.to_text_range(unformatted, &line_index);
|
||||
format_range(unformatted, byte_range, options).map(|formatted_range| {
|
||||
let mut formatted = unformatted.to_string();
|
||||
let mut formatted = unformatted.clone();
|
||||
formatted.replace_range(
|
||||
std::ops::Range::<usize>::from(formatted_range.source_range()),
|
||||
formatted_range.as_code(),
|
||||
|
||||
@@ -9,9 +9,7 @@ use itertools::{Itertools, iterate};
|
||||
use ruff_linter::linter::FixTable;
|
||||
use serde::Serialize;
|
||||
|
||||
use ruff_db::diagnostic::{
|
||||
Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig, DisplayDiagnostics, SecondaryCode,
|
||||
};
|
||||
use ruff_db::diagnostic::{Diagnostic, DisplayDiagnosticConfig, SecondaryCode};
|
||||
use ruff_linter::fs::relativize_path;
|
||||
use ruff_linter::logging::LogLevel;
|
||||
use ruff_linter::message::{EmitterContext, render_diagnostics};
|
||||
@@ -390,21 +388,18 @@ impl Printer {
|
||||
|
||||
let context = EmitterContext::new(&diagnostics.notebook_indexes);
|
||||
let format = if preview {
|
||||
DiagnosticFormat::Full
|
||||
self.format
|
||||
} else {
|
||||
DiagnosticFormat::Concise
|
||||
OutputFormat::Concise
|
||||
};
|
||||
let config = DisplayDiagnosticConfig::default()
|
||||
.preview(preview)
|
||||
.hide_severity(true)
|
||||
.color(!cfg!(test) && colored::control::SHOULD_COLORIZE.should_colorize())
|
||||
.with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref()))
|
||||
.format(format)
|
||||
.with_fix_applicability(self.unsafe_fixes.required_applicability());
|
||||
write!(
|
||||
writer,
|
||||
"{}",
|
||||
DisplayDiagnostics::new(&context, &config, &diagnostics.inner)
|
||||
)?;
|
||||
.with_fix_applicability(self.unsafe_fixes.required_applicability())
|
||||
.show_fix_diff(preview);
|
||||
render_diagnostics(writer, format, config, &context, &diagnostics.inner)?;
|
||||
}
|
||||
writer.flush()?;
|
||||
|
||||
|
||||
@@ -653,3 +653,133 @@ fn venv() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn notebook_basic() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let root = ChildPath::new(tempdir.path());
|
||||
|
||||
root.child("ruff").child("__init__.py").write_str("")?;
|
||||
root.child("ruff")
|
||||
.child("a.py")
|
||||
.write_str(indoc::indoc! {r#"
|
||||
def helper():
|
||||
pass
|
||||
"#})?;
|
||||
|
||||
// Create a basic notebook with a simple import
|
||||
root.child("notebook.ipynb").write_str(indoc::indoc! {r#"
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from ruff.a import helper"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"language_info": {
|
||||
"name": "python",
|
||||
"version": "3.12.0"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
"#})?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => INSTA_FILTERS.to_vec(),
|
||||
}, {
|
||||
assert_cmd_snapshot!(command().current_dir(&root), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
{
|
||||
"notebook.ipynb": [
|
||||
"ruff/a.py"
|
||||
],
|
||||
"ruff/__init__.py": [],
|
||||
"ruff/a.py": []
|
||||
}
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn notebook_with_magic() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let root = ChildPath::new(tempdir.path());
|
||||
|
||||
root.child("ruff").child("__init__.py").write_str("")?;
|
||||
root.child("ruff")
|
||||
.child("a.py")
|
||||
.write_str(indoc::indoc! {r#"
|
||||
def helper():
|
||||
pass
|
||||
"#})?;
|
||||
|
||||
// Create a notebook with IPython magic commands and imports
|
||||
root.child("notebook.ipynb").write_str(indoc::indoc! {r#"
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%load_ext autoreload\n",
|
||||
"%autoreload 2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from ruff.a import helper"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"language_info": {
|
||||
"name": "python",
|
||||
"version": "3.12.0"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
"#})?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => INSTA_FILTERS.to_vec(),
|
||||
}, {
|
||||
assert_cmd_snapshot!(command().current_dir(&root), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
{
|
||||
"notebook.ipynb": [
|
||||
"ruff/a.py"
|
||||
],
|
||||
"ruff/__init__.py": [],
|
||||
"ruff/a.py": []
|
||||
}
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -226,7 +226,7 @@ static STATIC_FRAME: Benchmark = Benchmark::new(
|
||||
max_dep_date: "2025-08-09",
|
||||
python_version: PythonVersion::PY311,
|
||||
},
|
||||
750,
|
||||
800,
|
||||
);
|
||||
|
||||
#[track_caller]
|
||||
|
||||
@@ -470,6 +470,11 @@ impl File {
|
||||
self.source_type(db).is_stub()
|
||||
}
|
||||
|
||||
/// Returns `true` if the file is an `__init__.pyi`
|
||||
pub fn is_package_stub(self, db: &dyn Db) -> bool {
|
||||
self.path(db).as_str().ends_with("__init__.pyi")
|
||||
}
|
||||
|
||||
pub fn source_type(self, db: &dyn Db) -> PySourceType {
|
||||
match self.path(db) {
|
||||
FilePath::System(path) => path
|
||||
|
||||
@@ -200,7 +200,12 @@ impl System for OsSystem {
|
||||
/// The walker ignores files according to [`ignore::WalkBuilder::standard_filters`]
|
||||
/// when setting [`WalkDirectoryBuilder::standard_filters`] to true.
|
||||
fn walk_directory(&self, path: &SystemPath) -> WalkDirectoryBuilder {
|
||||
WalkDirectoryBuilder::new(path, OsDirectoryWalker {})
|
||||
WalkDirectoryBuilder::new(
|
||||
path,
|
||||
OsDirectoryWalker {
|
||||
cwd: self.current_directory().to_path_buf(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn glob(
|
||||
@@ -454,7 +459,9 @@ struct ListedDirectory {
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct OsDirectoryWalker;
|
||||
struct OsDirectoryWalker {
|
||||
cwd: SystemPathBuf,
|
||||
}
|
||||
|
||||
impl DirectoryWalker for OsDirectoryWalker {
|
||||
fn walk(
|
||||
@@ -473,6 +480,7 @@ impl DirectoryWalker for OsDirectoryWalker {
|
||||
};
|
||||
|
||||
let mut builder = ignore::WalkBuilder::new(first.as_std_path());
|
||||
builder.current_dir(self.cwd.as_std_path());
|
||||
|
||||
builder.standard_filters(standard_filters);
|
||||
builder.hidden(hidden);
|
||||
|
||||
@@ -723,10 +723,11 @@ impl ruff_cache::CacheKey for SystemPathBuf {
|
||||
|
||||
/// A slice of a virtual path on [`System`](super::System) (akin to [`str`]).
|
||||
#[repr(transparent)]
|
||||
#[derive(Eq, PartialEq, Hash, PartialOrd, Ord)]
|
||||
pub struct SystemVirtualPath(str);
|
||||
|
||||
impl SystemVirtualPath {
|
||||
pub fn new(path: &str) -> &SystemVirtualPath {
|
||||
pub const fn new(path: &str) -> &SystemVirtualPath {
|
||||
// SAFETY: SystemVirtualPath is marked as #[repr(transparent)] so the conversion from a
|
||||
// *const str to a *const SystemVirtualPath is valid.
|
||||
unsafe { &*(path as *const str as *const SystemVirtualPath) }
|
||||
@@ -767,8 +768,8 @@ pub struct SystemVirtualPathBuf(String);
|
||||
|
||||
impl SystemVirtualPathBuf {
|
||||
#[inline]
|
||||
pub fn as_path(&self) -> &SystemVirtualPath {
|
||||
SystemVirtualPath::new(&self.0)
|
||||
pub const fn as_path(&self) -> &SystemVirtualPath {
|
||||
SystemVirtualPath::new(self.0.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -852,6 +853,12 @@ impl ruff_cache::CacheKey for SystemVirtualPathBuf {
|
||||
}
|
||||
}
|
||||
|
||||
impl Borrow<SystemVirtualPath> for SystemVirtualPathBuf {
|
||||
fn borrow(&self) -> &SystemVirtualPath {
|
||||
self.as_path()
|
||||
}
|
||||
}
|
||||
|
||||
/// Deduplicates identical paths and removes nested paths.
|
||||
///
|
||||
/// # Examples
|
||||
|
||||
@@ -62,7 +62,7 @@ fn generate_set(output: &mut String, set: Set, parents: &mut Vec<Set>) {
|
||||
generate_set(
|
||||
output,
|
||||
Set::Named {
|
||||
name: set_name.to_string(),
|
||||
name: set_name.clone(),
|
||||
set: *sub_set,
|
||||
},
|
||||
parents,
|
||||
|
||||
@@ -104,7 +104,7 @@ fn generate_set(output: &mut String, set: Set, parents: &mut Vec<Set>) {
|
||||
generate_set(
|
||||
output,
|
||||
Set::Named {
|
||||
name: set_name.to_string(),
|
||||
name: set_name.clone(),
|
||||
set: *sub_set,
|
||||
},
|
||||
parents,
|
||||
|
||||
@@ -1006,7 +1006,7 @@ impl<Context> std::fmt::Debug for Align<'_, Context> {
|
||||
/// Block indents indent a block of code, such as in a function body, and therefore insert a line
|
||||
/// break before and after the content.
|
||||
///
|
||||
/// Doesn't create an indentation if the passed in content is [`FormatElement.is_empty`].
|
||||
/// Doesn't create an indentation if the passed in content is empty.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
|
||||
@@ -3,8 +3,9 @@ use std::collections::{BTreeMap, BTreeSet};
|
||||
use anyhow::Result;
|
||||
|
||||
use ruff_db::system::{SystemPath, SystemPathBuf};
|
||||
use ruff_python_ast::PySourceType;
|
||||
use ruff_python_ast::helpers::to_module_path;
|
||||
use ruff_python_parser::{Mode, ParseOptions, parse};
|
||||
use ruff_python_parser::{ParseOptions, parse};
|
||||
|
||||
use crate::collector::Collector;
|
||||
pub use crate::db::ModuleDb;
|
||||
@@ -24,13 +25,14 @@ impl ModuleImports {
|
||||
/// Detect the [`ModuleImports`] for a given Python file.
|
||||
pub fn detect(
|
||||
db: &ModuleDb,
|
||||
source: &str,
|
||||
source_type: PySourceType,
|
||||
path: &SystemPath,
|
||||
package: Option<&SystemPath>,
|
||||
string_imports: StringImports,
|
||||
) -> Result<Self> {
|
||||
// Read and parse the source code.
|
||||
let source = std::fs::read_to_string(path)?;
|
||||
let parsed = parse(&source, ParseOptions::from(Mode::Module))?;
|
||||
// Parse the source code.
|
||||
let parsed = parse(source, ParseOptions::from(source_type))?;
|
||||
|
||||
let module_path =
|
||||
package.and_then(|package| to_module_path(package.as_std_path(), path.as_std_path()));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.14.2"
|
||||
version = "0.14.3"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -10,6 +10,7 @@ from airflow.datasets import (
|
||||
)
|
||||
from airflow.datasets.manager import DatasetManager
|
||||
from airflow.lineage.hook import DatasetLineageInfo, HookLineageCollector
|
||||
from airflow.models.dag import DAG
|
||||
from airflow.providers.amazon.aws.auth_manager.aws_auth_manager import AwsAuthManager
|
||||
from airflow.providers.apache.beam.hooks import BeamHook, NotAir302HookError
|
||||
from airflow.providers.google.cloud.secrets.secret_manager import (
|
||||
@@ -20,6 +21,7 @@ from airflow.providers_manager import ProvidersManager
|
||||
from airflow.secrets.base_secrets import BaseSecretsBackend
|
||||
from airflow.secrets.local_filesystem import LocalFilesystemBackend
|
||||
|
||||
|
||||
# airflow.Dataset
|
||||
dataset_from_root = DatasetFromRoot()
|
||||
dataset_from_root.iter_datasets()
|
||||
@@ -56,6 +58,10 @@ hlc.add_input_dataset()
|
||||
hlc.add_output_dataset()
|
||||
hlc.collected_datasets()
|
||||
|
||||
# airflow.models.dag.DAG
|
||||
test_dag = DAG(dag_id="test_dag")
|
||||
test_dag.create_dagrun()
|
||||
|
||||
# airflow.providers.amazon.auth_manager.aws_auth_manager
|
||||
aam = AwsAuthManager()
|
||||
aam.is_authorized_dataset()
|
||||
@@ -96,3 +102,15 @@ base_secret_backend.get_connections()
|
||||
# airflow.secrets.local_filesystem
|
||||
lfb = LocalFilesystemBackend()
|
||||
lfb.get_connections()
|
||||
|
||||
from airflow.models import DAG
|
||||
|
||||
# airflow.DAG
|
||||
test_dag = DAG(dag_id="test_dag")
|
||||
test_dag.create_dagrun()
|
||||
|
||||
from airflow import DAG
|
||||
|
||||
# airflow.DAG
|
||||
test_dag = DAG(dag_id="test_dag")
|
||||
test_dag.create_dagrun()
|
||||
|
||||
@@ -91,10 +91,20 @@ get_unique_task_id()
|
||||
task_decorator_factory()
|
||||
|
||||
|
||||
from airflow.models import Param
|
||||
from airflow.models import DagParam, Param, ParamsDict
|
||||
|
||||
# airflow.models
|
||||
Param()
|
||||
DagParam()
|
||||
ParamsDict()
|
||||
|
||||
|
||||
from airflow.models.param import DagParam, Param, ParamsDict
|
||||
|
||||
# airflow.models.param
|
||||
Param()
|
||||
DagParam()
|
||||
ParamsDict()
|
||||
|
||||
|
||||
from airflow.sensors.base import (
|
||||
|
||||
@@ -46,3 +46,9 @@ class CorrectModel(models.Model):
|
||||
max_length=255, null=True, blank=True, unique=True
|
||||
)
|
||||
urlfieldu = models.URLField(max_length=255, null=True, blank=True, unique=True)
|
||||
|
||||
|
||||
class IncorrectModelWithSimpleAnnotations(models.Model):
|
||||
charfield: models.CharField = models.CharField(max_length=255, null=True)
|
||||
textfield: models.TextField = models.TextField(max_length=255, null=True)
|
||||
slugfield: models.SlugField = models.SlugField(max_length=255, null=True)
|
||||
|
||||
7
crates/ruff_linter/resources/test/fixtures/flake8_implicit_str_concat/ISC_syntax_error_2.py
vendored
Normal file
7
crates/ruff_linter/resources/test/fixtures/flake8_implicit_str_concat/ISC_syntax_error_2.py
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
# Regression test for https://github.com/astral-sh/ruff/issues/21023
|
||||
'' '
|
||||
"" ""
|
||||
'' '' '
|
||||
"" "" "
|
||||
f"" f"
|
||||
f"" f"" f"
|
||||
@@ -359,3 +359,29 @@ class Generic5(list[PotentialTypeVar]):
|
||||
def __new__(cls: type[Generic5]) -> Generic5: ...
|
||||
def __enter__(self: Generic5) -> Generic5: ...
|
||||
|
||||
|
||||
# Test cases based on issue #20781 - metaclasses that triggers IsMetaclass::Maybe
|
||||
class MetaclassInWhichSelfCannotBeUsed5(type(Protocol)):
|
||||
def __new__(
|
||||
cls, name: str, bases: tuple[type[Any], ...], attrs: dict[str, Any], **kwargs: Any
|
||||
) -> MetaclassInWhichSelfCannotBeUsed5:
|
||||
new_class = super().__new__(cls, name, bases, attrs, **kwargs)
|
||||
return new_class
|
||||
|
||||
|
||||
import django.db.models.base
|
||||
|
||||
|
||||
class MetaclassInWhichSelfCannotBeUsed6(django.db.models.base.ModelBase):
|
||||
def __new__(cls, name: str, bases: tuple[Any, ...], attrs: dict[str, Any], **kwargs: Any) -> MetaclassInWhichSelfCannotBeUsed6:
|
||||
...
|
||||
|
||||
|
||||
class MetaclassInWhichSelfCannotBeUsed7(django.db.models.base.ModelBase):
|
||||
def __new__(cls, /, name: str, bases: tuple[object, ...], attrs: dict[str, object], **kwds: object) -> MetaclassInWhichSelfCannotBeUsed7:
|
||||
...
|
||||
|
||||
|
||||
class MetaclassInWhichSelfCannotBeUsed8(django.db.models.base.ModelBase):
|
||||
def __new__(cls, name: builtins.str, bases: tuple, attributes: dict, /, **kw) -> MetaclassInWhichSelfCannotBeUsed8:
|
||||
...
|
||||
|
||||
@@ -252,3 +252,28 @@ from some_module import PotentialTypeVar
|
||||
class Generic5(list[PotentialTypeVar]):
|
||||
def __new__(cls: type[Generic5]) -> Generic5: ...
|
||||
def __enter__(self: Generic5) -> Generic5: ...
|
||||
|
||||
|
||||
# Test case based on issue #20781 - metaclass that triggers IsMetaclass::Maybe
|
||||
class MetaclassInWhichSelfCannotBeUsed5(type(Protocol)):
|
||||
def __new__(
|
||||
cls, name: str, bases: tuple[type[Any], ...], attrs: dict[str, Any], **kwargs: Any
|
||||
) -> MetaclassInWhichSelfCannotBeUsed5: ...
|
||||
|
||||
|
||||
import django.db.models.base
|
||||
|
||||
|
||||
class MetaclassInWhichSelfCannotBeUsed6(django.db.models.base.ModelBase):
|
||||
def __new__(cls, name: str, bases: tuple[Any, ...], attrs: dict[str, Any], **kwargs: Any) -> MetaclassInWhichSelfCannotBeUsed6:
|
||||
...
|
||||
|
||||
|
||||
class MetaclassInWhichSelfCannotBeUsed7(django.db.models.base.ModelBase):
|
||||
def __new__(cls, /, name: str, bases: tuple[object, ...], attrs: dict[str, object], **kwds: object) -> MetaclassInWhichSelfCannotBeUsed7:
|
||||
...
|
||||
|
||||
|
||||
class MetaclassInWhichSelfCannotBeUsed8(django.db.models.base.ModelBase):
|
||||
def __new__(cls, name: builtins.str, bases: tuple, attributes: dict, /, **kw) -> MetaclassInWhichSelfCannotBeUsed8:
|
||||
...
|
||||
|
||||
@@ -14,3 +14,14 @@ def f():
|
||||
import os
|
||||
|
||||
print(os)
|
||||
|
||||
|
||||
# regression test for https://github.com/astral-sh/ruff/issues/21121
|
||||
from dataclasses import KW_ONLY, dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class DataClass:
|
||||
a: int
|
||||
_: KW_ONLY # should be an exception to TC003, even with future-annotations
|
||||
b: int
|
||||
|
||||
@@ -370,3 +370,22 @@ class Foo:
|
||||
The flag converter instance with all flags parsed.
|
||||
"""
|
||||
return
|
||||
|
||||
# OK
|
||||
def baz(x: int) -> int:
|
||||
"""
|
||||
Show a `Warnings` DOC102 false positive.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x : int
|
||||
|
||||
Warnings
|
||||
--------
|
||||
This function demonstrates a DOC102 false positive
|
||||
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
"""
|
||||
return x
|
||||
|
||||
@@ -81,3 +81,55 @@ def calculate_speed(distance: float, time: float) -> float:
|
||||
except TypeError:
|
||||
print("Not a number? Shame on you!")
|
||||
raise
|
||||
|
||||
|
||||
# This should NOT trigger DOC502 because OSError is explicitly re-raised
|
||||
def f():
|
||||
"""Do nothing.
|
||||
|
||||
Raises:
|
||||
OSError: If the OS errors.
|
||||
"""
|
||||
try:
|
||||
pass
|
||||
except OSError as e:
|
||||
raise e
|
||||
|
||||
|
||||
# This should NOT trigger DOC502 because OSError is explicitly re-raised with from None
|
||||
def g():
|
||||
"""Do nothing.
|
||||
|
||||
Raises:
|
||||
OSError: If the OS errors.
|
||||
"""
|
||||
try:
|
||||
pass
|
||||
except OSError as e:
|
||||
raise e from None
|
||||
|
||||
|
||||
# This should NOT trigger DOC502 because ValueError is explicitly re-raised from tuple exception
|
||||
def h():
|
||||
"""Do nothing.
|
||||
|
||||
Raises:
|
||||
ValueError: If something goes wrong.
|
||||
"""
|
||||
try:
|
||||
pass
|
||||
except (ValueError, TypeError) as e:
|
||||
raise e
|
||||
|
||||
|
||||
# This should NOT trigger DOC502 because TypeError is explicitly re-raised from tuple exception
|
||||
def i():
|
||||
"""Do nothing.
|
||||
|
||||
Raises:
|
||||
TypeError: If something goes wrong.
|
||||
"""
|
||||
try:
|
||||
pass
|
||||
except (ValueError, TypeError) as e:
|
||||
raise e
|
||||
|
||||
21
crates/ruff_linter/resources/test/fixtures/pyflakes/F821_33.py
vendored
Normal file
21
crates/ruff_linter/resources/test/fixtures/pyflakes/F821_33.py
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
class C:
|
||||
f = lambda self: __class__
|
||||
|
||||
|
||||
print(C().f().__name__)
|
||||
|
||||
# Test: nested lambda
|
||||
class D:
|
||||
g = lambda self: (lambda: __class__)
|
||||
|
||||
|
||||
print(D().g()().__name__)
|
||||
|
||||
# Test: lambda outside class (should still fail)
|
||||
h = lambda: __class__
|
||||
|
||||
# Test: lambda referencing module-level variable (should not be flagged as F821)
|
||||
import uuid
|
||||
|
||||
class E:
|
||||
uuid = lambda: str(uuid.uuid4())
|
||||
131
crates/ruff_linter/resources/test/fixtures/pylint/stop_iteration_return.py
vendored
Normal file
131
crates/ruff_linter/resources/test/fixtures/pylint/stop_iteration_return.py
vendored
Normal file
@@ -0,0 +1,131 @@
|
||||
"""Test cases for PLR1708 stop-iteration-return."""
|
||||
|
||||
|
||||
# Valid cases - should not trigger the rule
|
||||
def normal_function():
|
||||
raise StopIteration # Not a generator, should not trigger
|
||||
|
||||
|
||||
def normal_function_with_value():
|
||||
raise StopIteration("value") # Not a generator, should not trigger
|
||||
|
||||
|
||||
def generator_with_return():
|
||||
yield 1
|
||||
yield 2
|
||||
return "finished" # This is the correct way
|
||||
|
||||
|
||||
def generator_with_yield_from():
|
||||
yield from [1, 2, 3]
|
||||
|
||||
|
||||
def generator_without_stop_iteration():
|
||||
yield 1
|
||||
yield 2
|
||||
# No explicit termination
|
||||
|
||||
|
||||
def generator_with_other_exception():
|
||||
yield 1
|
||||
raise ValueError("something else") # Different exception
|
||||
|
||||
|
||||
# Invalid cases - should trigger the rule
|
||||
def generator_with_stop_iteration():
|
||||
yield 1
|
||||
yield 2
|
||||
raise StopIteration # Should trigger
|
||||
|
||||
|
||||
def generator_with_stop_iteration_value():
|
||||
yield 1
|
||||
yield 2
|
||||
raise StopIteration("finished") # Should trigger
|
||||
|
||||
|
||||
def generator_with_stop_iteration_expr():
|
||||
yield 1
|
||||
yield 2
|
||||
raise StopIteration(1 + 2) # Should trigger
|
||||
|
||||
|
||||
def async_generator_with_stop_iteration():
|
||||
yield 1
|
||||
yield 2
|
||||
raise StopIteration("async") # Should trigger
|
||||
|
||||
|
||||
def nested_generator():
|
||||
def inner_gen():
|
||||
yield 1
|
||||
raise StopIteration("inner") # Should trigger
|
||||
|
||||
yield from inner_gen()
|
||||
|
||||
|
||||
def generator_in_class():
|
||||
class MyClass:
|
||||
def generator_method(self):
|
||||
yield 1
|
||||
raise StopIteration("method") # Should trigger
|
||||
|
||||
return MyClass
|
||||
|
||||
|
||||
# Complex cases
|
||||
def complex_generator():
|
||||
try:
|
||||
yield 1
|
||||
yield 2
|
||||
raise StopIteration("complex") # Should trigger
|
||||
except ValueError:
|
||||
yield 3
|
||||
finally:
|
||||
pass
|
||||
|
||||
|
||||
def generator_with_conditional_stop_iteration(condition):
|
||||
yield 1
|
||||
if condition:
|
||||
raise StopIteration("conditional") # Should trigger
|
||||
yield 2
|
||||
|
||||
|
||||
# Edge cases
|
||||
def generator_with_bare_stop_iteration():
|
||||
yield 1
|
||||
raise StopIteration # Should trigger (no arguments)
|
||||
|
||||
|
||||
def generator_with_stop_iteration_in_loop():
|
||||
for i in range(5):
|
||||
yield i
|
||||
if i == 3:
|
||||
raise StopIteration("loop") # Should trigger
|
||||
|
||||
|
||||
# Should not trigger - different exceptions
|
||||
def generator_with_runtime_error():
|
||||
yield 1
|
||||
raise RuntimeError("not StopIteration") # Should not trigger
|
||||
|
||||
|
||||
def generator_with_custom_exception():
|
||||
yield 1
|
||||
raise CustomException("custom") # Should not trigger
|
||||
|
||||
|
||||
class CustomException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
# Generator comprehensions should not be affected
|
||||
list_comp = [x for x in range(10)] # Should not trigger
|
||||
|
||||
|
||||
# Lambda in generator context
|
||||
def generator_with_lambda():
|
||||
yield 1
|
||||
func = lambda x: x # Just a regular lambda
|
||||
yield 2
|
||||
17
crates/ruff_linter/resources/test/fixtures/pyupgrade/UP037_3.py
vendored
Normal file
17
crates/ruff_linter/resources/test/fixtures/pyupgrade/UP037_3.py
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
"""
|
||||
Regression test for an ecosystem hit on
|
||||
https://github.com/astral-sh/ruff/pull/21125.
|
||||
|
||||
We should mark all of the components of special dataclass annotations as
|
||||
runtime-required, not just the first layer.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import ClassVar, Optional
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EmptyCell:
|
||||
_singleton: ClassVar[Optional["EmptyCell"]] = None
|
||||
# the behavior of _singleton above should match a non-ClassVar
|
||||
_doubleton: "EmptyCell"
|
||||
13
crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046_2.py
vendored
Normal file
13
crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046_2.py
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
"""This is placed in a separate fixture as `TypeVar` needs to be imported
|
||||
from `typing_extensions` to support default arguments in Python version < 3.13.
|
||||
We verify that UP046 doesn't apply in this case.
|
||||
"""
|
||||
|
||||
from typing import Generic
|
||||
from typing_extensions import TypeVar
|
||||
|
||||
T = TypeVar("T", default=str)
|
||||
|
||||
|
||||
class DefaultTypeVar(Generic[T]):
|
||||
var: T
|
||||
12
crates/ruff_linter/resources/test/fixtures/pyupgrade/UP047_1.py
vendored
Normal file
12
crates/ruff_linter/resources/test/fixtures/pyupgrade/UP047_1.py
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
"""This is placed in a separate fixture as `TypeVar` needs to be imported
|
||||
from `typing_extensions` to support default arguments in Python version < 3.13.
|
||||
We verify that UP047 doesn't apply in this case.
|
||||
"""
|
||||
|
||||
from typing_extensions import TypeVar
|
||||
|
||||
T = TypeVar("T", default=int)
|
||||
|
||||
|
||||
def default_var(var: T) -> T:
|
||||
return var
|
||||
@@ -145,3 +145,11 @@ with open("file.txt", "w") as f:
|
||||
with open("file.txt", "w") as f:
|
||||
for line in text:
|
||||
f.write(line)
|
||||
|
||||
# See: https://github.com/astral-sh/ruff/issues/20785
|
||||
import json
|
||||
|
||||
data = {"price": 100}
|
||||
|
||||
with open("test.json", "wb") as f:
|
||||
f.write(json.dumps(data, indent=4).encode("utf-8"))
|
||||
@@ -69,3 +69,19 @@ Decimal(float("\N{space}\N{hyPHen-MINus}nan"))
|
||||
Decimal(float("\x20\N{character tabulation}\N{hyphen-minus}nan"))
|
||||
Decimal(float(" -" "nan"))
|
||||
Decimal(float("-nAn"))
|
||||
|
||||
# Test cases for digit separators (safe fixes)
|
||||
# https://github.com/astral-sh/ruff/issues/20572
|
||||
Decimal("15_000_000") # Safe fix: normalizes separators, becomes Decimal(15_000_000)
|
||||
Decimal("1_234_567") # Safe fix: normalizes separators, becomes Decimal(1_234_567)
|
||||
Decimal("-5_000") # Safe fix: normalizes separators, becomes Decimal(-5_000)
|
||||
Decimal("+9_999") # Safe fix: normalizes separators, becomes Decimal(+9_999)
|
||||
|
||||
# Test cases for non-thousands separators
|
||||
Decimal("12_34_56_78") # Safe fix: preserves non-thousands separators
|
||||
Decimal("1234_5678") # Safe fix: preserves non-thousands separators
|
||||
|
||||
# Separators _and_ leading zeros
|
||||
Decimal("0001_2345")
|
||||
Decimal("000_1_2345")
|
||||
Decimal("000_000")
|
||||
|
||||
@@ -43,3 +43,29 @@ logging.warning("Value: %r", repr(42))
|
||||
logging.error("Error: %r", repr([1, 2, 3]))
|
||||
logging.info("Debug info: %s", repr("test\nstring"))
|
||||
logging.warning("Value: %s", repr(42))
|
||||
|
||||
# %s + ascii()
|
||||
logging.info("ASCII: %s", ascii("Hello\nWorld"))
|
||||
logging.warning("ASCII: %s", ascii("test"))
|
||||
|
||||
# %s + oct()
|
||||
logging.info("Octal: %s", oct(42))
|
||||
logging.warning("Octal: %s", oct(255))
|
||||
|
||||
# %s + hex()
|
||||
logging.info("Hex: %s", hex(42))
|
||||
logging.warning("Hex: %s", hex(255))
|
||||
|
||||
|
||||
# Test with imported functions
|
||||
from logging import info, log
|
||||
|
||||
info("ASCII: %s", ascii("Hello\nWorld"))
|
||||
log(logging.INFO, "ASCII: %s", ascii("test"))
|
||||
|
||||
info("Octal: %s", oct(42))
|
||||
log(logging.INFO, "Octal: %s", oct(255))
|
||||
|
||||
info("Hex: %s", hex(42))
|
||||
log(logging.INFO, "Hex: %s", hex(255))
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
async def f(): return [[x async for x in foo(n)] for n in range(3)]
|
||||
|
||||
async def test(): return [[x async for x in elements(n)] async for n in range(3)]
|
||||
|
||||
async def f(): [x for x in foo()] and [x async for x in foo()]
|
||||
|
||||
async def f():
|
||||
def g(): ...
|
||||
[x async for x in foo()]
|
||||
|
||||
[x async for x in y]
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
match x:
|
||||
case Point(x=1, x=2):
|
||||
pass
|
||||
3
crates/ruff_linter/resources/test/fixtures/semantic_errors/duplicate_match_key.py
vendored
Normal file
3
crates/ruff_linter/resources/test/fixtures/semantic_errors/duplicate_match_key.py
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
match x:
|
||||
case {'key': 1, 'key': 2}:
|
||||
pass
|
||||
1
crates/ruff_linter/resources/test/fixtures/semantic_errors/duplicate_type_parameter.py
vendored
Normal file
1
crates/ruff_linter/resources/test/fixtures/semantic_errors/duplicate_type_parameter.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
class C[T, T]: pass
|
||||
29
crates/ruff_linter/resources/test/fixtures/semantic_errors/global_parameter.py
vendored
Normal file
29
crates/ruff_linter/resources/test/fixtures/semantic_errors/global_parameter.py
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
def f(a):
|
||||
global a
|
||||
|
||||
def g(a):
|
||||
if True:
|
||||
global a
|
||||
|
||||
def h(a):
|
||||
def inner():
|
||||
global a
|
||||
|
||||
def i(a):
|
||||
try:
|
||||
global a
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def f(a):
|
||||
a = 1
|
||||
global a
|
||||
|
||||
def f(a):
|
||||
a = 1
|
||||
a = 2
|
||||
global a
|
||||
|
||||
def f(a):
|
||||
class Inner:
|
||||
global a # ok
|
||||
8
crates/ruff_linter/resources/test/fixtures/semantic_errors/invalid_expression.py
vendored
Normal file
8
crates/ruff_linter/resources/test/fixtures/semantic_errors/invalid_expression.py
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
type X[T: (yield 1)] = int
|
||||
|
||||
type Y = (yield 1)
|
||||
|
||||
def f[T](x: int) -> (y := 3): return x
|
||||
|
||||
class C[T]((yield from [object])):
|
||||
pass
|
||||
8
crates/ruff_linter/resources/test/fixtures/semantic_errors/invalid_star_expression.py
vendored
Normal file
8
crates/ruff_linter/resources/test/fixtures/semantic_errors/invalid_star_expression.py
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
def func():
|
||||
return *x
|
||||
|
||||
for *x in range(10):
|
||||
pass
|
||||
|
||||
def func():
|
||||
yield *x
|
||||
11
crates/ruff_linter/resources/test/fixtures/semantic_errors/irrefutable_case_pattern.py
vendored
Normal file
11
crates/ruff_linter/resources/test/fixtures/semantic_errors/irrefutable_case_pattern.py
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
match value:
|
||||
case _:
|
||||
pass
|
||||
case 1:
|
||||
pass
|
||||
|
||||
match value:
|
||||
case irrefutable:
|
||||
pass
|
||||
case 1:
|
||||
pass
|
||||
5
crates/ruff_linter/resources/test/fixtures/semantic_errors/multiple_case_assignment.py
vendored
Normal file
5
crates/ruff_linter/resources/test/fixtures/semantic_errors/multiple_case_assignment.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
match x:
|
||||
case [a, a]:
|
||||
pass
|
||||
case _:
|
||||
pass
|
||||
1
crates/ruff_linter/resources/test/fixtures/semantic_errors/rebound_comprehension.py
vendored
Normal file
1
crates/ruff_linter/resources/test/fixtures/semantic_errors/rebound_comprehension.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
[x:= 2 for x in range(2)]
|
||||
1
crates/ruff_linter/resources/test/fixtures/semantic_errors/single_starred_assignment.py
vendored
Normal file
1
crates/ruff_linter/resources/test/fixtures/semantic_errors/single_starred_assignment.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*a = [1, 2, 3, 4]
|
||||
7
crates/ruff_linter/resources/test/fixtures/semantic_errors/write_to_debug.py
vendored
Normal file
7
crates/ruff_linter/resources/test/fixtures/semantic_errors/write_to_debug.py
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
__debug__ = False
|
||||
|
||||
def process(__debug__):
|
||||
pass
|
||||
|
||||
class Generic[__debug__]:
|
||||
pass
|
||||
@@ -951,6 +951,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
if checker.is_rule_enabled(Rule::MisplacedBareRaise) {
|
||||
pylint::rules::misplaced_bare_raise(checker, raise);
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::StopIterationReturn) {
|
||||
pylint::rules::stop_iteration_return(checker, raise);
|
||||
}
|
||||
}
|
||||
Stmt::AugAssign(aug_assign @ ast::StmtAugAssign { target, .. }) => {
|
||||
if checker.is_rule_enabled(Rule::GlobalStatement) {
|
||||
|
||||
@@ -1400,6 +1400,14 @@ impl<'a> Visitor<'a> for Checker<'a> {
|
||||
AnnotationContext::RuntimeRequired => {
|
||||
self.visit_runtime_required_annotation(annotation);
|
||||
}
|
||||
AnnotationContext::RuntimeEvaluated
|
||||
if flake8_type_checking::helpers::is_dataclass_meta_annotation(
|
||||
annotation,
|
||||
self.semantic(),
|
||||
) =>
|
||||
{
|
||||
self.visit_runtime_required_annotation(annotation);
|
||||
}
|
||||
AnnotationContext::RuntimeEvaluated => {
|
||||
self.visit_runtime_evaluated_annotation(annotation);
|
||||
}
|
||||
@@ -2116,7 +2124,7 @@ impl<'a> Visitor<'a> for Checker<'a> {
|
||||
| Expr::DictComp(_)
|
||||
| Expr::SetComp(_) => {
|
||||
self.analyze.scopes.push(self.semantic.scope_id);
|
||||
self.semantic.pop_scope();
|
||||
self.semantic.pop_scope(); // Lambda/Generator/Comprehension scope
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -3041,7 +3049,35 @@ impl<'a> Checker<'a> {
|
||||
if let Some(parameters) = parameters {
|
||||
self.visit_parameters(parameters);
|
||||
}
|
||||
|
||||
// Here we add the implicit scope surrounding a lambda which allows code in the
|
||||
// lambda to access `__class__` at runtime when the lambda is defined within a class.
|
||||
// See the `ScopeKind::DunderClassCell` docs for more information.
|
||||
let added_dunder_class_scope = if self
|
||||
.semantic
|
||||
.current_scopes()
|
||||
.any(|scope| scope.kind.is_class())
|
||||
{
|
||||
self.semantic.push_scope(ScopeKind::DunderClassCell);
|
||||
let binding_id = self.semantic.push_binding(
|
||||
TextRange::default(),
|
||||
BindingKind::DunderClassCell,
|
||||
BindingFlags::empty(),
|
||||
);
|
||||
self.semantic
|
||||
.current_scope_mut()
|
||||
.add("__class__", binding_id);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
self.visit_expr(body);
|
||||
|
||||
// Pop the DunderClassCell scope if it was added
|
||||
if added_dunder_class_scope {
|
||||
self.semantic.pop_scope();
|
||||
}
|
||||
}
|
||||
}
|
||||
self.semantic.restore(snapshot);
|
||||
|
||||
@@ -286,6 +286,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Pylint, "R1702") => rules::pylint::rules::TooManyNestedBlocks,
|
||||
(Pylint, "R1704") => rules::pylint::rules::RedefinedArgumentFromLocal,
|
||||
(Pylint, "R1706") => rules::pylint::rules::AndOrTernary,
|
||||
(Pylint, "R1708") => rules::pylint::rules::StopIterationReturn,
|
||||
(Pylint, "R1711") => rules::pylint::rules::UselessReturn,
|
||||
(Pylint, "R1714") => rules::pylint::rules::RepeatedEqualityComparison,
|
||||
(Pylint, "R1722") => rules::pylint::rules::SysExitAlias,
|
||||
|
||||
@@ -11,6 +11,8 @@ pub(crate) static GOOGLE_SECTIONS: &[SectionKind] = &[
|
||||
SectionKind::References,
|
||||
SectionKind::Returns,
|
||||
SectionKind::SeeAlso,
|
||||
SectionKind::Warnings,
|
||||
SectionKind::Warns,
|
||||
SectionKind::Yields,
|
||||
// Google-only
|
||||
SectionKind::Args,
|
||||
@@ -32,7 +34,5 @@ pub(crate) static GOOGLE_SECTIONS: &[SectionKind] = &[
|
||||
SectionKind::Tip,
|
||||
SectionKind::Todo,
|
||||
SectionKind::Warning,
|
||||
SectionKind::Warnings,
|
||||
SectionKind::Warns,
|
||||
SectionKind::Yield,
|
||||
];
|
||||
|
||||
@@ -11,11 +11,14 @@ pub(crate) static NUMPY_SECTIONS: &[SectionKind] = &[
|
||||
SectionKind::References,
|
||||
SectionKind::Returns,
|
||||
SectionKind::SeeAlso,
|
||||
SectionKind::Warnings,
|
||||
SectionKind::Warns,
|
||||
SectionKind::Yields,
|
||||
// NumPy-only
|
||||
SectionKind::ExtendedSummary,
|
||||
SectionKind::OtherParams,
|
||||
SectionKind::OtherParameters,
|
||||
SectionKind::Parameters,
|
||||
SectionKind::Receives,
|
||||
SectionKind::ShortSummary,
|
||||
];
|
||||
|
||||
@@ -36,6 +36,7 @@ pub(crate) enum SectionKind {
|
||||
OtherParameters,
|
||||
Parameters,
|
||||
Raises,
|
||||
Receives,
|
||||
References,
|
||||
Return,
|
||||
Returns,
|
||||
@@ -76,6 +77,7 @@ impl SectionKind {
|
||||
"other parameters" => Some(Self::OtherParameters),
|
||||
"parameters" => Some(Self::Parameters),
|
||||
"raises" => Some(Self::Raises),
|
||||
"receives" => Some(Self::Receives),
|
||||
"references" => Some(Self::References),
|
||||
"return" => Some(Self::Return),
|
||||
"returns" => Some(Self::Returns),
|
||||
@@ -117,6 +119,7 @@ impl SectionKind {
|
||||
Self::OtherParameters => "Other Parameters",
|
||||
Self::Parameters => "Parameters",
|
||||
Self::Raises => "Raises",
|
||||
Self::Receives => "Receives",
|
||||
Self::References => "References",
|
||||
Self::Return => "Return",
|
||||
Self::Returns => "Returns",
|
||||
|
||||
@@ -14,7 +14,7 @@ use ruff_text_size::TextSize;
|
||||
/// The length of a line of text that is considered too long.
|
||||
///
|
||||
/// The allowed range of values is 1..=320
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct LineLength(
|
||||
#[cfg_attr(feature = "schemars", schemars(range(min = 1, max = 320)))] NonZeroU16,
|
||||
@@ -46,6 +46,21 @@ impl fmt::Display for LineLength {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for LineLength {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let value = u16::deserialize(deserializer)?;
|
||||
Self::try_from(value).map_err(|_| {
|
||||
serde::de::Error::custom(format!(
|
||||
"line-length must be between 1 and {} (got {value})",
|
||||
Self::MAX,
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl CacheKey for LineLength {
|
||||
fn cache_key(&self, state: &mut CacheKeyHasher) {
|
||||
state.write_u16(self.0.get());
|
||||
|
||||
@@ -919,17 +919,6 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Wrapper around `test_contents_syntax_errors` for testing a snippet of code instead of a
|
||||
/// file.
|
||||
fn test_snippet_syntax_errors(contents: &str, settings: &LinterSettings) -> Vec<Diagnostic> {
|
||||
let contents = dedent(contents);
|
||||
test_contents_syntax_errors(
|
||||
&SourceKind::Python(contents.to_string()),
|
||||
Path::new("<filename>"),
|
||||
settings,
|
||||
)
|
||||
}
|
||||
|
||||
/// A custom test runner that prints syntax errors in addition to other diagnostics. Adapted
|
||||
/// from `flakes` in pyflakes/mod.rs.
|
||||
fn test_contents_syntax_errors(
|
||||
@@ -972,245 +961,38 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test_case(
|
||||
"async_in_sync_error_on_310",
|
||||
"async def f(): return [[x async for x in foo(n)] for n in range(3)]",
|
||||
PythonVersion::PY310,
|
||||
"AsyncComprehensionOutsideAsyncFunction"
|
||||
Path::new("async_comprehension_outside_async_function.py"),
|
||||
PythonVersion::PY311
|
||||
)]
|
||||
#[test_case(
|
||||
"async_in_sync_okay_on_311",
|
||||
"async def f(): return [[x async for x in foo(n)] for n in range(3)]",
|
||||
PythonVersion::PY311,
|
||||
"AsyncComprehensionOutsideAsyncFunction"
|
||||
Path::new("async_comprehension_outside_async_function.py"),
|
||||
PythonVersion::PY310
|
||||
)]
|
||||
#[test_case(
|
||||
"async_in_sync_okay_on_310",
|
||||
"async def test(): return [[x async for x in elements(n)] async for n in range(3)]",
|
||||
PythonVersion::PY310,
|
||||
"AsyncComprehensionOutsideAsyncFunction"
|
||||
)]
|
||||
#[test_case(
|
||||
"deferred_function_body",
|
||||
"
|
||||
async def f(): [x for x in foo()] and [x async for x in foo()]
|
||||
async def f():
|
||||
def g(): ...
|
||||
[x async for x in foo()]
|
||||
",
|
||||
PythonVersion::PY310,
|
||||
"AsyncComprehensionOutsideAsyncFunction"
|
||||
)]
|
||||
#[test_case(
|
||||
"async_in_sync_false_positive",
|
||||
"[x async for x in y]",
|
||||
PythonVersion::PY310,
|
||||
"AsyncComprehensionOutsideAsyncFunction"
|
||||
)]
|
||||
#[test_case(
|
||||
"rebound_comprehension",
|
||||
"[x:= 2 for x in range(2)]",
|
||||
PythonVersion::PY310,
|
||||
"ReboundComprehensionVariable"
|
||||
)]
|
||||
#[test_case(
|
||||
"duplicate_type_param",
|
||||
"class C[T, T]: pass",
|
||||
PythonVersion::PY312,
|
||||
"DuplicateTypeParameter"
|
||||
)]
|
||||
#[test_case(
|
||||
"multiple_case_assignment",
|
||||
"
|
||||
match x:
|
||||
case [a, a]:
|
||||
pass
|
||||
case _:
|
||||
pass
|
||||
",
|
||||
PythonVersion::PY310,
|
||||
"MultipleCaseAssignment"
|
||||
)]
|
||||
#[test_case(
|
||||
"duplicate_match_key",
|
||||
"
|
||||
match x:
|
||||
case {'key': 1, 'key': 2}:
|
||||
pass
|
||||
",
|
||||
PythonVersion::PY310,
|
||||
"DuplicateMatchKey"
|
||||
)]
|
||||
#[test_case(
|
||||
"global_parameter",
|
||||
"
|
||||
def f(a):
|
||||
global a
|
||||
#[test_case(Path::new("rebound_comprehension.py"), PythonVersion::PY310)]
|
||||
#[test_case(Path::new("duplicate_type_parameter.py"), PythonVersion::PY312)]
|
||||
#[test_case(Path::new("multiple_case_assignment.py"), PythonVersion::PY310)]
|
||||
#[test_case(Path::new("duplicate_match_key.py"), PythonVersion::PY310)]
|
||||
#[test_case(Path::new("duplicate_match_class_attribute.py"), PythonVersion::PY310)]
|
||||
#[test_case(Path::new("invalid_star_expression.py"), PythonVersion::PY310)]
|
||||
#[test_case(Path::new("irrefutable_case_pattern.py"), PythonVersion::PY310)]
|
||||
#[test_case(Path::new("single_starred_assignment.py"), PythonVersion::PY310)]
|
||||
#[test_case(Path::new("write_to_debug.py"), PythonVersion::PY312)]
|
||||
#[test_case(Path::new("write_to_debug.py"), PythonVersion::PY310)]
|
||||
#[test_case(Path::new("invalid_expression.py"), PythonVersion::PY312)]
|
||||
#[test_case(Path::new("global_parameter.py"), PythonVersion::PY310)]
|
||||
fn test_semantic_errors(path: &Path, python_version: PythonVersion) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
"semantic_syntax_error_{}_{}",
|
||||
path.to_string_lossy(),
|
||||
python_version
|
||||
);
|
||||
let path = Path::new("resources/test/fixtures/semantic_errors").join(path);
|
||||
let contents = std::fs::read_to_string(&path)?;
|
||||
let source_kind = SourceKind::Python(contents);
|
||||
|
||||
def g(a):
|
||||
if True:
|
||||
global a
|
||||
|
||||
def h(a):
|
||||
def inner():
|
||||
global a
|
||||
|
||||
def i(a):
|
||||
try:
|
||||
global a
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def f(a):
|
||||
a = 1
|
||||
global a
|
||||
|
||||
def f(a):
|
||||
a = 1
|
||||
a = 2
|
||||
global a
|
||||
|
||||
def f(a):
|
||||
class Inner:
|
||||
global a # ok
|
||||
",
|
||||
PythonVersion::PY310,
|
||||
"GlobalParameter"
|
||||
)]
|
||||
#[test_case(
|
||||
"duplicate_match_class_attribute",
|
||||
"
|
||||
match x:
|
||||
case Point(x=1, x=2):
|
||||
pass
|
||||
",
|
||||
PythonVersion::PY310,
|
||||
"DuplicateMatchClassAttribute"
|
||||
)]
|
||||
#[test_case(
|
||||
"invalid_star_expression",
|
||||
"
|
||||
def func():
|
||||
return *x
|
||||
",
|
||||
PythonVersion::PY310,
|
||||
"InvalidStarExpression"
|
||||
)]
|
||||
#[test_case(
|
||||
"invalid_star_expression_for",
|
||||
"
|
||||
for *x in range(10):
|
||||
pass
|
||||
",
|
||||
PythonVersion::PY310,
|
||||
"InvalidStarExpression"
|
||||
)]
|
||||
#[test_case(
|
||||
"invalid_star_expression_yield",
|
||||
"
|
||||
def func():
|
||||
yield *x
|
||||
",
|
||||
PythonVersion::PY310,
|
||||
"InvalidStarExpression"
|
||||
)]
|
||||
#[test_case(
|
||||
"irrefutable_case_pattern_wildcard",
|
||||
"
|
||||
match value:
|
||||
case _:
|
||||
pass
|
||||
case 1:
|
||||
pass
|
||||
",
|
||||
PythonVersion::PY310,
|
||||
"IrrefutableCasePattern"
|
||||
)]
|
||||
#[test_case(
|
||||
"irrefutable_case_pattern_capture",
|
||||
"
|
||||
match value:
|
||||
case irrefutable:
|
||||
pass
|
||||
case 1:
|
||||
pass
|
||||
",
|
||||
PythonVersion::PY310,
|
||||
"IrrefutableCasePattern"
|
||||
)]
|
||||
#[test_case(
|
||||
"single_starred_assignment",
|
||||
"*a = [1, 2, 3, 4]",
|
||||
PythonVersion::PY310,
|
||||
"SingleStarredAssignment"
|
||||
)]
|
||||
#[test_case(
|
||||
"write_to_debug",
|
||||
"
|
||||
__debug__ = False
|
||||
",
|
||||
PythonVersion::PY310,
|
||||
"WriteToDebug"
|
||||
)]
|
||||
#[test_case(
|
||||
"write_to_debug_in_function_param",
|
||||
"
|
||||
def process(__debug__):
|
||||
pass
|
||||
",
|
||||
PythonVersion::PY310,
|
||||
"WriteToDebug"
|
||||
)]
|
||||
#[test_case(
|
||||
"write_to_debug_class_type_param",
|
||||
"
|
||||
class Generic[__debug__]:
|
||||
pass
|
||||
",
|
||||
PythonVersion::PY312,
|
||||
"WriteToDebug"
|
||||
)]
|
||||
#[test_case(
|
||||
"invalid_expression_yield_in_type_param",
|
||||
"
|
||||
type X[T: (yield 1)] = int
|
||||
",
|
||||
PythonVersion::PY312,
|
||||
"InvalidExpression"
|
||||
)]
|
||||
#[test_case(
|
||||
"invalid_expression_yield_in_type_alias",
|
||||
"
|
||||
type Y = (yield 1)
|
||||
",
|
||||
PythonVersion::PY312,
|
||||
"InvalidExpression"
|
||||
)]
|
||||
#[test_case(
|
||||
"invalid_expression_walrus_in_return_annotation",
|
||||
"
|
||||
def f[T](x: int) -> (y := 3): return x
|
||||
",
|
||||
PythonVersion::PY312,
|
||||
"InvalidExpression"
|
||||
)]
|
||||
#[test_case(
|
||||
"invalid_expression_yield_from_in_base_class",
|
||||
"
|
||||
class C[T]((yield from [object])):
|
||||
pass
|
||||
",
|
||||
PythonVersion::PY312,
|
||||
"InvalidExpression"
|
||||
)]
|
||||
fn test_semantic_errors(
|
||||
name: &str,
|
||||
contents: &str,
|
||||
python_version: PythonVersion,
|
||||
error_type: &str,
|
||||
) {
|
||||
let snapshot = format!("semantic_syntax_error_{error_type}_{name}_{python_version}");
|
||||
let diagnostics = test_snippet_syntax_errors(
|
||||
contents,
|
||||
let diagnostics = test_contents_syntax_errors(
|
||||
&source_kind,
|
||||
&path,
|
||||
&LinterSettings {
|
||||
rules: settings::rule_table::RuleTable::empty(),
|
||||
unresolved_target_version: python_version.into(),
|
||||
@@ -1218,7 +1000,11 @@ mod tests {
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
assert_diagnostics!(snapshot, diagnostics);
|
||||
insta::with_settings!({filters => vec![(r"\\", "/")]}, {
|
||||
assert_diagnostics!(format!("{snapshot}"), diagnostics);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(PythonVersion::PY310)]
|
||||
|
||||
@@ -4,4 +4,4 @@ expression: content
|
||||
---
|
||||
syntax_errors.py:
|
||||
1:15 invalid-syntax: Expected one or more symbol names after import
|
||||
3:12 invalid-syntax: Expected ')', found newline
|
||||
3:12 invalid-syntax: Expected `)`, found newline
|
||||
|
||||
@@ -492,6 +492,12 @@ fn check_method(checker: &Checker, call_expr: &ExprCall) {
|
||||
"collected_datasets" => Replacement::AttrName("collected_assets"),
|
||||
_ => return,
|
||||
},
|
||||
["airflow", "models", "dag", "DAG"] | ["airflow", "models", "DAG"] | ["airflow", "DAG"] => {
|
||||
match attr.as_str() {
|
||||
"create_dagrun" => Replacement::None,
|
||||
_ => return,
|
||||
}
|
||||
}
|
||||
["airflow", "providers_manager", "ProvidersManager"] => match attr.as_str() {
|
||||
"initialize_providers_dataset_uri_resources" => {
|
||||
Replacement::AttrName("initialize_providers_asset_uri_resources")
|
||||
|
||||
@@ -262,9 +262,14 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
|
||||
name: (*rest).to_string(),
|
||||
}
|
||||
}
|
||||
["airflow", "models", "Param"] => Replacement::Rename {
|
||||
[
|
||||
"airflow",
|
||||
"models",
|
||||
..,
|
||||
rest @ ("Param" | "ParamsDict" | "DagParam"),
|
||||
] => Replacement::SourceModuleMoved {
|
||||
module: "airflow.sdk.definitions.param",
|
||||
name: "Param",
|
||||
name: (*rest).to_string(),
|
||||
},
|
||||
|
||||
// airflow.models.baseoperator
|
||||
@@ -283,10 +288,12 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
|
||||
},
|
||||
|
||||
// airflow.model..DAG
|
||||
["airflow", "models", .., "DAG"] => Replacement::SourceModuleMoved {
|
||||
module: "airflow.sdk",
|
||||
name: "DAG".to_string(),
|
||||
},
|
||||
["airflow", "models", "dag", "DAG"] | ["airflow", "models", "DAG"] | ["airflow", "DAG"] => {
|
||||
Replacement::SourceModuleMoved {
|
||||
module: "airflow.sdk",
|
||||
name: "DAG".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
// airflow.sensors.base
|
||||
[
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,25 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/airflow/mod.rs
|
||||
---
|
||||
AIR311 [*] `airflow.DAG` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
--> AIR311_args.py:13:1
|
||||
|
|
||||
13 | DAG(dag_id="class_sla_callback", sla_miss_callback=sla_callback)
|
||||
| ^^^
|
||||
|
|
||||
help: Use `DAG` from `airflow.sdk` instead.
|
||||
2 |
|
||||
3 | from datetime import timedelta
|
||||
4 |
|
||||
- from airflow import DAG, dag
|
||||
5 + from airflow import dag
|
||||
6 | from airflow.operators.datetime import BranchDateTimeOperator
|
||||
7 + from airflow.sdk import DAG
|
||||
8 |
|
||||
9 |
|
||||
10 | def sla_callback(*arg, **kwargs):
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
AIR311 `sla_miss_callback` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
--> AIR311_args.py:13:34
|
||||
|
|
||||
|
||||
@@ -737,79 +737,185 @@ AIR311 [*] `airflow.models.Param` is removed in Airflow 3.0; It still works in A
|
||||
96 | # airflow.models
|
||||
97 | Param()
|
||||
| ^^^^^
|
||||
98 | DagParam()
|
||||
99 | ParamsDict()
|
||||
|
|
||||
help: Use `Param` from `airflow.sdk.definitions.param` instead.
|
||||
91 | task_decorator_factory()
|
||||
92 |
|
||||
93 |
|
||||
- from airflow.models import Param
|
||||
94 + from airflow.sdk.definitions.param import Param
|
||||
95 |
|
||||
- from airflow.models import DagParam, Param, ParamsDict
|
||||
94 + from airflow.models import DagParam, ParamsDict
|
||||
95 + from airflow.sdk.definitions.param import Param
|
||||
96 |
|
||||
97 | # airflow.models
|
||||
98 | Param()
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
AIR311 [*] `airflow.models.DagParam` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
--> AIR311_names.py:98:1
|
||||
|
|
||||
96 | # airflow.models
|
||||
97 | Param()
|
||||
98 | DagParam()
|
||||
| ^^^^^^^^
|
||||
99 | ParamsDict()
|
||||
|
|
||||
help: Use `DagParam` from `airflow.sdk.definitions.param` instead.
|
||||
91 | task_decorator_factory()
|
||||
92 |
|
||||
93 |
|
||||
- from airflow.models import DagParam, Param, ParamsDict
|
||||
94 + from airflow.models import Param, ParamsDict
|
||||
95 + from airflow.sdk.definitions.param import DagParam
|
||||
96 |
|
||||
97 | # airflow.models
|
||||
98 | Param()
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
AIR311 [*] `airflow.models.ParamsDict` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
--> AIR311_names.py:99:1
|
||||
|
|
||||
97 | Param()
|
||||
98 | DagParam()
|
||||
99 | ParamsDict()
|
||||
| ^^^^^^^^^^
|
||||
|
|
||||
help: Use `ParamsDict` from `airflow.sdk.definitions.param` instead.
|
||||
91 | task_decorator_factory()
|
||||
92 |
|
||||
93 |
|
||||
- from airflow.models import DagParam, Param, ParamsDict
|
||||
94 + from airflow.models import DagParam, Param
|
||||
95 + from airflow.sdk.definitions.param import ParamsDict
|
||||
96 |
|
||||
97 | # airflow.models
|
||||
98 | Param()
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
AIR311 [*] `airflow.models.param.Param` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
--> AIR311_names.py:105:1
|
||||
|
|
||||
104 | # airflow.models.param
|
||||
105 | Param()
|
||||
| ^^^^^
|
||||
106 | DagParam()
|
||||
107 | ParamsDict()
|
||||
|
|
||||
help: Use `Param` from `airflow.sdk.definitions.param` instead.
|
||||
99 | ParamsDict()
|
||||
100 |
|
||||
101 |
|
||||
- from airflow.models.param import DagParam, Param, ParamsDict
|
||||
102 + from airflow.models.param import DagParam, ParamsDict
|
||||
103 + from airflow.sdk.definitions.param import Param
|
||||
104 |
|
||||
105 | # airflow.models.param
|
||||
106 | Param()
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
AIR311 [*] `airflow.models.param.DagParam` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
--> AIR311_names.py:106:1
|
||||
|
|
||||
104 | # airflow.models.param
|
||||
105 | Param()
|
||||
106 | DagParam()
|
||||
| ^^^^^^^^
|
||||
107 | ParamsDict()
|
||||
|
|
||||
help: Use `DagParam` from `airflow.sdk.definitions.param` instead.
|
||||
99 | ParamsDict()
|
||||
100 |
|
||||
101 |
|
||||
- from airflow.models.param import DagParam, Param, ParamsDict
|
||||
102 + from airflow.models.param import Param, ParamsDict
|
||||
103 + from airflow.sdk.definitions.param import DagParam
|
||||
104 |
|
||||
105 | # airflow.models.param
|
||||
106 | Param()
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
AIR311 [*] `airflow.models.param.ParamsDict` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
--> AIR311_names.py:107:1
|
||||
|
|
||||
105 | Param()
|
||||
106 | DagParam()
|
||||
107 | ParamsDict()
|
||||
| ^^^^^^^^^^
|
||||
|
|
||||
help: Use `ParamsDict` from `airflow.sdk.definitions.param` instead.
|
||||
99 | ParamsDict()
|
||||
100 |
|
||||
101 |
|
||||
- from airflow.models.param import DagParam, Param, ParamsDict
|
||||
102 + from airflow.models.param import DagParam, Param
|
||||
103 + from airflow.sdk.definitions.param import ParamsDict
|
||||
104 |
|
||||
105 | # airflow.models.param
|
||||
106 | Param()
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
AIR311 [*] `airflow.sensors.base.BaseSensorOperator` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
--> AIR311_names.py:107:1
|
||||
--> AIR311_names.py:117:1
|
||||
|
|
||||
106 | # airflow.sensors.base
|
||||
107 | BaseSensorOperator()
|
||||
116 | # airflow.sensors.base
|
||||
117 | BaseSensorOperator()
|
||||
| ^^^^^^^^^^^^^^^^^^
|
||||
108 | PokeReturnValue()
|
||||
109 | poke_mode_only()
|
||||
118 | PokeReturnValue()
|
||||
119 | poke_mode_only()
|
||||
|
|
||||
help: Use `BaseSensorOperator` from `airflow.sdk` instead.
|
||||
98 |
|
||||
99 |
|
||||
100 | from airflow.sensors.base import (
|
||||
108 |
|
||||
109 |
|
||||
110 | from airflow.sensors.base import (
|
||||
- BaseSensorOperator,
|
||||
101 | PokeReturnValue,
|
||||
102 | poke_mode_only,
|
||||
103 | )
|
||||
104 + from airflow.sdk import BaseSensorOperator
|
||||
105 |
|
||||
106 | # airflow.sensors.base
|
||||
107 | BaseSensorOperator()
|
||||
111 | PokeReturnValue,
|
||||
112 | poke_mode_only,
|
||||
113 | )
|
||||
114 + from airflow.sdk import BaseSensorOperator
|
||||
115 |
|
||||
116 | # airflow.sensors.base
|
||||
117 | BaseSensorOperator()
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
AIR311 [*] `airflow.sensors.base.PokeReturnValue` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
--> AIR311_names.py:108:1
|
||||
--> AIR311_names.py:118:1
|
||||
|
|
||||
106 | # airflow.sensors.base
|
||||
107 | BaseSensorOperator()
|
||||
108 | PokeReturnValue()
|
||||
116 | # airflow.sensors.base
|
||||
117 | BaseSensorOperator()
|
||||
118 | PokeReturnValue()
|
||||
| ^^^^^^^^^^^^^^^
|
||||
109 | poke_mode_only()
|
||||
119 | poke_mode_only()
|
||||
|
|
||||
help: Use `PokeReturnValue` from `airflow.sdk` instead.
|
||||
99 |
|
||||
100 | from airflow.sensors.base import (
|
||||
101 | BaseSensorOperator,
|
||||
109 |
|
||||
110 | from airflow.sensors.base import (
|
||||
111 | BaseSensorOperator,
|
||||
- PokeReturnValue,
|
||||
102 | poke_mode_only,
|
||||
103 | )
|
||||
104 + from airflow.sdk import PokeReturnValue
|
||||
105 |
|
||||
106 | # airflow.sensors.base
|
||||
107 | BaseSensorOperator()
|
||||
112 | poke_mode_only,
|
||||
113 | )
|
||||
114 + from airflow.sdk import PokeReturnValue
|
||||
115 |
|
||||
116 | # airflow.sensors.base
|
||||
117 | BaseSensorOperator()
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
AIR311 [*] `airflow.sensors.base.poke_mode_only` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
|
||||
--> AIR311_names.py:109:1
|
||||
--> AIR311_names.py:119:1
|
||||
|
|
||||
107 | BaseSensorOperator()
|
||||
108 | PokeReturnValue()
|
||||
109 | poke_mode_only()
|
||||
117 | BaseSensorOperator()
|
||||
118 | PokeReturnValue()
|
||||
119 | poke_mode_only()
|
||||
| ^^^^^^^^^^^^^^
|
||||
|
|
||||
help: Use `poke_mode_only` from `airflow.sdk` instead.
|
||||
100 | from airflow.sensors.base import (
|
||||
101 | BaseSensorOperator,
|
||||
102 | PokeReturnValue,
|
||||
110 | from airflow.sensors.base import (
|
||||
111 | BaseSensorOperator,
|
||||
112 | PokeReturnValue,
|
||||
- poke_mode_only,
|
||||
103 | )
|
||||
104 + from airflow.sdk import poke_mode_only
|
||||
105 |
|
||||
106 | # airflow.sensors.base
|
||||
107 | BaseSensorOperator()
|
||||
113 | )
|
||||
114 + from airflow.sdk import poke_mode_only
|
||||
115 |
|
||||
116 | # airflow.sensors.base
|
||||
117 | BaseSensorOperator()
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
@@ -375,7 +375,7 @@ impl Violation for SuspiciousEvalUsage {
|
||||
///
|
||||
///
|
||||
/// def render_username(username):
|
||||
/// return django.utils.html.format_html("<i>{}</i>", username) # username is escaped.
|
||||
/// return format_html("<i>{}</i>", username) # username is escaped.
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
|
||||
@@ -61,9 +61,14 @@ pub(crate) fn nullable_model_string_field(checker: &Checker, body: &[Stmt]) {
|
||||
}
|
||||
|
||||
for statement in body {
|
||||
let Stmt::Assign(ast::StmtAssign { value, .. }) = statement else {
|
||||
continue;
|
||||
let value = match statement {
|
||||
Stmt::Assign(ast::StmtAssign { value, .. }) => value,
|
||||
Stmt::AnnAssign(ast::StmtAnnAssign {
|
||||
value: Some(value), ..
|
||||
}) => value,
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
if let Some(field_name) = is_nullable_field(value, checker.semantic()) {
|
||||
checker.report_diagnostic(
|
||||
DjangoNullableModelStringField {
|
||||
|
||||
@@ -186,3 +186,32 @@ DJ001 Avoid using `null=True` on string-based fields such as `URLField`
|
||||
30 | urlfield = models.URLField(max_length=255, null=True)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
|
||||
DJ001 Avoid using `null=True` on string-based fields such as `CharField`
|
||||
--> DJ001.py:52:35
|
||||
|
|
||||
51 | class IncorrectModelWithSimpleAnnotations(models.Model):
|
||||
52 | charfield: models.CharField = models.CharField(max_length=255, null=True)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
53 | textfield: models.TextField = models.TextField(max_length=255, null=True)
|
||||
54 | slugfield: models.SlugField = models.SlugField(max_length=255, null=True)
|
||||
|
|
||||
|
||||
DJ001 Avoid using `null=True` on string-based fields such as `TextField`
|
||||
--> DJ001.py:53:35
|
||||
|
|
||||
51 | class IncorrectModelWithSimpleAnnotations(models.Model):
|
||||
52 | charfield: models.CharField = models.CharField(max_length=255, null=True)
|
||||
53 | textfield: models.TextField = models.TextField(max_length=255, null=True)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
54 | slugfield: models.SlugField = models.SlugField(max_length=255, null=True)
|
||||
|
|
||||
|
||||
DJ001 Avoid using `null=True` on string-based fields such as `SlugField`
|
||||
--> DJ001.py:54:35
|
||||
|
|
||||
52 | charfield: models.CharField = models.CharField(max_length=255, null=True)
|
||||
53 | textfield: models.TextField = models.TextField(max_length=255, null=True)
|
||||
54 | slugfield: models.SlugField = models.SlugField(max_length=255, null=True)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
|
||||
@@ -23,6 +23,14 @@ mod tests {
|
||||
Rule::MultiLineImplicitStringConcatenation,
|
||||
Path::new("ISC_syntax_error.py")
|
||||
)]
|
||||
#[test_case(
|
||||
Rule::SingleLineImplicitStringConcatenation,
|
||||
Path::new("ISC_syntax_error_2.py")
|
||||
)]
|
||||
#[test_case(
|
||||
Rule::MultiLineImplicitStringConcatenation,
|
||||
Path::new("ISC_syntax_error_2.py")
|
||||
)]
|
||||
#[test_case(Rule::ExplicitStringConcatenation, Path::new("ISC.py"))]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use itertools::Itertools;
|
||||
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::str::{leading_quote, trailing_quote};
|
||||
use ruff_python_ast::StringFlags;
|
||||
use ruff_python_index::Indexer;
|
||||
use ruff_python_parser::{TokenKind, Tokens};
|
||||
use ruff_python_parser::{Token, TokenKind, Tokens};
|
||||
use ruff_source_file::LineRanges;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange};
|
||||
|
||||
use crate::Locator;
|
||||
use crate::checkers::ast::LintContext;
|
||||
@@ -169,7 +168,8 @@ pub(crate) fn implicit(
|
||||
SingleLineImplicitStringConcatenation,
|
||||
TextRange::new(a_range.start(), b_range.end()),
|
||||
) {
|
||||
if let Some(fix) = concatenate_strings(a_range, b_range, locator) {
|
||||
if let Some(fix) = concatenate_strings(a_token, b_token, a_range, b_range, locator)
|
||||
{
|
||||
diagnostic.set_fix(fix);
|
||||
}
|
||||
}
|
||||
@@ -177,38 +177,55 @@ pub(crate) fn implicit(
|
||||
}
|
||||
}
|
||||
|
||||
fn concatenate_strings(a_range: TextRange, b_range: TextRange, locator: &Locator) -> Option<Fix> {
|
||||
let a_text = locator.slice(a_range);
|
||||
let b_text = locator.slice(b_range);
|
||||
|
||||
let a_leading_quote = leading_quote(a_text)?;
|
||||
let b_leading_quote = leading_quote(b_text)?;
|
||||
|
||||
// Require, for now, that the leading quotes are the same.
|
||||
if a_leading_quote != b_leading_quote {
|
||||
/// Concatenates two strings
|
||||
///
|
||||
/// The `a_string_range` and `b_string_range` are the range of the entire string,
|
||||
/// not just of the string token itself (important for interpolated strings where
|
||||
/// the start token doesn't span the entire token).
|
||||
fn concatenate_strings(
|
||||
a_token: &Token,
|
||||
b_token: &Token,
|
||||
a_string_range: TextRange,
|
||||
b_string_range: TextRange,
|
||||
locator: &Locator,
|
||||
) -> Option<Fix> {
|
||||
if a_token.string_flags()?.is_unclosed() || b_token.string_flags()?.is_unclosed() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let a_trailing_quote = trailing_quote(a_text)?;
|
||||
let b_trailing_quote = trailing_quote(b_text)?;
|
||||
let a_string_flags = a_token.string_flags()?;
|
||||
let b_string_flags = b_token.string_flags()?;
|
||||
|
||||
// Require, for now, that the trailing quotes are the same.
|
||||
if a_trailing_quote != b_trailing_quote {
|
||||
let a_prefix = a_string_flags.prefix();
|
||||
let b_prefix = b_string_flags.prefix();
|
||||
|
||||
// Require, for now, that the strings have the same prefix,
|
||||
// quote style, and number of quotes
|
||||
if a_prefix != b_prefix
|
||||
|| a_string_flags.quote_style() != b_string_flags.quote_style()
|
||||
|| a_string_flags.is_triple_quoted() != b_string_flags.is_triple_quoted()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let a_text = locator.slice(a_string_range);
|
||||
let b_text = locator.slice(b_string_range);
|
||||
|
||||
let quotes = a_string_flags.quote_str();
|
||||
|
||||
let opener_len = a_string_flags.opener_len();
|
||||
let closer_len = a_string_flags.closer_len();
|
||||
|
||||
let mut a_body =
|
||||
Cow::Borrowed(&a_text[a_leading_quote.len()..a_text.len() - a_trailing_quote.len()]);
|
||||
let b_body = &b_text[b_leading_quote.len()..b_text.len() - b_trailing_quote.len()];
|
||||
Cow::Borrowed(&a_text[TextRange::new(opener_len, a_text.text_len() - closer_len)]);
|
||||
let b_body = &b_text[TextRange::new(opener_len, b_text.text_len() - closer_len)];
|
||||
|
||||
if a_leading_quote.find(['r', 'R']).is_none()
|
||||
&& matches!(b_body.bytes().next(), Some(b'0'..=b'7'))
|
||||
{
|
||||
if !a_string_flags.is_raw_string() && matches!(b_body.bytes().next(), Some(b'0'..=b'7')) {
|
||||
normalize_ending_octal(&mut a_body);
|
||||
}
|
||||
|
||||
let concatenation = format!("{a_leading_quote}{a_body}{b_body}{a_trailing_quote}");
|
||||
let range = TextRange::new(a_range.start(), b_range.end());
|
||||
let concatenation = format!("{a_prefix}{quotes}{a_body}{b_body}{quotes}");
|
||||
let range = TextRange::new(a_string_range.start(), b_string_range.end());
|
||||
|
||||
Some(Fix::safe_edit(Edit::range_replacement(
|
||||
concatenation,
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_implicit_str_concat/mod.rs
|
||||
---
|
||||
ISC001 Implicitly concatenated string literals on one line
|
||||
--> ISC_syntax_error_2.py:2:1
|
||||
|
|
||||
1 | # Regression test for https://github.com/astral-sh/ruff/issues/21023
|
||||
2 | '' '
|
||||
| ^^^^
|
||||
3 | "" ""
|
||||
4 | '' '' '
|
||||
|
|
||||
help: Combine string literals
|
||||
|
||||
invalid-syntax: missing closing quote in string literal
|
||||
--> ISC_syntax_error_2.py:2:4
|
||||
|
|
||||
1 | # Regression test for https://github.com/astral-sh/ruff/issues/21023
|
||||
2 | '' '
|
||||
| ^
|
||||
3 | "" ""
|
||||
4 | '' '' '
|
||||
|
|
||||
|
||||
ISC001 Implicitly concatenated string literals on one line
|
||||
--> ISC_syntax_error_2.py:3:1
|
||||
|
|
||||
1 | # Regression test for https://github.com/astral-sh/ruff/issues/21023
|
||||
2 | '' '
|
||||
3 | "" ""
|
||||
| ^^^^^
|
||||
4 | '' '' '
|
||||
5 | "" "" "
|
||||
|
|
||||
help: Combine string literals
|
||||
|
||||
ISC001 Implicitly concatenated string literals on one line
|
||||
--> ISC_syntax_error_2.py:4:1
|
||||
|
|
||||
2 | '' '
|
||||
3 | "" ""
|
||||
4 | '' '' '
|
||||
| ^^^^^
|
||||
5 | "" "" "
|
||||
6 | f"" f"
|
||||
|
|
||||
help: Combine string literals
|
||||
|
||||
ISC001 Implicitly concatenated string literals on one line
|
||||
--> ISC_syntax_error_2.py:4:4
|
||||
|
|
||||
2 | '' '
|
||||
3 | "" ""
|
||||
4 | '' '' '
|
||||
| ^^^^
|
||||
5 | "" "" "
|
||||
6 | f"" f"
|
||||
|
|
||||
help: Combine string literals
|
||||
|
||||
invalid-syntax: missing closing quote in string literal
|
||||
--> ISC_syntax_error_2.py:4:7
|
||||
|
|
||||
2 | '' '
|
||||
3 | "" ""
|
||||
4 | '' '' '
|
||||
| ^
|
||||
5 | "" "" "
|
||||
6 | f"" f"
|
||||
|
|
||||
|
||||
ISC001 Implicitly concatenated string literals on one line
|
||||
--> ISC_syntax_error_2.py:5:1
|
||||
|
|
||||
3 | "" ""
|
||||
4 | '' '' '
|
||||
5 | "" "" "
|
||||
| ^^^^^
|
||||
6 | f"" f"
|
||||
7 | f"" f"" f"
|
||||
|
|
||||
help: Combine string literals
|
||||
|
||||
ISC001 Implicitly concatenated string literals on one line
|
||||
--> ISC_syntax_error_2.py:5:4
|
||||
|
|
||||
3 | "" ""
|
||||
4 | '' '' '
|
||||
5 | "" "" "
|
||||
| ^^^^
|
||||
6 | f"" f"
|
||||
7 | f"" f"" f"
|
||||
|
|
||||
help: Combine string literals
|
||||
|
||||
invalid-syntax: missing closing quote in string literal
|
||||
--> ISC_syntax_error_2.py:5:7
|
||||
|
|
||||
3 | "" ""
|
||||
4 | '' '' '
|
||||
5 | "" "" "
|
||||
| ^
|
||||
6 | f"" f"
|
||||
7 | f"" f"" f"
|
||||
|
|
||||
|
||||
invalid-syntax: f-string: unterminated string
|
||||
--> ISC_syntax_error_2.py:6:7
|
||||
|
|
||||
4 | '' '' '
|
||||
5 | "" "" "
|
||||
6 | f"" f"
|
||||
| ^
|
||||
7 | f"" f"" f"
|
||||
|
|
||||
|
||||
ISC001 Implicitly concatenated string literals on one line
|
||||
--> ISC_syntax_error_2.py:7:1
|
||||
|
|
||||
5 | "" "" "
|
||||
6 | f"" f"
|
||||
7 | f"" f"" f"
|
||||
| ^^^^^^^
|
||||
|
|
||||
help: Combine string literals
|
||||
|
||||
invalid-syntax: f-string: unterminated string
|
||||
--> ISC_syntax_error_2.py:7:11
|
||||
|
|
||||
5 | "" "" "
|
||||
6 | f"" f"
|
||||
7 | f"" f"" f"
|
||||
| ^
|
||||
|
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_implicit_str_concat/mod.rs
|
||||
---
|
||||
invalid-syntax: missing closing quote in string literal
|
||||
--> ISC_syntax_error_2.py:2:4
|
||||
|
|
||||
1 | # Regression test for https://github.com/astral-sh/ruff/issues/21023
|
||||
2 | '' '
|
||||
| ^
|
||||
3 | "" ""
|
||||
4 | '' '' '
|
||||
|
|
||||
|
||||
invalid-syntax: missing closing quote in string literal
|
||||
--> ISC_syntax_error_2.py:4:7
|
||||
|
|
||||
2 | '' '
|
||||
3 | "" ""
|
||||
4 | '' '' '
|
||||
| ^
|
||||
5 | "" "" "
|
||||
6 | f"" f"
|
||||
|
|
||||
|
||||
invalid-syntax: missing closing quote in string literal
|
||||
--> ISC_syntax_error_2.py:5:7
|
||||
|
|
||||
3 | "" ""
|
||||
4 | '' '' '
|
||||
5 | "" "" "
|
||||
| ^
|
||||
6 | f"" f"
|
||||
7 | f"" f"" f"
|
||||
|
|
||||
|
||||
invalid-syntax: f-string: unterminated string
|
||||
--> ISC_syntax_error_2.py:6:7
|
||||
|
|
||||
4 | '' '' '
|
||||
5 | "" "" "
|
||||
6 | f"" f"
|
||||
| ^
|
||||
7 | f"" f"" f"
|
||||
|
|
||||
|
||||
invalid-syntax: f-string: unterminated string
|
||||
--> ISC_syntax_error_2.py:7:11
|
||||
|
|
||||
5 | "" "" "
|
||||
6 | f"" f"
|
||||
7 | f"" f"" f"
|
||||
| ^
|
||||
|
|
||||
@@ -78,7 +78,7 @@ pub(crate) fn unconventional_import_alias(
|
||||
let mut diagnostic = checker.report_diagnostic(
|
||||
UnconventionalImportAlias {
|
||||
name: qualified_name,
|
||||
asname: expected_alias.to_string(),
|
||||
asname: expected_alias.clone(),
|
||||
},
|
||||
binding.range(),
|
||||
);
|
||||
|
||||
@@ -50,6 +50,29 @@ use ruff_text_size::Ranged;
|
||||
/// 1. `__aiter__` methods that return `AsyncIterator`, despite the class
|
||||
/// inheriting directly from `AsyncIterator`.
|
||||
///
|
||||
/// The rule attempts to avoid flagging methods on metaclasses, since
|
||||
/// [PEP 673] specifies that `Self` is disallowed in metaclasses. Ruff can
|
||||
/// detect a class as being a metaclass if it inherits from a stdlib
|
||||
/// metaclass such as `builtins.type` or `abc.ABCMeta`, and additionally
|
||||
/// infers that a class may be a metaclass if it has a `__new__` method
|
||||
/// with a similar signature to `type.__new__`. The heuristic used to
|
||||
/// identify a metaclass-like `__new__` method signature is that it:
|
||||
///
|
||||
/// 1. Has exactly 5 parameters (including `cls`)
|
||||
/// 1. Has a second parameter annotated with `str`
|
||||
/// 1. Has a third parameter annotated with a `tuple` type
|
||||
/// 1. Has a fourth parameter annotated with a `dict` type
|
||||
/// 1. Has a fifth parameter is keyword-variadic (`**kwargs`)
|
||||
///
|
||||
/// For example, the following class would be detected as a metaclass, disabling
|
||||
/// the rule:
|
||||
///
|
||||
/// ```python
|
||||
/// class MyMetaclass(django.db.models.base.ModelBase):
|
||||
/// def __new__(cls, name: str, bases: tuple[Any, ...], attrs: dict[str, Any], **kwargs: Any) -> MyMetaclass:
|
||||
/// ...
|
||||
/// ```
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```pyi
|
||||
@@ -87,6 +110,8 @@ use ruff_text_size::Ranged;
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `typing.Self`](https://docs.python.org/3/library/typing.html#typing.Self)
|
||||
///
|
||||
/// [PEP 673]: https://peps.python.org/pep-0673/#valid-locations-for-self
|
||||
#[derive(ViolationMetadata)]
|
||||
#[violation_metadata(stable_since = "v0.0.271")]
|
||||
pub(crate) struct NonSelfReturnType {
|
||||
@@ -143,7 +168,10 @@ pub(crate) fn non_self_return_type(
|
||||
};
|
||||
|
||||
// PEP 673 forbids the use of `typing(_extensions).Self` in metaclasses.
|
||||
if analyze::class::is_metaclass(class_def, semantic).is_yes() {
|
||||
if !matches!(
|
||||
analyze::class::is_metaclass(class_def, semantic),
|
||||
analyze::class::IsMetaclass::No
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -451,6 +451,7 @@ help: Use `Self` as return type
|
||||
359 + def __new__(cls) -> typing.Self: ...
|
||||
360 | def __enter__(self: Generic5) -> Generic5: ...
|
||||
361 |
|
||||
362 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
PYI034 [*] `__enter__` methods in classes like `Generic5` usually return `self` at runtime
|
||||
@@ -468,4 +469,6 @@ help: Use `Self` as return type
|
||||
- def __enter__(self: Generic5) -> Generic5: ...
|
||||
360 + def __enter__(self) -> typing.Self: ...
|
||||
361 |
|
||||
362 |
|
||||
363 | # Test cases based on issue #20781 - metaclasses that triggers IsMetaclass::Maybe
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
@@ -431,6 +431,8 @@ help: Use `Self` as return type
|
||||
- def __new__(cls: type[Generic5]) -> Generic5: ...
|
||||
253 + def __new__(cls) -> typing.Self: ...
|
||||
254 | def __enter__(self: Generic5) -> Generic5: ...
|
||||
255 |
|
||||
256 |
|
||||
note: This is a display-only fix and is likely to be incorrect
|
||||
|
||||
PYI034 [*] `__enter__` methods in classes like `Generic5` usually return `self` at runtime
|
||||
@@ -447,4 +449,7 @@ help: Use `Self` as return type
|
||||
253 | def __new__(cls: type[Generic5]) -> Generic5: ...
|
||||
- def __enter__(self: Generic5) -> Generic5: ...
|
||||
254 + def __enter__(self) -> typing.Self: ...
|
||||
255 |
|
||||
256 |
|
||||
257 | # Test case based on issue #20781 - metaclass that triggers IsMetaclass::Maybe
|
||||
note: This is a display-only fix and is likely to be incorrect
|
||||
|
||||
@@ -6,21 +6,17 @@ use ruff_macros::CacheKey;
|
||||
|
||||
#[derive(Clone, Copy, Debug, CacheKey, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[derive(Default)]
|
||||
pub enum ParametrizeNameType {
|
||||
#[serde(rename = "csv")]
|
||||
Csv,
|
||||
#[serde(rename = "tuple")]
|
||||
#[default]
|
||||
Tuple,
|
||||
#[serde(rename = "list")]
|
||||
List,
|
||||
}
|
||||
|
||||
impl Default for ParametrizeNameType {
|
||||
fn default() -> Self {
|
||||
Self::Tuple
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ParametrizeNameType {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
@@ -33,19 +29,15 @@ impl Display for ParametrizeNameType {
|
||||
|
||||
#[derive(Clone, Copy, Debug, CacheKey, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[derive(Default)]
|
||||
pub enum ParametrizeValuesType {
|
||||
#[serde(rename = "tuple")]
|
||||
Tuple,
|
||||
#[serde(rename = "list")]
|
||||
#[default]
|
||||
List,
|
||||
}
|
||||
|
||||
impl Default for ParametrizeValuesType {
|
||||
fn default() -> Self {
|
||||
Self::List
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ParametrizeValuesType {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
@@ -57,19 +49,15 @@ impl Display for ParametrizeValuesType {
|
||||
|
||||
#[derive(Clone, Copy, Debug, CacheKey, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[derive(Default)]
|
||||
pub enum ParametrizeValuesRowType {
|
||||
#[serde(rename = "tuple")]
|
||||
#[default]
|
||||
Tuple,
|
||||
#[serde(rename = "list")]
|
||||
List,
|
||||
}
|
||||
|
||||
impl Default for ParametrizeValuesRowType {
|
||||
fn default() -> Self {
|
||||
Self::Tuple
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ParametrizeValuesRowType {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
|
||||
@@ -9,19 +9,15 @@ use ruff_macros::CacheKey;
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, CacheKey)]
|
||||
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[derive(Default)]
|
||||
pub enum Quote {
|
||||
/// Use double quotes.
|
||||
#[default]
|
||||
Double,
|
||||
/// Use single quotes.
|
||||
Single,
|
||||
}
|
||||
|
||||
impl Default for Quote {
|
||||
fn default() -> Self {
|
||||
Self::Double
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ruff_python_ast::str::Quote> for Quote {
|
||||
fn from(value: ruff_python_ast::str::Quote) -> Self {
|
||||
match value {
|
||||
|
||||
@@ -116,7 +116,7 @@ pub(crate) fn convert_for_loop_to_any_all(checker: &Checker, stmt: &Stmt) {
|
||||
|
||||
let mut diagnostic = checker.report_diagnostic(
|
||||
ReimplementedBuiltin {
|
||||
replacement: contents.to_string(),
|
||||
replacement: contents.clone(),
|
||||
},
|
||||
TextRange::new(stmt.start(), terminal.stmt.end()),
|
||||
);
|
||||
@@ -212,7 +212,7 @@ pub(crate) fn convert_for_loop_to_any_all(checker: &Checker, stmt: &Stmt) {
|
||||
|
||||
let mut diagnostic = checker.report_diagnostic(
|
||||
ReimplementedBuiltin {
|
||||
replacement: contents.to_string(),
|
||||
replacement: contents.clone(),
|
||||
},
|
||||
TextRange::new(stmt.start(), terminal.stmt.end()),
|
||||
);
|
||||
|
||||
@@ -47,7 +47,7 @@ pub(crate) fn banned_api<T: Ranged>(checker: &Checker, policy: &NameMatchPolicy,
|
||||
checker.report_diagnostic(
|
||||
BannedApi {
|
||||
name: banned_module,
|
||||
message: reason.msg.to_string(),
|
||||
message: reason.msg.clone(),
|
||||
},
|
||||
node.range(),
|
||||
);
|
||||
@@ -74,8 +74,8 @@ pub(crate) fn banned_attribute_access(checker: &Checker, expr: &Expr) {
|
||||
{
|
||||
checker.report_diagnostic(
|
||||
BannedApi {
|
||||
name: banned_path.to_string(),
|
||||
message: ban.msg.to_string(),
|
||||
name: banned_path.clone(),
|
||||
message: ban.msg.clone(),
|
||||
},
|
||||
expr.range(),
|
||||
);
|
||||
|
||||
@@ -98,6 +98,26 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("TC003.py"))]
|
||||
fn add_future_import_dataclass_kw_only_py313(rule: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
"add_future_import_kw_only__{}_{}",
|
||||
rule.noqa_code(),
|
||||
path.to_string_lossy()
|
||||
);
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_type_checking").join(path).as_path(),
|
||||
&settings::LinterSettings {
|
||||
future_annotations: true,
|
||||
// The issue in #21121 also didn't trigger on Python 3.14
|
||||
unresolved_target_version: PythonVersion::PY313.into(),
|
||||
..settings::LinterSettings::for_rule(rule)
|
||||
},
|
||||
)?;
|
||||
assert_diagnostics!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// we test these rules as a pair, since they're opposites of one another
|
||||
// so we want to make sure their fixes are not going around in circles.
|
||||
#[test_case(Rule::UnquotedTypeAlias, Path::new("TC007.py"))]
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
|
||||
---
|
||||
TC003 [*] Move standard library import `os` into a type-checking block
|
||||
--> TC003.py:8:12
|
||||
|
|
||||
7 | def f():
|
||||
8 | import os
|
||||
| ^^
|
||||
9 |
|
||||
10 | x: os
|
||||
|
|
||||
help: Move into type-checking block
|
||||
2 |
|
||||
3 | For typing-only import detection tests, see `TC002.py`.
|
||||
4 | """
|
||||
5 + from typing import TYPE_CHECKING
|
||||
6 +
|
||||
7 + if TYPE_CHECKING:
|
||||
8 + import os
|
||||
9 |
|
||||
10 |
|
||||
11 | def f():
|
||||
- import os
|
||||
12 |
|
||||
13 | x: os
|
||||
14 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
@@ -20,21 +20,17 @@ use super::categorize::ImportSection;
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, CacheKey)]
|
||||
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[derive(Default)]
|
||||
pub enum RelativeImportsOrder {
|
||||
/// Place "closer" imports (fewer `.` characters, most local) before
|
||||
/// "further" imports (more `.` characters, least local).
|
||||
ClosestToFurthest,
|
||||
/// Place "further" imports (more `.` characters, least local) imports
|
||||
/// before "closer" imports (fewer `.` characters, most local).
|
||||
#[default]
|
||||
FurthestToClosest,
|
||||
}
|
||||
|
||||
impl Default for RelativeImportsOrder {
|
||||
fn default() -> Self {
|
||||
Self::FurthestToClosest
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for RelativeImportsOrder {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
|
||||
@@ -427,7 +427,7 @@ pub(crate) fn literal_comparisons(checker: &Checker, compare: &ast::ExprCompare)
|
||||
|
||||
for diagnostic in &mut diagnostics {
|
||||
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
|
||||
content.to_string(),
|
||||
content.clone(),
|
||||
compare.range(),
|
||||
)));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
|
||||
---
|
||||
E231 [*] Missing whitespace after ','
|
||||
E231 [*] Missing whitespace after `,`
|
||||
--> E23.py:2:7
|
||||
|
|
||||
1 | #: E231
|
||||
@@ -18,7 +18,7 @@ help: Add missing whitespace
|
||||
4 | a[b1,:]
|
||||
5 | #: E231
|
||||
|
||||
E231 [*] Missing whitespace after ','
|
||||
E231 [*] Missing whitespace after `,`
|
||||
--> E23.py:4:5
|
||||
|
|
||||
2 | a = (1,2)
|
||||
@@ -38,7 +38,7 @@ help: Add missing whitespace
|
||||
6 | a = [{'a':''}]
|
||||
7 | #: Okay
|
||||
|
||||
E231 [*] Missing whitespace after ':'
|
||||
E231 [*] Missing whitespace after `:`
|
||||
--> E23.py:6:10
|
||||
|
|
||||
4 | a[b1,:]
|
||||
@@ -58,7 +58,7 @@ help: Add missing whitespace
|
||||
8 | a = (4,)
|
||||
9 | b = (5, )
|
||||
|
||||
E231 [*] Missing whitespace after ','
|
||||
E231 [*] Missing whitespace after `,`
|
||||
--> E23.py:19:10
|
||||
|
|
||||
17 | def foo() -> None:
|
||||
@@ -77,7 +77,7 @@ help: Add missing whitespace
|
||||
21 |
|
||||
22 | #: Okay
|
||||
|
||||
E231 [*] Missing whitespace after ':'
|
||||
E231 [*] Missing whitespace after `:`
|
||||
--> E23.py:29:20
|
||||
|
|
||||
27 | mdtypes_template = {
|
||||
@@ -96,7 +96,7 @@ help: Add missing whitespace
|
||||
31 |
|
||||
32 | # E231
|
||||
|
||||
E231 [*] Missing whitespace after ','
|
||||
E231 [*] Missing whitespace after `,`
|
||||
--> E23.py:33:6
|
||||
|
|
||||
32 | # E231
|
||||
@@ -115,7 +115,7 @@ help: Add missing whitespace
|
||||
35 | # Okay because it's hard to differentiate between the usages of a colon in a f-string
|
||||
36 | f"{a:=1}"
|
||||
|
||||
E231 [*] Missing whitespace after ':'
|
||||
E231 [*] Missing whitespace after `:`
|
||||
--> E23.py:47:37
|
||||
|
|
||||
46 | #: E231
|
||||
@@ -134,7 +134,7 @@ help: Add missing whitespace
|
||||
49 | #: Okay
|
||||
50 | a = (1,)
|
||||
|
||||
E231 [*] Missing whitespace after ':'
|
||||
E231 [*] Missing whitespace after `:`
|
||||
--> E23.py:60:13
|
||||
|
|
||||
58 | results = {
|
||||
@@ -154,7 +154,7 @@ help: Add missing whitespace
|
||||
62 | results_in_tuple = (
|
||||
63 | {
|
||||
|
||||
E231 [*] Missing whitespace after ':'
|
||||
E231 [*] Missing whitespace after `:`
|
||||
--> E23.py:65:17
|
||||
|
|
||||
63 | {
|
||||
@@ -174,7 +174,7 @@ help: Add missing whitespace
|
||||
67 | )
|
||||
68 | results_in_list = [
|
||||
|
||||
E231 [*] Missing whitespace after ':'
|
||||
E231 [*] Missing whitespace after `:`
|
||||
--> E23.py:71:17
|
||||
|
|
||||
69 | {
|
||||
@@ -194,7 +194,7 @@ help: Add missing whitespace
|
||||
73 | ]
|
||||
74 | results_in_list_first = [
|
||||
|
||||
E231 [*] Missing whitespace after ':'
|
||||
E231 [*] Missing whitespace after `:`
|
||||
--> E23.py:76:17
|
||||
|
|
||||
74 | results_in_list_first = [
|
||||
@@ -214,7 +214,7 @@ help: Add missing whitespace
|
||||
78 | ]
|
||||
79 |
|
||||
|
||||
E231 [*] Missing whitespace after ':'
|
||||
E231 [*] Missing whitespace after `:`
|
||||
--> E23.py:82:13
|
||||
|
|
||||
80 | x = [
|
||||
@@ -234,7 +234,7 @@ help: Add missing whitespace
|
||||
84 | "k3":[2], # E231
|
||||
85 | "k4": [2],
|
||||
|
||||
E231 [*] Missing whitespace after ':'
|
||||
E231 [*] Missing whitespace after `:`
|
||||
--> E23.py:84:13
|
||||
|
|
||||
82 | "k1":[2], # E231
|
||||
@@ -254,7 +254,7 @@ help: Add missing whitespace
|
||||
86 | "k5": [2],
|
||||
87 | "k6": [1, 2, 3, 4,5,6,7] # E231
|
||||
|
||||
E231 [*] Missing whitespace after ','
|
||||
E231 [*] Missing whitespace after `,`
|
||||
--> E23.py:87:26
|
||||
|
|
||||
85 | "k4": [2],
|
||||
@@ -274,7 +274,7 @@ help: Add missing whitespace
|
||||
89 | {
|
||||
90 | "k1": [
|
||||
|
||||
E231 [*] Missing whitespace after ','
|
||||
E231 [*] Missing whitespace after `,`
|
||||
--> E23.py:87:28
|
||||
|
|
||||
85 | "k4": [2],
|
||||
@@ -294,7 +294,7 @@ help: Add missing whitespace
|
||||
89 | {
|
||||
90 | "k1": [
|
||||
|
||||
E231 [*] Missing whitespace after ','
|
||||
E231 [*] Missing whitespace after `,`
|
||||
--> E23.py:87:30
|
||||
|
|
||||
85 | "k4": [2],
|
||||
@@ -314,7 +314,7 @@ help: Add missing whitespace
|
||||
89 | {
|
||||
90 | "k1": [
|
||||
|
||||
E231 [*] Missing whitespace after ':'
|
||||
E231 [*] Missing whitespace after `:`
|
||||
--> E23.py:92:21
|
||||
|
|
||||
90 | "k1": [
|
||||
@@ -334,7 +334,7 @@ help: Add missing whitespace
|
||||
94 | {
|
||||
95 | "kb": [2,3], # E231
|
||||
|
||||
E231 [*] Missing whitespace after ','
|
||||
E231 [*] Missing whitespace after `,`
|
||||
--> E23.py:92:24
|
||||
|
|
||||
90 | "k1": [
|
||||
@@ -354,7 +354,7 @@ help: Add missing whitespace
|
||||
94 | {
|
||||
95 | "kb": [2,3], # E231
|
||||
|
||||
E231 [*] Missing whitespace after ','
|
||||
E231 [*] Missing whitespace after `,`
|
||||
--> E23.py:95:25
|
||||
|
|
||||
93 | },
|
||||
@@ -374,7 +374,7 @@ help: Add missing whitespace
|
||||
97 | {
|
||||
98 | "ka":[2, 3], # E231
|
||||
|
||||
E231 [*] Missing whitespace after ':'
|
||||
E231 [*] Missing whitespace after `:`
|
||||
--> E23.py:98:21
|
||||
|
|
||||
96 | },
|
||||
@@ -394,7 +394,7 @@ help: Add missing whitespace
|
||||
100 | "kc": [2, 3], # Ok
|
||||
101 | "kd": [2,3], # E231
|
||||
|
||||
E231 [*] Missing whitespace after ','
|
||||
E231 [*] Missing whitespace after `,`
|
||||
--> E23.py:101:25
|
||||
|
|
||||
99 | "kb": [2, 3], # Ok
|
||||
@@ -414,7 +414,7 @@ help: Add missing whitespace
|
||||
103 | },
|
||||
104 | ]
|
||||
|
||||
E231 [*] Missing whitespace after ':'
|
||||
E231 [*] Missing whitespace after `:`
|
||||
--> E23.py:102:21
|
||||
|
|
||||
100 | "kc": [2, 3], # Ok
|
||||
@@ -434,7 +434,7 @@ help: Add missing whitespace
|
||||
104 | ]
|
||||
105 | }
|
||||
|
||||
E231 [*] Missing whitespace after ','
|
||||
E231 [*] Missing whitespace after `,`
|
||||
--> E23.py:102:24
|
||||
|
|
||||
100 | "kc": [2, 3], # Ok
|
||||
@@ -454,7 +454,7 @@ help: Add missing whitespace
|
||||
104 | ]
|
||||
105 | }
|
||||
|
||||
E231 [*] Missing whitespace after ':'
|
||||
E231 [*] Missing whitespace after `:`
|
||||
--> E23.py:109:18
|
||||
|
|
||||
108 | # Should be E231 errors on all of these type parameters and function parameters, but not on their (strange) defaults
|
||||
@@ -473,7 +473,7 @@ help: Add missing whitespace
|
||||
111 | y:B = [[["foo", "bar"]]],
|
||||
112 | z:object = "fooo",
|
||||
|
||||
E231 [*] Missing whitespace after ':'
|
||||
E231 [*] Missing whitespace after `:`
|
||||
--> E23.py:109:40
|
||||
|
|
||||
108 | # Should be E231 errors on all of these type parameters and function parameters, but not on their (strange) defaults
|
||||
@@ -492,7 +492,7 @@ help: Add missing whitespace
|
||||
111 | y:B = [[["foo", "bar"]]],
|
||||
112 | z:object = "fooo",
|
||||
|
||||
E231 [*] Missing whitespace after ':'
|
||||
E231 [*] Missing whitespace after `:`
|
||||
--> E23.py:109:70
|
||||
|
|
||||
108 | # Should be E231 errors on all of these type parameters and function parameters, but not on their (strange) defaults
|
||||
@@ -511,7 +511,7 @@ help: Add missing whitespace
|
||||
111 | y:B = [[["foo", "bar"]]],
|
||||
112 | z:object = "fooo",
|
||||
|
||||
E231 [*] Missing whitespace after ':'
|
||||
E231 [*] Missing whitespace after `:`
|
||||
--> E23.py:110:6
|
||||
|
|
||||
108 | # Should be E231 errors on all of these type parameters and function parameters, but not on their (strange) defaults
|
||||
@@ -531,7 +531,7 @@ help: Add missing whitespace
|
||||
112 | z:object = "fooo",
|
||||
113 | ):
|
||||
|
||||
E231 [*] Missing whitespace after ':'
|
||||
E231 [*] Missing whitespace after `:`
|
||||
--> E23.py:111:6
|
||||
|
|
||||
109 | def pep_696_bad[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](
|
||||
@@ -551,7 +551,7 @@ help: Add missing whitespace
|
||||
113 | ):
|
||||
114 | pass
|
||||
|
||||
E231 [*] Missing whitespace after ':'
|
||||
E231 [*] Missing whitespace after `:`
|
||||
--> E23.py:112:6
|
||||
|
|
||||
110 | x:A = "foo"[::-1],
|
||||
@@ -571,7 +571,7 @@ help: Add missing whitespace
|
||||
114 | pass
|
||||
115 |
|
||||
|
||||
E231 [*] Missing whitespace after ':'
|
||||
E231 [*] Missing whitespace after `:`
|
||||
--> E23.py:116:18
|
||||
|
|
||||
114 | pass
|
||||
@@ -591,7 +591,7 @@ help: Add missing whitespace
|
||||
118 | self,
|
||||
119 | x:A = "foo"[::-1],
|
||||
|
||||
E231 [*] Missing whitespace after ':'
|
||||
E231 [*] Missing whitespace after `:`
|
||||
--> E23.py:116:40
|
||||
|
|
||||
114 | pass
|
||||
@@ -611,7 +611,7 @@ help: Add missing whitespace
|
||||
118 | self,
|
||||
119 | x:A = "foo"[::-1],
|
||||
|
||||
E231 [*] Missing whitespace after ':'
|
||||
E231 [*] Missing whitespace after `:`
|
||||
--> E23.py:116:70
|
||||
|
|
||||
114 | pass
|
||||
@@ -631,7 +631,7 @@ help: Add missing whitespace
|
||||
118 | self,
|
||||
119 | x:A = "foo"[::-1],
|
||||
|
||||
E231 [*] Missing whitespace after ':'
|
||||
E231 [*] Missing whitespace after `:`
|
||||
--> E23.py:117:29
|
||||
|
|
||||
116 | class PEP696Bad[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes]:
|
||||
@@ -650,7 +650,7 @@ help: Add missing whitespace
|
||||
119 | x:A = "foo"[::-1],
|
||||
120 | y:B = [[["foo", "bar"]]],
|
||||
|
||||
E231 [*] Missing whitespace after ':'
|
||||
E231 [*] Missing whitespace after `:`
|
||||
--> E23.py:117:51
|
||||
|
|
||||
116 | class PEP696Bad[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes]:
|
||||
@@ -669,7 +669,7 @@ help: Add missing whitespace
|
||||
119 | x:A = "foo"[::-1],
|
||||
120 | y:B = [[["foo", "bar"]]],
|
||||
|
||||
E231 [*] Missing whitespace after ':'
|
||||
E231 [*] Missing whitespace after `:`
|
||||
--> E23.py:117:81
|
||||
|
|
||||
116 | class PEP696Bad[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes]:
|
||||
@@ -688,7 +688,7 @@ help: Add missing whitespace
|
||||
119 | x:A = "foo"[::-1],
|
||||
120 | y:B = [[["foo", "bar"]]],
|
||||
|
||||
E231 [*] Missing whitespace after ':'
|
||||
E231 [*] Missing whitespace after `:`
|
||||
--> E23.py:119:10
|
||||
|
|
||||
117 | def pep_696_bad_method[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](
|
||||
@@ -708,7 +708,7 @@ help: Add missing whitespace
|
||||
121 | z:object = "fooo",
|
||||
122 | ):
|
||||
|
||||
E231 [*] Missing whitespace after ':'
|
||||
E231 [*] Missing whitespace after `:`
|
||||
--> E23.py:120:10
|
||||
|
|
||||
118 | self,
|
||||
@@ -728,7 +728,7 @@ help: Add missing whitespace
|
||||
122 | ):
|
||||
123 | pass
|
||||
|
||||
E231 [*] Missing whitespace after ':'
|
||||
E231 [*] Missing whitespace after `:`
|
||||
--> E23.py:121:10
|
||||
|
|
||||
119 | x:A = "foo"[::-1],
|
||||
@@ -748,7 +748,7 @@ help: Add missing whitespace
|
||||
123 | pass
|
||||
124 |
|
||||
|
||||
E231 [*] Missing whitespace after ':'
|
||||
E231 [*] Missing whitespace after `:`
|
||||
--> E23.py:125:32
|
||||
|
|
||||
123 | pass
|
||||
@@ -768,7 +768,7 @@ help: Add missing whitespace
|
||||
127 | pass
|
||||
128 |
|
||||
|
||||
E231 [*] Missing whitespace after ':'
|
||||
E231 [*] Missing whitespace after `:`
|
||||
--> E23.py:125:54
|
||||
|
|
||||
123 | pass
|
||||
@@ -788,7 +788,7 @@ help: Add missing whitespace
|
||||
127 | pass
|
||||
128 |
|
||||
|
||||
E231 [*] Missing whitespace after ':'
|
||||
E231 [*] Missing whitespace after `:`
|
||||
--> E23.py:125:84
|
||||
|
|
||||
123 | pass
|
||||
@@ -808,7 +808,7 @@ help: Add missing whitespace
|
||||
127 | pass
|
||||
128 |
|
||||
|
||||
E231 [*] Missing whitespace after ':'
|
||||
E231 [*] Missing whitespace after `:`
|
||||
--> E23.py:126:47
|
||||
|
|
||||
125 | class PEP696BadWithEmptyBases[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes]():
|
||||
@@ -826,7 +826,7 @@ help: Add missing whitespace
|
||||
128 |
|
||||
129 | # Should be no E231 errors on any of these:
|
||||
|
||||
E231 [*] Missing whitespace after ':'
|
||||
E231 [*] Missing whitespace after `:`
|
||||
--> E23.py:126:69
|
||||
|
|
||||
125 | class PEP696BadWithEmptyBases[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes]():
|
||||
@@ -844,7 +844,7 @@ help: Add missing whitespace
|
||||
128 |
|
||||
129 | # Should be no E231 errors on any of these:
|
||||
|
||||
E231 [*] Missing whitespace after ':'
|
||||
E231 [*] Missing whitespace after `:`
|
||||
--> E23.py:126:99
|
||||
|
|
||||
125 | class PEP696BadWithEmptyBases[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes]():
|
||||
@@ -862,7 +862,7 @@ help: Add missing whitespace
|
||||
128 |
|
||||
129 | # Should be no E231 errors on any of these:
|
||||
|
||||
E231 [*] Missing whitespace after ','
|
||||
E231 [*] Missing whitespace after `,`
|
||||
--> E23.py:147:6
|
||||
|
|
||||
146 | # E231
|
||||
@@ -881,7 +881,7 @@ help: Add missing whitespace
|
||||
149 | # Okay because it's hard to differentiate between the usages of a colon in a t-string
|
||||
150 | t"{a:=1}"
|
||||
|
||||
E231 [*] Missing whitespace after ':'
|
||||
E231 [*] Missing whitespace after `:`
|
||||
--> E23.py:161:37
|
||||
|
|
||||
160 | #: E231
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
|
||||
---
|
||||
invalid-syntax: Expected ']', found '('
|
||||
invalid-syntax: Expected `]`, found `(`
|
||||
--> E30_syntax_error.py:4:15
|
||||
|
|
||||
2 | # parenthesis.
|
||||
@@ -11,7 +11,7 @@ invalid-syntax: Expected ']', found '('
|
||||
5 | pass
|
||||
|
|
||||
|
||||
invalid-syntax: Expected ')', found newline
|
||||
invalid-syntax: Expected `)`, found newline
|
||||
--> E30_syntax_error.py:13:18
|
||||
|
|
||||
12 | class Foo:
|
||||
@@ -32,7 +32,7 @@ E301 Expected 1 blank line, found 0
|
||||
|
|
||||
help: Add missing blank line
|
||||
|
||||
invalid-syntax: Expected ')', found newline
|
||||
invalid-syntax: Expected `)`, found newline
|
||||
--> E30_syntax_error.py:18:11
|
||||
|
|
||||
16 | pass
|
||||
@@ -41,7 +41,7 @@ invalid-syntax: Expected ')', found newline
|
||||
| ^
|
||||
|
|
||||
|
||||
invalid-syntax: Expected ')', found newline
|
||||
invalid-syntax: Expected `)`, found newline
|
||||
--> E30_syntax_error.py:21:9
|
||||
|
|
||||
21 | def top(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
|
||||
---
|
||||
invalid-syntax: Expected ']', found '('
|
||||
invalid-syntax: Expected `]`, found `(`
|
||||
--> E30_syntax_error.py:4:15
|
||||
|
|
||||
2 | # parenthesis.
|
||||
@@ -22,7 +22,7 @@ E302 Expected 2 blank lines, found 1
|
||||
|
|
||||
help: Add missing blank line(s)
|
||||
|
||||
invalid-syntax: Expected ')', found newline
|
||||
invalid-syntax: Expected `)`, found newline
|
||||
--> E30_syntax_error.py:13:18
|
||||
|
|
||||
12 | class Foo:
|
||||
@@ -32,7 +32,7 @@ invalid-syntax: Expected ')', found newline
|
||||
15 | def method():
|
||||
|
|
||||
|
||||
invalid-syntax: Expected ')', found newline
|
||||
invalid-syntax: Expected `)`, found newline
|
||||
--> E30_syntax_error.py:18:11
|
||||
|
|
||||
16 | pass
|
||||
@@ -41,7 +41,7 @@ invalid-syntax: Expected ')', found newline
|
||||
| ^
|
||||
|
|
||||
|
||||
invalid-syntax: Expected ')', found newline
|
||||
invalid-syntax: Expected `)`, found newline
|
||||
--> E30_syntax_error.py:21:9
|
||||
|
|
||||
21 | def top(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
|
||||
---
|
||||
invalid-syntax: Expected ']', found '('
|
||||
invalid-syntax: Expected `]`, found `(`
|
||||
--> E30_syntax_error.py:4:15
|
||||
|
|
||||
2 | # parenthesis.
|
||||
@@ -21,7 +21,7 @@ E303 Too many blank lines (3)
|
||||
|
|
||||
help: Remove extraneous blank line(s)
|
||||
|
||||
invalid-syntax: Expected ')', found newline
|
||||
invalid-syntax: Expected `)`, found newline
|
||||
--> E30_syntax_error.py:13:18
|
||||
|
|
||||
12 | class Foo:
|
||||
@@ -31,7 +31,7 @@ invalid-syntax: Expected ')', found newline
|
||||
15 | def method():
|
||||
|
|
||||
|
||||
invalid-syntax: Expected ')', found newline
|
||||
invalid-syntax: Expected `)`, found newline
|
||||
--> E30_syntax_error.py:18:11
|
||||
|
|
||||
16 | pass
|
||||
@@ -40,7 +40,7 @@ invalid-syntax: Expected ')', found newline
|
||||
| ^
|
||||
|
|
||||
|
||||
invalid-syntax: Expected ')', found newline
|
||||
invalid-syntax: Expected `)`, found newline
|
||||
--> E30_syntax_error.py:21:9
|
||||
|
|
||||
21 | def top(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
|
||||
---
|
||||
invalid-syntax: Expected ']', found '('
|
||||
invalid-syntax: Expected `]`, found `(`
|
||||
--> E30_syntax_error.py:4:15
|
||||
|
|
||||
2 | # parenthesis.
|
||||
@@ -11,7 +11,7 @@ invalid-syntax: Expected ']', found '('
|
||||
5 | pass
|
||||
|
|
||||
|
||||
invalid-syntax: Expected ')', found newline
|
||||
invalid-syntax: Expected `)`, found newline
|
||||
--> E30_syntax_error.py:13:18
|
||||
|
|
||||
12 | class Foo:
|
||||
@@ -31,7 +31,7 @@ E305 Expected 2 blank lines after class or function definition, found (1)
|
||||
|
|
||||
help: Add missing blank line(s)
|
||||
|
||||
invalid-syntax: Expected ')', found newline
|
||||
invalid-syntax: Expected `)`, found newline
|
||||
--> E30_syntax_error.py:18:11
|
||||
|
|
||||
16 | pass
|
||||
@@ -40,7 +40,7 @@ invalid-syntax: Expected ')', found newline
|
||||
| ^
|
||||
|
|
||||
|
||||
invalid-syntax: Expected ')', found newline
|
||||
invalid-syntax: Expected `)`, found newline
|
||||
--> E30_syntax_error.py:21:9
|
||||
|
|
||||
21 | def top(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
|
||||
---
|
||||
invalid-syntax: Expected ']', found '('
|
||||
invalid-syntax: Expected `]`, found `(`
|
||||
--> E30_syntax_error.py:4:15
|
||||
|
|
||||
2 | # parenthesis.
|
||||
@@ -11,7 +11,7 @@ invalid-syntax: Expected ']', found '('
|
||||
5 | pass
|
||||
|
|
||||
|
||||
invalid-syntax: Expected ')', found newline
|
||||
invalid-syntax: Expected `)`, found newline
|
||||
--> E30_syntax_error.py:13:18
|
||||
|
|
||||
12 | class Foo:
|
||||
@@ -21,7 +21,7 @@ invalid-syntax: Expected ')', found newline
|
||||
15 | def method():
|
||||
|
|
||||
|
||||
invalid-syntax: Expected ')', found newline
|
||||
invalid-syntax: Expected `)`, found newline
|
||||
--> E30_syntax_error.py:18:11
|
||||
|
|
||||
16 | pass
|
||||
@@ -30,7 +30,7 @@ invalid-syntax: Expected ')', found newline
|
||||
| ^
|
||||
|
|
||||
|
||||
invalid-syntax: Expected ')', found newline
|
||||
invalid-syntax: Expected `)`, found newline
|
||||
--> E30_syntax_error.py:21:9
|
||||
|
|
||||
21 | def top(
|
||||
|
||||
@@ -9,6 +9,7 @@ use ruff_python_semantic::{Definition, SemanticModel};
|
||||
use ruff_python_stdlib::identifiers::is_identifier;
|
||||
use ruff_source_file::{LineRanges, NewlineWithTrailingNewline};
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use crate::Violation;
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -823,6 +824,8 @@ struct BodyVisitor<'a> {
|
||||
currently_suspended_exceptions: Option<&'a ast::Expr>,
|
||||
raised_exceptions: Vec<ExceptionEntry<'a>>,
|
||||
semantic: &'a SemanticModel<'a>,
|
||||
/// Maps exception variable names to their exception expressions in the current except clause
|
||||
exception_variables: FxHashMap<&'a str, &'a ast::Expr>,
|
||||
}
|
||||
|
||||
impl<'a> BodyVisitor<'a> {
|
||||
@@ -833,6 +836,7 @@ impl<'a> BodyVisitor<'a> {
|
||||
currently_suspended_exceptions: None,
|
||||
raised_exceptions: Vec::new(),
|
||||
semantic,
|
||||
exception_variables: FxHashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -864,20 +868,47 @@ impl<'a> BodyVisitor<'a> {
|
||||
raised_exceptions,
|
||||
}
|
||||
}
|
||||
|
||||
/// Store `exception` if its qualified name does not correspond to one of the exempt types.
|
||||
fn maybe_store_exception(&mut self, exception: &'a Expr, range: TextRange) {
|
||||
let Some(qualified_name) = self.semantic.resolve_qualified_name(exception) else {
|
||||
return;
|
||||
};
|
||||
if is_exception_or_base_exception(&qualified_name) {
|
||||
return;
|
||||
}
|
||||
self.raised_exceptions.push(ExceptionEntry {
|
||||
qualified_name,
|
||||
range,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Visitor<'a> for BodyVisitor<'a> {
|
||||
fn visit_except_handler(&mut self, handler: &'a ast::ExceptHandler) {
|
||||
let ast::ExceptHandler::ExceptHandler(handler_inner) = handler;
|
||||
self.currently_suspended_exceptions = handler_inner.type_.as_deref();
|
||||
|
||||
// Track exception variable bindings
|
||||
if let Some(name) = handler_inner.name.as_ref() {
|
||||
if let Some(exceptions) = self.currently_suspended_exceptions {
|
||||
// Store the exception expression(s) for later resolution
|
||||
self.exception_variables
|
||||
.insert(name.id.as_str(), exceptions);
|
||||
}
|
||||
}
|
||||
|
||||
visitor::walk_except_handler(self, handler);
|
||||
self.currently_suspended_exceptions = None;
|
||||
// Clear exception variables when leaving the except handler
|
||||
self.exception_variables.clear();
|
||||
}
|
||||
|
||||
fn visit_stmt(&mut self, stmt: &'a Stmt) {
|
||||
match stmt {
|
||||
Stmt::Raise(ast::StmtRaise { exc, .. }) => {
|
||||
if let Some(exc) = exc.as_ref() {
|
||||
// First try to resolve the exception directly
|
||||
if let Some(qualified_name) =
|
||||
self.semantic.resolve_qualified_name(map_callable(exc))
|
||||
{
|
||||
@@ -885,28 +916,27 @@ impl<'a> Visitor<'a> for BodyVisitor<'a> {
|
||||
qualified_name,
|
||||
range: exc.range(),
|
||||
});
|
||||
} else if let ast::Expr::Name(name) = exc.as_ref() {
|
||||
// If it's a variable name, check if it's bound to an exception in the
|
||||
// current except clause
|
||||
if let Some(exception_expr) = self.exception_variables.get(name.id.as_str())
|
||||
{
|
||||
if let ast::Expr::Tuple(tuple) = exception_expr {
|
||||
for exception in tuple {
|
||||
self.maybe_store_exception(exception, stmt.range());
|
||||
}
|
||||
} else {
|
||||
self.maybe_store_exception(exception_expr, stmt.range());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let Some(exceptions) = self.currently_suspended_exceptions {
|
||||
let mut maybe_store_exception = |exception| {
|
||||
let Some(qualified_name) = self.semantic.resolve_qualified_name(exception)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if is_exception_or_base_exception(&qualified_name) {
|
||||
return;
|
||||
}
|
||||
self.raised_exceptions.push(ExceptionEntry {
|
||||
qualified_name,
|
||||
range: stmt.range(),
|
||||
});
|
||||
};
|
||||
|
||||
if let ast::Expr::Tuple(tuple) = exceptions {
|
||||
for exception in tuple {
|
||||
maybe_store_exception(exception);
|
||||
self.maybe_store_exception(exception, stmt.range());
|
||||
}
|
||||
} else {
|
||||
maybe_store_exception(exceptions);
|
||||
self.maybe_store_exception(exceptions, stmt.range());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,66 @@ DOC501 Raised exception `FasterThanLightError` missing from docstring
|
||||
|
|
||||
help: Add `FasterThanLightError` to the docstring
|
||||
|
||||
DOC501 Raised exception `ZeroDivisionError` missing from docstring
|
||||
--> DOC501_google.py:70:5
|
||||
|
|
||||
68 | # DOC501
|
||||
69 | def calculate_speed(distance: float, time: float) -> float:
|
||||
70 | / """Calculate speed as distance divided by time.
|
||||
71 | |
|
||||
72 | | Args:
|
||||
73 | | distance: Distance traveled.
|
||||
74 | | time: Time spent traveling.
|
||||
75 | |
|
||||
76 | | Returns:
|
||||
77 | | Speed as distance divided by time.
|
||||
78 | | """
|
||||
| |_______^
|
||||
79 | try:
|
||||
80 | return distance / time
|
||||
|
|
||||
help: Add `ZeroDivisionError` to the docstring
|
||||
|
||||
DOC501 Raised exception `ValueError` missing from docstring
|
||||
--> DOC501_google.py:88:5
|
||||
|
|
||||
86 | # DOC501
|
||||
87 | def calculate_speed(distance: float, time: float) -> float:
|
||||
88 | / """Calculate speed as distance divided by time.
|
||||
89 | |
|
||||
90 | | Args:
|
||||
91 | | distance: Distance traveled.
|
||||
92 | | time: Time spent traveling.
|
||||
93 | |
|
||||
94 | | Returns:
|
||||
95 | | Speed as distance divided by time.
|
||||
96 | | """
|
||||
| |_______^
|
||||
97 | try:
|
||||
98 | return distance / time
|
||||
|
|
||||
help: Add `ValueError` to the docstring
|
||||
|
||||
DOC501 Raised exception `ZeroDivisionError` missing from docstring
|
||||
--> DOC501_google.py:88:5
|
||||
|
|
||||
86 | # DOC501
|
||||
87 | def calculate_speed(distance: float, time: float) -> float:
|
||||
88 | / """Calculate speed as distance divided by time.
|
||||
89 | |
|
||||
90 | | Args:
|
||||
91 | | distance: Distance traveled.
|
||||
92 | | time: Time spent traveling.
|
||||
93 | |
|
||||
94 | | Returns:
|
||||
95 | | Speed as distance divided by time.
|
||||
96 | | """
|
||||
| |_______^
|
||||
97 | try:
|
||||
98 | return distance / time
|
||||
|
|
||||
help: Add `ZeroDivisionError` to the docstring
|
||||
|
||||
DOC501 Raised exception `AnotherError` missing from docstring
|
||||
--> DOC501_google.py:106:5
|
||||
|
|
||||
|
||||
@@ -61,6 +61,66 @@ DOC501 Raised exception `FasterThanLightError` missing from docstring
|
||||
|
|
||||
help: Add `FasterThanLightError` to the docstring
|
||||
|
||||
DOC501 Raised exception `ZeroDivisionError` missing from docstring
|
||||
--> DOC501_google.py:70:5
|
||||
|
|
||||
68 | # DOC501
|
||||
69 | def calculate_speed(distance: float, time: float) -> float:
|
||||
70 | / """Calculate speed as distance divided by time.
|
||||
71 | |
|
||||
72 | | Args:
|
||||
73 | | distance: Distance traveled.
|
||||
74 | | time: Time spent traveling.
|
||||
75 | |
|
||||
76 | | Returns:
|
||||
77 | | Speed as distance divided by time.
|
||||
78 | | """
|
||||
| |_______^
|
||||
79 | try:
|
||||
80 | return distance / time
|
||||
|
|
||||
help: Add `ZeroDivisionError` to the docstring
|
||||
|
||||
DOC501 Raised exception `ValueError` missing from docstring
|
||||
--> DOC501_google.py:88:5
|
||||
|
|
||||
86 | # DOC501
|
||||
87 | def calculate_speed(distance: float, time: float) -> float:
|
||||
88 | / """Calculate speed as distance divided by time.
|
||||
89 | |
|
||||
90 | | Args:
|
||||
91 | | distance: Distance traveled.
|
||||
92 | | time: Time spent traveling.
|
||||
93 | |
|
||||
94 | | Returns:
|
||||
95 | | Speed as distance divided by time.
|
||||
96 | | """
|
||||
| |_______^
|
||||
97 | try:
|
||||
98 | return distance / time
|
||||
|
|
||||
help: Add `ValueError` to the docstring
|
||||
|
||||
DOC501 Raised exception `ZeroDivisionError` missing from docstring
|
||||
--> DOC501_google.py:88:5
|
||||
|
|
||||
86 | # DOC501
|
||||
87 | def calculate_speed(distance: float, time: float) -> float:
|
||||
88 | / """Calculate speed as distance divided by time.
|
||||
89 | |
|
||||
90 | | Args:
|
||||
91 | | distance: Distance traveled.
|
||||
92 | | time: Time spent traveling.
|
||||
93 | |
|
||||
94 | | Returns:
|
||||
95 | | Speed as distance divided by time.
|
||||
96 | | """
|
||||
| |_______^
|
||||
97 | try:
|
||||
98 | return distance / time
|
||||
|
|
||||
help: Add `ZeroDivisionError` to the docstring
|
||||
|
||||
DOC501 Raised exception `AnotherError` missing from docstring
|
||||
--> DOC501_google.py:106:5
|
||||
|
|
||||
|
||||
@@ -94,7 +94,7 @@ pub(crate) fn capitalized(checker: &Checker, docstring: &Docstring) {
|
||||
let mut diagnostic = checker.report_diagnostic(
|
||||
FirstWordUncapitalized {
|
||||
first_word: first_word.to_string(),
|
||||
capitalized_word: capitalized_word.to_string(),
|
||||
capitalized_word: capitalized_word.clone(),
|
||||
},
|
||||
docstring.range(),
|
||||
);
|
||||
|
||||
@@ -166,6 +166,7 @@ mod tests {
|
||||
#[test_case(Rule::UndefinedName, Path::new("F821_30.py"))]
|
||||
#[test_case(Rule::UndefinedName, Path::new("F821_31.py"))]
|
||||
#[test_case(Rule::UndefinedName, Path::new("F821_32.pyi"))]
|
||||
#[test_case(Rule::UndefinedName, Path::new("F821_33.py"))]
|
||||
#[test_case(Rule::UndefinedExport, Path::new("F822_0.py"))]
|
||||
#[test_case(Rule::UndefinedExport, Path::new("F822_0.pyi"))]
|
||||
#[test_case(Rule::UndefinedExport, Path::new("F822_1.py"))]
|
||||
@@ -527,6 +528,38 @@ mod tests {
|
||||
import a",
|
||||
"f401_use_in_between_imports"
|
||||
)]
|
||||
#[test_case(
|
||||
r"
|
||||
if cond:
|
||||
import a
|
||||
import a.b
|
||||
a.foo()
|
||||
",
|
||||
"f401_same_branch"
|
||||
)]
|
||||
#[test_case(
|
||||
r"
|
||||
try:
|
||||
import a.b.c
|
||||
except ImportError:
|
||||
import argparse
|
||||
import a
|
||||
a.b = argparse.Namespace()
|
||||
",
|
||||
"f401_different_branch"
|
||||
)]
|
||||
#[test_case(
|
||||
r"
|
||||
import mlflow.pyfunc.loaders.chat_agent
|
||||
import mlflow.pyfunc.loaders.chat_model
|
||||
import mlflow.pyfunc.loaders.code_model
|
||||
from mlflow.utils.pydantic_utils import IS_PYDANTIC_V2_OR_NEWER
|
||||
|
||||
if IS_PYDANTIC_V2_OR_NEWER:
|
||||
import mlflow.pyfunc.loaders.responses_agent
|
||||
",
|
||||
"f401_type_checking"
|
||||
)]
|
||||
fn f401_preview_refined_submodule_handling(contents: &str, snapshot: &str) {
|
||||
let diagnostics = test_contents(
|
||||
&SourceKind::Python(dedent(contents).to_string()),
|
||||
|
||||
@@ -898,6 +898,10 @@ fn best_match<'a, 'b>(
|
||||
|
||||
#[inline]
|
||||
fn has_simple_shadowed_bindings(scope: &Scope, id: BindingId, semantic: &SemanticModel) -> bool {
|
||||
let Some(binding_node) = semantic.binding(id).source else {
|
||||
return false;
|
||||
};
|
||||
|
||||
scope.shadowed_bindings(id).enumerate().all(|(i, shadow)| {
|
||||
let shadowed_binding = semantic.binding(shadow);
|
||||
// Bail if one of the shadowed bindings is
|
||||
@@ -912,6 +916,34 @@ fn has_simple_shadowed_bindings(scope: &Scope, id: BindingId, semantic: &Semanti
|
||||
if i > 0 && shadowed_binding.is_used() {
|
||||
return false;
|
||||
}
|
||||
// We want to allow a situation like this:
|
||||
//
|
||||
// ```python
|
||||
// import a.b
|
||||
// if TYPE_CHECKING:
|
||||
// import a.b.c
|
||||
// ```
|
||||
// but bail in a situation like this:
|
||||
//
|
||||
// ```python
|
||||
// try:
|
||||
// import a.b
|
||||
// except ImportError:
|
||||
// import argparse
|
||||
// import a
|
||||
// a.b = argparse.Namespace()
|
||||
// ```
|
||||
//
|
||||
// So we require that all the shadowed bindings dominate the
|
||||
// last live binding for the import. That is: if the last live
|
||||
// binding is executed it should imply that all the shadowed
|
||||
// bindings were executed as well.
|
||||
if shadowed_binding
|
||||
.source
|
||||
.is_none_or(|node_id| !semantic.dominates(node_id, binding_node))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
matches!(
|
||||
shadowed_binding.kind,
|
||||
BindingKind::Import(_) | BindingKind::SubmoduleImport(_)
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
F821 Undefined name `__class__`
|
||||
--> F821_33.py:15:13
|
||||
|
|
||||
14 | # Test: lambda outside class (should still fail)
|
||||
15 | h = lambda: __class__
|
||||
| ^^^^^^^^^
|
||||
16 |
|
||||
17 | # Test: lambda referencing module-level variable (should not be flagged as F821)
|
||||
|
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
F401 [*] `a.b` imported but unused
|
||||
--> f401_preview_submodule.py:4:12
|
||||
|
|
||||
2 | if cond:
|
||||
3 | import a
|
||||
4 | import a.b
|
||||
| ^^^
|
||||
5 | a.foo()
|
||||
|
|
||||
help: Remove unused import: `a.b`
|
||||
1 |
|
||||
2 | if cond:
|
||||
3 | import a
|
||||
- import a.b
|
||||
4 | a.foo()
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user