Compare commits
51 Commits
v0.1.6
...
zb/fmt-ski
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c8017ef13 | ||
|
|
d9845a2628 | ||
|
|
1f14d9a9f7 | ||
|
|
0202a49297 | ||
|
|
d0591561c9 | ||
|
|
2f5859e79e | ||
|
|
16085339bc | ||
|
|
074812115f | ||
|
|
9b17724d77 | ||
|
|
0d4af9d3c6 | ||
|
|
1dbfab9a0c | ||
|
|
501cca8b72 | ||
|
|
626b0577cd | ||
|
|
017e829115 | ||
|
|
2590aa30ae | ||
|
|
852a8f4a4f | ||
|
|
8365d2e0fd | ||
|
|
5b726f70f4 | ||
|
|
727e389cac | ||
|
|
0cb438dd65 | ||
|
|
63a87dda63 | ||
|
|
359a68d18f | ||
|
|
948094e691 | ||
|
|
f1ed0f27c2 | ||
|
|
5ce6299e22 | ||
|
|
5373759f62 | ||
|
|
d9151b1948 | ||
|
|
b61ce7fa46 | ||
|
|
6fb6478887 | ||
|
|
bf729e7a77 | ||
|
|
6ca2aaa245 | ||
|
|
e306359411 | ||
|
|
aec80dc3ab | ||
|
|
10d937c1a1 | ||
|
|
653e51ae97 | ||
|
|
04f0625d23 | ||
|
|
29dc8b986b | ||
|
|
f750257ef4 | ||
|
|
0f2c9ab4d2 | ||
|
|
779a5a9c32 | ||
|
|
bba43029d4 | ||
|
|
1de5425f7d | ||
|
|
71573fd35c | ||
|
|
95e2f632e6 | ||
|
|
00a015ca24 | ||
|
|
94178a0320 | ||
|
|
7165f8f05d | ||
|
|
415e808046 | ||
|
|
9279114521 | ||
|
|
8b86e8004d | ||
|
|
a7fc785cc5 |
3
.gitattributes
vendored
3
.gitattributes
vendored
@@ -3,5 +3,8 @@
|
||||
crates/ruff_linter/resources/test/fixtures/isort/line_ending_crlf.py text eol=crlf
|
||||
crates/ruff_linter/resources/test/fixtures/pycodestyle/W605_1.py text eol=crlf
|
||||
|
||||
crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring_code_examples_crlf.py text eol=crlf
|
||||
crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples_crlf.py.snap text eol=crlf
|
||||
|
||||
ruff.schema.json linguist-generated=true text=auto eol=lf
|
||||
*.md.snap linguist-language=Markdown
|
||||
|
||||
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@@ -44,6 +44,7 @@ jobs:
|
||||
- "!crates/ruff_shrinking/**"
|
||||
- scripts/*
|
||||
- .github/workflows/ci.yaml
|
||||
- python/**
|
||||
|
||||
formatter:
|
||||
- Cargo.toml
|
||||
@@ -58,6 +59,7 @@ jobs:
|
||||
- crates/ruff_python_parser/**
|
||||
- crates/ruff_dev/**
|
||||
- scripts/*
|
||||
- python/**
|
||||
- .github/workflows/ci.yaml
|
||||
|
||||
cargo-fmt:
|
||||
|
||||
58
.github/workflows/release.yaml
vendored
58
.github/workflows/release.yaml
vendored
@@ -516,6 +516,62 @@ jobs:
|
||||
files: binaries/*
|
||||
tag_name: v${{ inputs.tag }}
|
||||
|
||||
docker-publish:
|
||||
# This action doesn't need to wait on any other task, it's easy to re-tag if something failed and we're validating
|
||||
# the tag here also
|
||||
name: Push Docker image ghcr.io/astral-sh/ruff
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: release
|
||||
permissions:
|
||||
# For the docker push
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.sha }}
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ghcr.io/astral-sh/ruff
|
||||
|
||||
- name: Check tag consistency
|
||||
# Unlike validate-tag we don't check if the commit is on the main branch, but it seems good enough since we can
|
||||
# change docker tags
|
||||
if: ${{ inputs.tag }}
|
||||
run: |
|
||||
version=$(grep "version = " pyproject.toml | sed -e 's/version = "\(.*\)"/\1/g')
|
||||
if [ "${{ inputs.tag }}" != "${version}" ]; then
|
||||
echo "The input tag does not match the version from pyproject.toml:" >&2
|
||||
echo "${{ inputs.tag }}" >&2
|
||||
echo "${version}" >&2
|
||||
exit 1
|
||||
else
|
||||
echo "Releasing ${version}"
|
||||
fi
|
||||
|
||||
- name: "Build and push Docker image"
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
# Reuse the builder
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
push: ${{ inputs.tag != '' }}
|
||||
tags: ghcr.io/astral-sh/ruff:latest,ghcr.io/astral-sh/ruff:${{ inputs.tag || 'dry-run' }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
# After the release has been published, we update downstream repositories
|
||||
# This is separate because if this fails the release is still fine, we just need to do some manual workflow triggers
|
||||
update-dependents:
|
||||
@@ -524,7 +580,7 @@ jobs:
|
||||
needs: publish-release
|
||||
steps:
|
||||
- name: "Update pre-commit mirror"
|
||||
uses: actions/github-script@v6
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.RUFF_PRE_COMMIT_PAT }}
|
||||
script: |
|
||||
|
||||
@@ -295,7 +295,7 @@ To preview any changes to the documentation locally:
|
||||
|
||||
```shell
|
||||
# For contributors.
|
||||
mkdocs serve -f mkdocs.generated.yml
|
||||
mkdocs serve -f mkdocs.public.yml
|
||||
|
||||
# For members of the Astral org, which has access to MkDocs Insiders via sponsorship.
|
||||
mkdocs serve -f mkdocs.insiders.yml
|
||||
|
||||
80
Cargo.lock
generated
80
Cargo.lock
generated
@@ -405,9 +405,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "codspeed"
|
||||
version = "2.3.1"
|
||||
version = "2.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "918b13a0f1a32460ab3bd5debd56b5a27a7071fa5ff5dfeb3a5cf291a85b174b"
|
||||
checksum = "0eb4ab4dcb6554eb4f590fb16f99d3b102ab76f5f56554c9a5340518b32c499b"
|
||||
dependencies = [
|
||||
"colored",
|
||||
"libc",
|
||||
@@ -416,9 +416,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "codspeed-criterion-compat"
|
||||
version = "2.3.1"
|
||||
version = "2.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c683c7fef2b873fbbdf4062782914c652309951244bf0bd362fe608b7d6f901c"
|
||||
checksum = "cc07a3d3f7e0c8961d0ffdee149d39b231bafdcdc3d978dc5ad790c615f55f3f"
|
||||
dependencies = [
|
||||
"codspeed",
|
||||
"colored",
|
||||
@@ -444,9 +444,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "configparser"
|
||||
version = "3.0.2"
|
||||
version = "3.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5458d9d1a587efaf5091602c59d299696a3877a439c8f6d461a2d3cce11df87a"
|
||||
checksum = "e0e56e414a2a52ab2a104f85cd40933c2fbc278b83637facf646ecf451b49237"
|
||||
|
||||
[[package]]
|
||||
name = "console"
|
||||
@@ -903,15 +903,15 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
|
||||
|
||||
[[package]]
|
||||
name = "globset"
|
||||
version = "0.4.13"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "759c97c1e17c55525b57192c06a267cda0ac5210b222d6b82189a2338fa1c13d"
|
||||
checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"bstr",
|
||||
"fnv",
|
||||
"log",
|
||||
"regex",
|
||||
"regex-automata 0.4.3",
|
||||
"regex-syntax 0.8.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1170,9 +1170,9 @@ checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.64"
|
||||
version = "0.3.65"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a"
|
||||
checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
@@ -1793,9 +1793,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.69"
|
||||
version = "1.0.70"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da"
|
||||
checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -2334,6 +2334,7 @@ dependencies = [
|
||||
"itertools 0.11.0",
|
||||
"memchr",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"ruff_cache",
|
||||
"ruff_formatter",
|
||||
"ruff_macros",
|
||||
@@ -3194,20 +3195,20 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-log"
|
||||
version = "0.1.3"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922"
|
||||
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"log",
|
||||
"once_cell",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.17"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77"
|
||||
checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
|
||||
dependencies = [
|
||||
"matchers",
|
||||
"nu-ansi-term",
|
||||
@@ -3367,9 +3368,9 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.5.0"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc"
|
||||
checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"rand",
|
||||
@@ -3379,9 +3380,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "uuid-macro-internal"
|
||||
version = "1.5.0"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d8c6bba9b149ee82950daefc9623b32bb1dacbfb1890e352f6b887bd582adaf"
|
||||
checksum = "f49e7f3f3db8040a100710a11932239fd30697115e2ba4107080d8252939845e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3460,9 +3461,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.87"
|
||||
version = "0.2.88"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342"
|
||||
checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"wasm-bindgen-macro",
|
||||
@@ -3470,9 +3471,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.87"
|
||||
version = "0.2.88"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd"
|
||||
checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
@@ -3485,9 +3486,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.37"
|
||||
version = "0.4.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03"
|
||||
checksum = "9afec9963e3d0994cac82455b2b3502b81a7f40f9a0d32181f7528d9f4b43e02"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
@@ -3497,9 +3498,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.87"
|
||||
version = "0.2.88"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d"
|
||||
checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
@@ -3507,9 +3508,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.87"
|
||||
version = "0.2.88"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
|
||||
checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3520,15 +3521,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.87"
|
||||
version = "0.2.88"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1"
|
||||
checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-test"
|
||||
version = "0.3.37"
|
||||
version = "0.3.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e6e302a7ea94f83a6d09e78e7dc7d9ca7b186bc2829c24a22d0753efd680671"
|
||||
checksum = "c6433b7c56db97397842c46b67e11873eda263170afeb3a2dc74a7cb370fee0d"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"js-sys",
|
||||
@@ -3540,12 +3541,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-test-macro"
|
||||
version = "0.3.37"
|
||||
version = "0.3.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ecb993dd8c836930ed130e020e77d9b2e65dd0fbab1b67c790b0f5d80b11a575"
|
||||
checksum = "493fcbab756bb764fa37e6bee8cec2dd709eb4273d06d0c282a5e74275ded735"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.39",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -19,7 +19,7 @@ clap = { version = "4.4.7", features = ["derive"] }
|
||||
colored = { version = "2.0.0" }
|
||||
filetime = { version = "0.2.20" }
|
||||
glob = { version = "0.3.1" }
|
||||
globset = { version = "0.4.10" }
|
||||
globset = { version = "0.4.14" }
|
||||
ignore = { version = "0.4.20" }
|
||||
insta = { version = "1.34.0", feature = ["filters", "glob"] }
|
||||
is-macro = { version = "0.3.0" }
|
||||
@@ -29,7 +29,7 @@ log = { version = "0.4.17" }
|
||||
memchr = { version = "2.6.4" }
|
||||
once_cell = { version = "1.17.1" }
|
||||
path-absolutize = { version = "3.1.1" }
|
||||
proc-macro2 = { version = "1.0.69" }
|
||||
proc-macro2 = { version = "1.0.70" }
|
||||
quote = { version = "1.0.23" }
|
||||
regex = { version = "1.10.2" }
|
||||
rustc-hash = { version = "1.1.0" }
|
||||
@@ -48,11 +48,11 @@ thiserror = { version = "1.0.50" }
|
||||
toml = { version = "0.7.8" }
|
||||
tracing = { version = "0.1.40" }
|
||||
tracing-indicatif = { version = "0.3.4" }
|
||||
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
unicode-ident = { version = "1.0.12" }
|
||||
unicode_names2 = { version = "1.2.0" }
|
||||
unicode-width = { version = "0.1.11" }
|
||||
uuid = { version = "1.5.0", features = ["v4", "fast-rng", "macro-diagnostics", "js"] }
|
||||
uuid = { version = "1.6.1", features = ["v4", "fast-rng", "macro-diagnostics", "js"] }
|
||||
wsl = { version = "0.1.0" }
|
||||
|
||||
[workspace.lints.rust]
|
||||
|
||||
38
Dockerfile
Normal file
38
Dockerfile
Normal file
@@ -0,0 +1,38 @@
|
||||
FROM --platform=$BUILDPLATFORM ubuntu as build
|
||||
ENV HOME="/root"
|
||||
WORKDIR $HOME
|
||||
|
||||
RUN apt update && apt install -y build-essential curl python3-venv
|
||||
|
||||
# Setup zig as cross compiling linker
|
||||
RUN python3 -m venv $HOME/.venv
|
||||
RUN .venv/bin/pip install cargo-zigbuild
|
||||
ENV PATH="$HOME/.venv/bin:$PATH"
|
||||
|
||||
# Install rust
|
||||
ARG TARGETPLATFORM
|
||||
RUN case "$TARGETPLATFORM" in \
|
||||
"linux/arm64") echo "aarch64-unknown-linux-musl" > rust_target.txt ;; \
|
||||
"linux/amd64") echo "x86_64-unknown-linux-musl" > rust_target.txt ;; \
|
||||
*) exit 1 ;; \
|
||||
esac
|
||||
# Update rustup whenever we bump the rust version
|
||||
COPY rust-toolchain.toml rust-toolchain.toml
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --target $(cat rust_target.txt) --profile minimal --default-toolchain none
|
||||
ENV PATH="$HOME/.cargo/bin:$PATH"
|
||||
# Installs the correct toolchain version from rust-toolchain.toml and then the musl target
|
||||
RUN rustup target add $(cat rust_target.txt)
|
||||
|
||||
# Build
|
||||
COPY crates crates
|
||||
COPY Cargo.toml Cargo.toml
|
||||
COPY Cargo.lock Cargo.lock
|
||||
RUN cargo zigbuild --bin ruff --target $(cat rust_target.txt) --release
|
||||
RUN cp target/$(cat rust_target.txt)/release/ruff /ruff
|
||||
# TODO: Optimize binary size, with a version that also works when cross compiling
|
||||
# RUN strip --strip-all /ruff
|
||||
|
||||
FROM scratch
|
||||
COPY --from=build /ruff /ruff
|
||||
WORKDIR /io
|
||||
ENTRYPOINT ["/ruff"]
|
||||
@@ -19,7 +19,7 @@ ruff_workspace = { path = "../ruff_workspace" }
|
||||
anyhow = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
colored = { workspace = true }
|
||||
configparser = { version = "3.0.2" }
|
||||
configparser = { version = "3.0.3" }
|
||||
itertools = { workspace = true }
|
||||
log = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
|
||||
@@ -37,7 +37,7 @@ serde_json.workspace = true
|
||||
url = "2.3.1"
|
||||
ureq = "2.8.0"
|
||||
criterion = { version = "0.5.1", default-features = false }
|
||||
codspeed-criterion-compat = { version="2.3.1", default-features = false, optional = true}
|
||||
codspeed-criterion-compat = { version="2.3.3", default-features = false, optional = true}
|
||||
|
||||
[dev-dependencies]
|
||||
ruff_linter.path = "../ruff_linter"
|
||||
|
||||
0
crates/ruff_cli/resources/test/fixtures/include-test/a.py
vendored
Normal file
0
crates/ruff_cli/resources/test/fixtures/include-test/a.py
vendored
Normal file
0
crates/ruff_cli/resources/test/fixtures/include-test/b.py
vendored
Normal file
0
crates/ruff_cli/resources/test/fixtures/include-test/b.py
vendored
Normal file
0
crates/ruff_cli/resources/test/fixtures/include-test/nested-project/e.py
vendored
Normal file
0
crates/ruff_cli/resources/test/fixtures/include-test/nested-project/e.py
vendored
Normal file
2
crates/ruff_cli/resources/test/fixtures/include-test/nested-project/pyproject.toml
vendored
Normal file
2
crates/ruff_cli/resources/test/fixtures/include-test/nested-project/pyproject.toml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
[tool.ruff]
|
||||
select = []
|
||||
2
crates/ruff_cli/resources/test/fixtures/include-test/pyproject.toml
vendored
Normal file
2
crates/ruff_cli/resources/test/fixtures/include-test/pyproject.toml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
[tool.ruff]
|
||||
include = ["a.py", "subdirectory/c.py"]
|
||||
0
crates/ruff_cli/resources/test/fixtures/include-test/subdirectory/c.py
vendored
Normal file
0
crates/ruff_cli/resources/test/fixtures/include-test/subdirectory/c.py
vendored
Normal file
0
crates/ruff_cli/resources/test/fixtures/include-test/subdirectory/d.py
vendored
Normal file
0
crates/ruff_cli/resources/test/fixtures/include-test/subdirectory/d.py
vendored
Normal file
@@ -88,6 +88,7 @@ pub enum Command {
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
pub struct CheckCommand {
|
||||
/// List of files or directories to check.
|
||||
#[clap(help = "List of files or directories to check [default: .]")]
|
||||
pub files: Vec<PathBuf>,
|
||||
/// Apply fixes to resolve lint violations.
|
||||
/// Use `--no-fix` to disable or `--unsafe-fixes` to include unsafe fixes.
|
||||
@@ -363,6 +364,7 @@ pub struct CheckCommand {
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
pub struct FormatCommand {
|
||||
/// List of files or directories to format.
|
||||
#[clap(help = "List of files or directories to format [default: .]")]
|
||||
pub files: Vec<PathBuf>,
|
||||
/// Avoid writing any formatted files back; instead, exit with a non-zero status code if any
|
||||
/// files would have been modified, and zero otherwise.
|
||||
|
||||
@@ -34,7 +34,7 @@ use crate::args::{CliOverrides, FormatArguments};
|
||||
use crate::cache::{Cache, FileCacheKey, PackageCacheMap, PackageCaches};
|
||||
use crate::panic::{catch_unwind, PanicError};
|
||||
use crate::resolve::resolve;
|
||||
use crate::ExitStatus;
|
||||
use crate::{resolve_default_files, ExitStatus};
|
||||
|
||||
#[derive(Debug, Copy, Clone, is_macro::Is)]
|
||||
pub(crate) enum FormatMode {
|
||||
@@ -60,7 +60,7 @@ impl FormatMode {
|
||||
|
||||
/// Format a set of files, and return the exit status.
|
||||
pub(crate) fn format(
|
||||
cli: &FormatArguments,
|
||||
cli: FormatArguments,
|
||||
overrides: &CliOverrides,
|
||||
log_level: LogLevel,
|
||||
) -> Result<ExitStatus> {
|
||||
@@ -70,8 +70,9 @@ pub(crate) fn format(
|
||||
overrides,
|
||||
cli.stdin_filename.as_deref(),
|
||||
)?;
|
||||
let mode = FormatMode::from_cli(cli);
|
||||
let (paths, resolver) = python_files_in_path(&cli.files, &pyproject_config, overrides)?;
|
||||
let mode = FormatMode::from_cli(&cli);
|
||||
let files = resolve_default_files(cli.files, false);
|
||||
let (paths, resolver) = python_files_in_path(&files, &pyproject_config, overrides)?;
|
||||
|
||||
if paths.is_empty() {
|
||||
warn_user_once!("No Python files found under the given path(s)");
|
||||
|
||||
@@ -101,6 +101,19 @@ fn is_stdin(files: &[PathBuf], stdin_filename: Option<&Path>) -> bool {
|
||||
file == Path::new("-")
|
||||
}
|
||||
|
||||
/// Returns the default set of files if none are provided, otherwise returns `None`.
|
||||
fn resolve_default_files(files: Vec<PathBuf>, is_stdin: bool) -> Vec<PathBuf> {
|
||||
if files.is_empty() {
|
||||
if is_stdin {
|
||||
vec![Path::new("-").to_path_buf()]
|
||||
} else {
|
||||
vec![Path::new(".").to_path_buf()]
|
||||
}
|
||||
} else {
|
||||
files
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the actual value of the `format` desired from either `output_format`
|
||||
/// or `format`, and warn the user if they're using the deprecated form.
|
||||
fn resolve_help_output_format(output_format: HelpFormat, format: Option<HelpFormat>) -> HelpFormat {
|
||||
@@ -196,7 +209,7 @@ fn format(args: FormatCommand, log_level: LogLevel) -> Result<ExitStatus> {
|
||||
if is_stdin(&cli.files, cli.stdin_filename.as_deref()) {
|
||||
commands::format_stdin::format_stdin(&cli, &overrides)
|
||||
} else {
|
||||
commands::format::format(&cli, &overrides, log_level)
|
||||
commands::format::format(cli, &overrides, log_level)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,17 +235,15 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
|
||||
};
|
||||
let stderr_writer = Box::new(BufWriter::new(io::stderr()));
|
||||
|
||||
let is_stdin = is_stdin(&cli.files, cli.stdin_filename.as_deref());
|
||||
let files = resolve_default_files(cli.files, is_stdin);
|
||||
|
||||
if cli.show_settings {
|
||||
commands::show_settings::show_settings(
|
||||
&cli.files,
|
||||
&pyproject_config,
|
||||
&overrides,
|
||||
&mut writer,
|
||||
)?;
|
||||
commands::show_settings::show_settings(&files, &pyproject_config, &overrides, &mut writer)?;
|
||||
return Ok(ExitStatus::Success);
|
||||
}
|
||||
if cli.show_files {
|
||||
commands::show_files::show_files(&cli.files, &pyproject_config, &overrides, &mut writer)?;
|
||||
commands::show_files::show_files(&files, &pyproject_config, &overrides, &mut writer)?;
|
||||
return Ok(ExitStatus::Success);
|
||||
}
|
||||
|
||||
@@ -295,8 +306,7 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
|
||||
if !fix_mode.is_generate() {
|
||||
warn_user!("--fix is incompatible with --add-noqa.");
|
||||
}
|
||||
let modifications =
|
||||
commands::add_noqa::add_noqa(&cli.files, &pyproject_config, &overrides)?;
|
||||
let modifications = commands::add_noqa::add_noqa(&files, &pyproject_config, &overrides)?;
|
||||
if modifications > 0 && log_level >= LogLevel::Default {
|
||||
let s = if modifications == 1 { "" } else { "s" };
|
||||
#[allow(clippy::print_stderr)]
|
||||
@@ -323,7 +333,7 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
|
||||
// Configure the file watcher.
|
||||
let (tx, rx) = channel();
|
||||
let mut watcher = recommended_watcher(tx)?;
|
||||
for file in &cli.files {
|
||||
for file in &files {
|
||||
watcher.watch(file, RecursiveMode::Recursive)?;
|
||||
}
|
||||
if let Some(file) = pyproject_config.path.as_ref() {
|
||||
@@ -335,7 +345,7 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
|
||||
printer.write_to_user("Starting linter in watch mode...\n");
|
||||
|
||||
let messages = commands::check::check(
|
||||
&cli.files,
|
||||
&files,
|
||||
&pyproject_config,
|
||||
&overrides,
|
||||
cache.into(),
|
||||
@@ -368,7 +378,7 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
|
||||
printer.write_to_user("File change detected...\n");
|
||||
|
||||
let messages = commands::check::check(
|
||||
&cli.files,
|
||||
&files,
|
||||
&pyproject_config,
|
||||
&overrides,
|
||||
cache.into(),
|
||||
@@ -382,8 +392,6 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let is_stdin = is_stdin(&cli.files, cli.stdin_filename.as_deref());
|
||||
|
||||
// Generate lint violations.
|
||||
let diagnostics = if is_stdin {
|
||||
commands::check_stdin::check_stdin(
|
||||
@@ -395,7 +403,7 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
|
||||
)?
|
||||
} else {
|
||||
commands::check::check(
|
||||
&cli.files,
|
||||
&files,
|
||||
&pyproject_config,
|
||||
&overrides,
|
||||
cache.into(),
|
||||
|
||||
@@ -43,6 +43,53 @@ if condition:
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_files() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
fs::write(
|
||||
tempdir.path().join("foo.py"),
|
||||
r#"
|
||||
foo = "needs formatting"
|
||||
"#,
|
||||
)?;
|
||||
fs::write(
|
||||
tempdir.path().join("bar.py"),
|
||||
r#"
|
||||
bar = "needs formatting"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["format", "--isolated", "--no-cache", "--check"]).current_dir(tempdir.path()), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
Would reformat: bar.py
|
||||
Would reformat: foo.py
|
||||
2 files would be reformatted
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_warn_stdin_filename_with_files() {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["format", "--isolated", "--stdin-filename", "foo.py"])
|
||||
.arg("foo.py")
|
||||
.pass_stdin("foo = 1"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
foo = 1
|
||||
|
||||
----- stderr -----
|
||||
warning: Ignoring file foo.py in favor of standard input.
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_options() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
101
crates/ruff_cli/tests/resolve_files.rs
Normal file
101
crates/ruff_cli/tests/resolve_files.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
#![cfg(not(target_family = "wasm"))]
|
||||
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use std::str;
|
||||
|
||||
use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
|
||||
const BIN_NAME: &str = "ruff";
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
const TEST_FILTERS: &[(&str, &str)] = &[(".*/resources/test/fixtures/", "[BASEPATH]/")];
|
||||
#[cfg(target_os = "windows")]
|
||||
const TEST_FILTERS: &[(&str, &str)] = &[
|
||||
(r".*\\resources\\test\\fixtures\\", "[BASEPATH]\\"),
|
||||
(r"\\", "/"),
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn check_project_include_defaults() {
|
||||
// Defaults to checking the current working directory
|
||||
//
|
||||
// The test directory includes:
|
||||
// - A pyproject.toml which specifies an include
|
||||
// - A nested pyproject.toml which has a Ruff section
|
||||
//
|
||||
// The nested project should all be checked instead of respecting the parent includes
|
||||
|
||||
insta::with_settings!({
|
||||
filters => TEST_FILTERS.to_vec()
|
||||
}, {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["check", "--show-files"]).current_dir(Path::new("./resources/test/fixtures/include-test")), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
[BASEPATH]/include-test/a.py
|
||||
[BASEPATH]/include-test/nested-project/e.py
|
||||
[BASEPATH]/include-test/nested-project/pyproject.toml
|
||||
[BASEPATH]/include-test/subdirectory/c.py
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_project_respects_direct_paths() {
|
||||
// Given a direct path not included in the project `includes`, it should be checked
|
||||
|
||||
insta::with_settings!({
|
||||
filters => TEST_FILTERS.to_vec()
|
||||
}, {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["check", "--show-files", "b.py"]).current_dir(Path::new("./resources/test/fixtures/include-test")), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
[BASEPATH]/include-test/b.py
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_project_respects_subdirectory_includes() {
|
||||
// Given a direct path to a subdirectory, the include should be respected
|
||||
|
||||
insta::with_settings!({
|
||||
filters => TEST_FILTERS.to_vec()
|
||||
}, {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["check", "--show-files", "subdirectory"]).current_dir(Path::new("./resources/test/fixtures/include-test")), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
[BASEPATH]/include-test/subdirectory/c.py
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_project_from_project_subdirectory_respects_includes() {
|
||||
// Run from a project subdirectory, the include specified in the parent directory should be respected
|
||||
|
||||
insta::with_settings!({
|
||||
filters => TEST_FILTERS.to_vec()
|
||||
}, {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["check", "--show-files"]).current_dir(Path::new("./resources/test/fixtures/include-test/subdirectory")), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
[BASEPATH]/include-test/subdirectory/c.py
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
});
|
||||
}
|
||||
65
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S202.py
vendored
Normal file
65
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S202.py
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
import sys
|
||||
import tarfile
|
||||
import tempfile
|
||||
|
||||
|
||||
def unsafe_archive_handler(filename):
|
||||
tar = tarfile.open(filename)
|
||||
tar.extractall(path=tempfile.mkdtemp())
|
||||
tar.close()
|
||||
|
||||
|
||||
def managed_members_archive_handler(filename):
|
||||
tar = tarfile.open(filename)
|
||||
tar.extractall(path=tempfile.mkdtemp(), members=members_filter(tar))
|
||||
tar.close()
|
||||
|
||||
|
||||
def list_members_archive_handler(filename):
|
||||
tar = tarfile.open(filename)
|
||||
tar.extractall(path=tempfile.mkdtemp(), members=[])
|
||||
tar.close()
|
||||
|
||||
|
||||
def provided_members_archive_handler(filename):
|
||||
tar = tarfile.open(filename)
|
||||
tarfile.extractall(path=tempfile.mkdtemp(), members=tar)
|
||||
tar.close()
|
||||
|
||||
|
||||
def filter_data(filename):
|
||||
tar = tarfile.open(filename)
|
||||
tarfile.extractall(path=tempfile.mkdtemp(), filter="data")
|
||||
tar.close()
|
||||
|
||||
|
||||
def filter_fully_trusted(filename):
|
||||
tar = tarfile.open(filename)
|
||||
tarfile.extractall(path=tempfile.mkdtemp(), filter="fully_trusted")
|
||||
tar.close()
|
||||
|
||||
|
||||
def filter_tar(filename):
|
||||
tar = tarfile.open(filename)
|
||||
tarfile.extractall(path=tempfile.mkdtemp(), filter="tar")
|
||||
tar.close()
|
||||
|
||||
|
||||
def members_filter(tarfile):
|
||||
result = []
|
||||
for member in tarfile.getmembers():
|
||||
if '../' in member.name:
|
||||
print('Member name container directory traversal sequence')
|
||||
continue
|
||||
elif (member.issym() or member.islnk()) and ('../' in member.linkname):
|
||||
print('Symlink to external resource')
|
||||
continue
|
||||
result.append(member)
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) > 1:
|
||||
filename = sys.argv[1]
|
||||
unsafe_archive_handler(filename)
|
||||
managed_members_archive_handler(filename)
|
||||
13
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S611.py
vendored
Normal file
13
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S611.py
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
from django.db.models.expressions import RawSQL
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
User.objects.annotate(val=RawSQL('secure', []))
|
||||
User.objects.annotate(val=RawSQL('%secure' % 'nos', []))
|
||||
User.objects.annotate(val=RawSQL('{}secure'.format('no'), []))
|
||||
raw = '"username") AS "val" FROM "auth_user" WHERE "username"="admin" --'
|
||||
User.objects.annotate(val=RawSQL(raw, []))
|
||||
raw = '"username") AS "val" FROM "auth_user"' \
|
||||
' WHERE "username"="admin" OR 1=%s --'
|
||||
User.objects.annotate(val=RawSQL(raw, [0]))
|
||||
User.objects.annotate(val=RawSQL(sql='{}secure'.format('no'), params=[]))
|
||||
User.objects.annotate(val=RawSQL(params=[], sql='{}secure'.format('no')))
|
||||
149
crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B015.ipynb
vendored
Normal file
149
crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B015.ipynb
vendored
Normal file
@@ -0,0 +1,149 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "33faf7ad-a3fd-4ac4-a0c3-52e507ed49df",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"x = 1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"id": "481fb4bf-c1b9-47da-927f-3cfdfe4b49ec",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"True"
|
||||
]
|
||||
},
|
||||
"execution_count": 2,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Simple case\n",
|
||||
"x == 1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"id": "2f0c65a5-0a0e-4080-afce-5a8ed0d706df",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"True"
|
||||
]
|
||||
},
|
||||
"execution_count": 3,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Only skip the last expression\n",
|
||||
"x == 1 # B018\n",
|
||||
"x == 1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"id": "5a3fd75d-26d9-44f7-b013-1684aabfd0ae",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Nested expressions isn't relevant\n",
|
||||
"if True:\n",
|
||||
" x == 1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"id": "e00e1afa-b76c-4774-be2a-7223641579e4",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Semicolons shouldn't affect the output\n",
|
||||
"x == 1;"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"id": "05eab5b9-e2ba-4954-8ef3-b035a79573fe",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"True"
|
||||
]
|
||||
},
|
||||
"execution_count": 6,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Semicolons with multiple expressions\n",
|
||||
"x == 1; x == 1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"id": "9cbbddc5-83fc-4fdb-81ab-53a3912ae898",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"True"
|
||||
]
|
||||
},
|
||||
"execution_count": 7,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Comments, newlines and whitespace\n",
|
||||
"x == 1 # comment\n",
|
||||
"\n",
|
||||
"# another comment"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python (ruff-playground)",
|
||||
"language": "python",
|
||||
"name": "ruff-playground"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
149
crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B018.ipynb
vendored
Normal file
149
crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B018.ipynb
vendored
Normal file
@@ -0,0 +1,149 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "33faf7ad-a3fd-4ac4-a0c3-52e507ed49df",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"x = 1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"id": "481fb4bf-c1b9-47da-927f-3cfdfe4b49ec",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"1"
|
||||
]
|
||||
},
|
||||
"execution_count": 2,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Simple case\n",
|
||||
"x"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"id": "2f0c65a5-0a0e-4080-afce-5a8ed0d706df",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"1"
|
||||
]
|
||||
},
|
||||
"execution_count": 3,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Only skip the last expression\n",
|
||||
"x # B018\n",
|
||||
"x"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"id": "5a3fd75d-26d9-44f7-b013-1684aabfd0ae",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Nested expressions isn't relevant\n",
|
||||
"if True:\n",
|
||||
" x"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"id": "e00e1afa-b76c-4774-be2a-7223641579e4",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Semicolons shouldn't affect the output\n",
|
||||
"x;"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"id": "05eab5b9-e2ba-4954-8ef3-b035a79573fe",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"1"
|
||||
]
|
||||
},
|
||||
"execution_count": 6,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Semicolons with multiple expressions\n",
|
||||
"x; x"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"id": "9cbbddc5-83fc-4fdb-81ab-53a3912ae898",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"1"
|
||||
]
|
||||
},
|
||||
"execution_count": 7,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Comments, newlines and whitespace\n",
|
||||
"x # comment\n",
|
||||
"\n",
|
||||
"# another comment"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python (ruff-playground)",
|
||||
"language": "python",
|
||||
"name": "ruff-playground"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
@@ -177,3 +177,33 @@ for i in range(10):
|
||||
for i in range(10):
|
||||
...
|
||||
pass
|
||||
|
||||
from typing import Protocol
|
||||
|
||||
|
||||
class Repro(Protocol):
|
||||
def func(self) -> str:
|
||||
"""Docstring"""
|
||||
...
|
||||
|
||||
def impl(self) -> str:
|
||||
"""Docstring"""
|
||||
return self.func()
|
||||
|
||||
|
||||
import abc
|
||||
|
||||
|
||||
class Repro:
|
||||
@abc.abstractmethod
|
||||
def func(self) -> str:
|
||||
"""Docstring"""
|
||||
...
|
||||
|
||||
def impl(self) -> str:
|
||||
"""Docstring"""
|
||||
return self.func()
|
||||
|
||||
def stub(self) -> str:
|
||||
"""Docstring"""
|
||||
...
|
||||
|
||||
@@ -64,3 +64,9 @@ class FakeEnum10(enum.Enum):
|
||||
A = enum.auto()
|
||||
B = enum.auto()
|
||||
C = enum.auto()
|
||||
|
||||
|
||||
class FakeEnum10(enum.Enum):
|
||||
A = ...
|
||||
B = ... # PIE796
|
||||
C = ... # PIE796
|
||||
|
||||
7
crates/ruff_linter/resources/test/fixtures/flake8_pie/PIE796.pyi
vendored
Normal file
7
crates/ruff_linter/resources/test/fixtures/flake8_pie/PIE796.pyi
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
import enum
|
||||
|
||||
|
||||
class FakeEnum1(enum.Enum):
|
||||
A = ...
|
||||
B = ...
|
||||
C = ...
|
||||
@@ -91,3 +91,17 @@ field27 = list[str]
|
||||
field28 = builtins.str
|
||||
field29 = str
|
||||
field30 = str | bytes | None
|
||||
|
||||
# We shouldn't emit Y052 for `enum` subclasses.
|
||||
from enum import Enum
|
||||
|
||||
class Foo(Enum):
|
||||
FOO = 0
|
||||
BAR = 1
|
||||
|
||||
class Bar(Foo):
|
||||
BAZ = 2
|
||||
BOP = 3
|
||||
|
||||
class Bop:
|
||||
WIZ = 4
|
||||
|
||||
@@ -98,3 +98,17 @@ field27 = list[str]
|
||||
field28 = builtins.str
|
||||
field29 = str
|
||||
field30 = str | bytes | None
|
||||
|
||||
# We shouldn't emit Y052 for `enum` subclasses.
|
||||
from enum import Enum
|
||||
|
||||
class Foo(Enum):
|
||||
FOO = 0
|
||||
BAR = 1
|
||||
|
||||
class Bar(Foo):
|
||||
BAZ = 2
|
||||
BOP = 3
|
||||
|
||||
class Bop:
|
||||
WIZ = 4
|
||||
|
||||
@@ -134,3 +134,32 @@ with A() as a:
|
||||
f" something { my_dict["key"] } something else "
|
||||
|
||||
f"foo {f"bar {x}"} baz"
|
||||
|
||||
# Allow cascading for some statements.
|
||||
import anyio
|
||||
import asyncio
|
||||
import trio
|
||||
|
||||
async with asyncio.timeout(1):
|
||||
async with A() as a:
|
||||
pass
|
||||
|
||||
async with A():
|
||||
async with asyncio.timeout(1):
|
||||
pass
|
||||
|
||||
async with asyncio.timeout(1):
|
||||
async with asyncio.timeout_at(1):
|
||||
async with anyio.CancelScope():
|
||||
async with anyio.fail_after(1):
|
||||
async with anyio.move_on_after(1):
|
||||
async with trio.fail_after(1):
|
||||
async with trio.fail_at(1):
|
||||
async with trio.move_on_after(1):
|
||||
async with trio.move_on_at(1):
|
||||
pass
|
||||
|
||||
# Do not supress combination, if a context manager is already combined with another.
|
||||
async with asyncio.timeout(1), A():
|
||||
async with B():
|
||||
pass
|
||||
|
||||
@@ -25,3 +25,11 @@ a = {}.get(key, None)
|
||||
|
||||
# SIM910
|
||||
({}).get(key, None)
|
||||
|
||||
# SIM910
|
||||
ages = {"Tom": 23, "Maria": 23, "Dog": 11}
|
||||
age = ages.get("Cat", None)
|
||||
|
||||
# OK
|
||||
ages = ["Tom", "Maria", "Dog"]
|
||||
age = ages.get("Cat", None)
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence # TCH003
|
||||
from pandas import DataFrame
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class MyBaseClass:
|
||||
pass
|
||||
class Parent(BaseModel):
|
||||
...
|
||||
|
||||
|
||||
class Foo(MyBaseClass):
|
||||
foo: Sequence
|
||||
class Child(Parent):
|
||||
baz: DataFrame
|
||||
|
||||
5
crates/ruff_linter/resources/test/fixtures/isort/from_first.py
vendored
Normal file
5
crates/ruff_linter/resources/test/fixtures/isort/from_first.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
from __future__ import blah
|
||||
|
||||
from os import path
|
||||
|
||||
import os
|
||||
11
crates/ruff_linter/resources/test/fixtures/isort/main_first_party.py
vendored
Normal file
11
crates/ruff_linter/resources/test/fixtures/isort/main_first_party.py
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
import os
|
||||
|
||||
import __main__
|
||||
import third_party
|
||||
|
||||
import first_party
|
||||
|
||||
os.a
|
||||
third_party.a
|
||||
__main__.a
|
||||
first_party.a
|
||||
@@ -25,6 +25,8 @@ def assign():
|
||||
|
||||
IntOrStr: TypeAlias = int | str
|
||||
|
||||
type MyInt = int
|
||||
|
||||
|
||||
def aug_assign(rank, world_size):
|
||||
global CURRENT_PORT
|
||||
|
||||
@@ -50,3 +50,16 @@ import itertools
|
||||
|
||||
for i in itertools.product(foo_int): # Ok
|
||||
pass
|
||||
|
||||
for i in list(foo_list): # Ok
|
||||
foo_list.append(i + 1)
|
||||
|
||||
for i in list(foo_list): # PERF101
|
||||
# Make sure we match the correct list
|
||||
other_list.append(i + 1)
|
||||
|
||||
for i in list(foo_tuple): # Ok
|
||||
foo_tuple.append(i + 1)
|
||||
|
||||
for i in list(foo_set): # Ok
|
||||
foo_set.append(i + 1)
|
||||
|
||||
@@ -135,3 +135,15 @@ ham[lower + offset: :upper + offset]
|
||||
|
||||
#: E203:1:20
|
||||
ham[{lower + offset : upper + offset} : upper + offset]
|
||||
|
||||
#: Okay
|
||||
ham[upper:]
|
||||
|
||||
#: Okay
|
||||
ham[upper :]
|
||||
|
||||
#: E202:1:12
|
||||
ham[upper : ]
|
||||
|
||||
#: E203:1:10
|
||||
ham[upper :]
|
||||
|
||||
94
crates/ruff_linter/resources/test/fixtures/pycodestyle/E703.ipynb
vendored
Normal file
94
crates/ruff_linter/resources/test/fixtures/pycodestyle/E703.ipynb
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "33faf7ad-a3fd-4ac4-a0c3-52e507ed49df",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"x = 1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"id": "481fb4bf-c1b9-47da-927f-3cfdfe4b49ec",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Simple case\n",
|
||||
"x;"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"id": "2f0c65a5-0a0e-4080-afce-5a8ed0d706df",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Only skip the last expression\n",
|
||||
"x; # E703\n",
|
||||
"x;"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"id": "5a3fd75d-26d9-44f7-b013-1684aabfd0ae",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Nested expressions isn't relevant\n",
|
||||
"if True:\n",
|
||||
" x;"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"id": "05eab5b9-e2ba-4954-8ef3-b035a79573fe",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Semicolons with multiple expressions\n",
|
||||
"x; x;"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"id": "9cbbddc5-83fc-4fdb-81ab-53a3912ae898",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Comments, newlines and whitespace\n",
|
||||
"x; # comment\n",
|
||||
"\n",
|
||||
"# another comment"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python (ruff-playground)",
|
||||
"language": "python",
|
||||
"name": "ruff-playground"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
43
crates/ruff_linter/resources/test/fixtures/pydocstyle/D100.ipynb
vendored
Normal file
43
crates/ruff_linter/resources/test/fixtures/pydocstyle/D100.ipynb
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "33faf7ad-a3fd-4ac4-a0c3-52e507ed49df",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Hello world!\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"print(\"Hello world!\")"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python (ruff-playground)",
|
||||
"language": "python",
|
||||
"name": "ruff-playground"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
7
crates/ruff_linter/resources/test/fixtures/pyflakes/F821_23.py
vendored
Normal file
7
crates/ruff_linter/resources/test/fixtures/pyflakes/F821_23.py
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
"""Test for type annotation parsing in `NamedTuple`."""
|
||||
|
||||
import typing
|
||||
|
||||
User = typing.NamedTuple('User', **{'name': str, 'password': bytes})
|
||||
User = typing.NamedTuple('User', name=str, password=bytes)
|
||||
User = typing.NamedTuple('User', [('name', str), ('password', bytes)])
|
||||
@@ -50,7 +50,7 @@ class Apples:
|
||||
return "Docstring"
|
||||
|
||||
# Added in Python 3.12
|
||||
def __buffer__(self):
|
||||
def __buffer__(self):
|
||||
return memoryview(b'')
|
||||
|
||||
def __release_buffer__(self, buf):
|
||||
@@ -83,6 +83,10 @@ class Apples:
|
||||
def _(self):
|
||||
pass
|
||||
|
||||
# Allow custom dunder names (via setting).
|
||||
def __special_custom_magic__(self):
|
||||
pass
|
||||
|
||||
|
||||
def __foo_bar__(): # this is not checked by the [bad-dunder-name] rule
|
||||
...
|
||||
|
||||
20
crates/ruff_linter/resources/test/fixtures/pylint/repeated_keyword_argument.py
vendored
Normal file
20
crates/ruff_linter/resources/test/fixtures/pylint/repeated_keyword_argument.py
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
def func(a=10, b=20, c=30):
|
||||
pass
|
||||
|
||||
# Valid
|
||||
func(a=11, b=21, c=31)
|
||||
func(b=11, a=21, c=31)
|
||||
func(c=11, a=21, b=31)
|
||||
func(a=11, b=31, **{"c": 41})
|
||||
func(a=11, **{"b": 21}, **{"c": 41})
|
||||
func(a=11, **{"b": 21, "c": 41})
|
||||
func(**{"b": 21, "c": 41})
|
||||
func(**{"a": 11}, **{"b": 21}, **{"c": 41})
|
||||
func(**{"a": 11, "b": 21, "c": 41})
|
||||
|
||||
# Invalid
|
||||
func(a=11, c=31, **{"c": 41})
|
||||
func(a=11, c=31, **{"c": 41, "a": 51})
|
||||
func(a=11, b=21, c=31, **{"b": 22, "c": 41, "a": 51})
|
||||
func(a=11, b=21, **{"c": 31}, **{"c": 32})
|
||||
func(a=11, b=21, **{"c": 31, "c": 32})
|
||||
@@ -23,6 +23,9 @@ bar, (foo, baz) = bar, (foo, 1)
|
||||
(foo, (bar, baz)) = (foo, (bar, 1))
|
||||
foo: int = foo
|
||||
bar: int = bar
|
||||
foo = foo = bar
|
||||
(foo, bar) = (foo, bar) = baz
|
||||
(foo, bar) = baz = (foo, bar) = 1
|
||||
|
||||
# Non-errors.
|
||||
foo = bar
|
||||
|
||||
@@ -48,3 +48,14 @@ class E(Struct):
|
||||
without_annotation = []
|
||||
class_variable: ClassVar[list[int]] = []
|
||||
final_variable: Final[list[int]] = []
|
||||
|
||||
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class F(BaseSettings):
|
||||
mutable_default: list[int] = []
|
||||
immutable_annotation: Sequence[int] = []
|
||||
without_annotation = []
|
||||
class_variable: ClassVar[list[int]] = []
|
||||
final_variable: Final[list[int]] = []
|
||||
|
||||
@@ -109,3 +109,13 @@ def fine():
|
||||
a = 1
|
||||
except Exception:
|
||||
error("Context message here", exc_info=sys.exc_info())
|
||||
|
||||
|
||||
def nested():
|
||||
try:
|
||||
a = 1
|
||||
except Exception:
|
||||
try:
|
||||
b = 2
|
||||
except Exception:
|
||||
error("Context message here")
|
||||
|
||||
@@ -126,3 +126,25 @@ def main_function():
|
||||
except Exception as ex:
|
||||
exception(f"Found an error: {er}")
|
||||
exception(f"Found an error: {ex.field}")
|
||||
|
||||
|
||||
def nested():
|
||||
try:
|
||||
process()
|
||||
handle()
|
||||
except Exception as ex:
|
||||
try:
|
||||
finish()
|
||||
except Exception as ex:
|
||||
logger.exception(f"Found an error: {ex}") # TRY401
|
||||
|
||||
|
||||
def nested():
|
||||
try:
|
||||
process()
|
||||
handle()
|
||||
except Exception as ex:
|
||||
try:
|
||||
finish()
|
||||
except Exception:
|
||||
logger.exception(f"Found an error: {ex}") # TRY401
|
||||
|
||||
@@ -158,21 +158,23 @@ pub(crate) fn definitions(checker: &mut Checker) {
|
||||
}
|
||||
|
||||
// Extract a `Docstring` from a `Definition`.
|
||||
let Some(expr) = docstring else {
|
||||
let Some(string_literal) = docstring else {
|
||||
pydocstyle::rules::not_missing(checker, definition, *visibility);
|
||||
continue;
|
||||
};
|
||||
|
||||
let contents = checker.locator().slice(expr);
|
||||
let contents = checker.locator().slice(string_literal);
|
||||
|
||||
let indentation = checker.locator().slice(TextRange::new(
|
||||
checker.locator.line_start(expr.start()),
|
||||
expr.start(),
|
||||
checker.locator.line_start(string_literal.start()),
|
||||
string_literal.start(),
|
||||
));
|
||||
|
||||
if expr.implicit_concatenated {
|
||||
if string_literal.value.is_implicit_concatenated() {
|
||||
#[allow(deprecated)]
|
||||
let location = checker.locator.compute_source_location(expr.start());
|
||||
let location = checker
|
||||
.locator
|
||||
.compute_source_location(string_literal.start());
|
||||
warn_user!(
|
||||
"Docstring at {}:{}:{} contains implicit string concatenation; ignoring...",
|
||||
relativize_path(checker.path),
|
||||
@@ -186,7 +188,7 @@ pub(crate) fn definitions(checker: &mut Checker) {
|
||||
let body_range = raw_contents_range(contents).unwrap();
|
||||
let docstring = Docstring {
|
||||
definition,
|
||||
expr,
|
||||
expr: string_literal,
|
||||
contents,
|
||||
body_range,
|
||||
indentation,
|
||||
|
||||
@@ -369,18 +369,24 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
]) {
|
||||
if let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = func.as_ref() {
|
||||
let attr = attr.as_str();
|
||||
if let Expr::StringLiteral(ast::ExprStringLiteral { value: string, .. }) =
|
||||
value.as_ref()
|
||||
if let Expr::StringLiteral(ast::ExprStringLiteral {
|
||||
value: string_value,
|
||||
..
|
||||
}) = value.as_ref()
|
||||
{
|
||||
if attr == "join" {
|
||||
// "...".join(...) call
|
||||
if checker.enabled(Rule::StaticJoinToFString) {
|
||||
flynt::rules::static_join_to_fstring(checker, expr, string);
|
||||
flynt::rules::static_join_to_fstring(
|
||||
checker,
|
||||
expr,
|
||||
string_value.as_str(),
|
||||
);
|
||||
}
|
||||
} else if attr == "format" {
|
||||
// "...".format(...) call
|
||||
let location = expr.range();
|
||||
match pyflakes::format::FormatSummary::try_from(string.as_ref()) {
|
||||
match pyflakes::format::FormatSummary::try_from(string_value.as_str()) {
|
||||
Err(e) => {
|
||||
if checker.enabled(Rule::StringDotFormatInvalidFormat) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
@@ -425,7 +431,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
|
||||
if checker.enabled(Rule::BadStringFormatCharacter) {
|
||||
pylint::rules::bad_string_format_character::call(
|
||||
checker, string, location,
|
||||
checker,
|
||||
string_value.as_str(),
|
||||
location,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -612,6 +620,12 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
]) {
|
||||
flake8_bandit::rules::shell_injection(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::DjangoRawSql) {
|
||||
flake8_bandit::rules::django_raw_sql(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::TarfileUnsafeMembers) {
|
||||
flake8_bandit::rules::tarfile_unsafe_members(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::UnnecessaryGeneratorList) {
|
||||
flake8_comprehensions::rules::unnecessary_generator_list(
|
||||
checker, expr, func, args, keywords,
|
||||
@@ -776,6 +790,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::NestedMinMax) {
|
||||
pylint::rules::nested_min_max(checker, expr, func, args, keywords);
|
||||
}
|
||||
if checker.enabled(Rule::RepeatedKeywordArgument) {
|
||||
pylint::rules::repeated_keyword_argument(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::PytestPatchWithLambda) {
|
||||
if let Some(diagnostic) = flake8_pytest_style::rules::patch_with_lambda(call) {
|
||||
checker.diagnostics.push(diagnostic);
|
||||
@@ -979,15 +996,22 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
pylint::rules::await_outside_async(checker, expr);
|
||||
}
|
||||
}
|
||||
Expr::FString(fstring @ ast::ExprFString { values, .. }) => {
|
||||
Expr::FString(f_string_expr @ ast::ExprFString { value, .. }) => {
|
||||
if checker.enabled(Rule::FStringMissingPlaceholders) {
|
||||
pyflakes::rules::f_string_missing_placeholders(fstring, checker);
|
||||
pyflakes::rules::f_string_missing_placeholders(checker, f_string_expr);
|
||||
}
|
||||
if checker.enabled(Rule::ExplicitFStringTypeConversion) {
|
||||
for f_string in value.f_strings() {
|
||||
ruff::rules::explicit_f_string_type_conversion(checker, f_string);
|
||||
}
|
||||
}
|
||||
if checker.enabled(Rule::HardcodedSQLExpression) {
|
||||
flake8_bandit::rules::hardcoded_sql_expression(checker, expr);
|
||||
}
|
||||
if checker.enabled(Rule::ExplicitFStringTypeConversion) {
|
||||
ruff::rules::explicit_f_string_type_conversion(checker, expr, values);
|
||||
if checker.enabled(Rule::UnicodeKindPrefix) {
|
||||
for string_literal in value.literals() {
|
||||
pyupgrade::rules::unicode_kind_prefix(checker, string_literal);
|
||||
}
|
||||
}
|
||||
}
|
||||
Expr::BinOp(ast::ExprBinOp {
|
||||
@@ -1269,6 +1293,11 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::HardcodedTempFile) {
|
||||
flake8_bandit::rules::hardcoded_tmp_directory(checker, string);
|
||||
}
|
||||
if checker.enabled(Rule::UnicodeKindPrefix) {
|
||||
for string_part in string.value.parts() {
|
||||
pyupgrade::rules::unicode_kind_prefix(checker, string_part);
|
||||
}
|
||||
}
|
||||
if checker.source_type.is_stub() {
|
||||
if checker.enabled(Rule::StringOrBytesTooLong) {
|
||||
flake8_pyi::rules::string_or_bytes_too_long(checker, expr);
|
||||
|
||||
@@ -1269,7 +1269,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
perflint::rules::manual_dict_comprehension(checker, target, body);
|
||||
}
|
||||
if checker.enabled(Rule::UnnecessaryListCast) {
|
||||
perflint::rules::unnecessary_list_cast(checker, iter);
|
||||
perflint::rules::unnecessary_list_cast(checker, iter, body);
|
||||
}
|
||||
if !is_async {
|
||||
if checker.enabled(Rule::ReimplementedBuiltin) {
|
||||
@@ -1355,7 +1355,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
}
|
||||
}
|
||||
Stmt::Assign(assign @ ast::StmtAssign { targets, value, .. }) => {
|
||||
checker.enabled(Rule::NonAsciiName);
|
||||
if checker.enabled(Rule::LambdaAssignment) {
|
||||
if let [target] = &targets[..] {
|
||||
pycodestyle::rules::lambda_assignment(checker, target, value, None, stmt);
|
||||
@@ -1407,9 +1406,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
}
|
||||
}
|
||||
if checker.settings.rules.enabled(Rule::SelfAssigningVariable) {
|
||||
if let [target] = targets.as_slice() {
|
||||
pylint::rules::self_assigning_variable(checker, target, value);
|
||||
}
|
||||
pylint::rules::self_assignment(checker, assign);
|
||||
}
|
||||
if checker.settings.rules.enabled(Rule::TypeParamNameMismatch) {
|
||||
pylint::rules::type_param_name_mismatch(checker, value, targets);
|
||||
@@ -1479,9 +1476,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
stmt,
|
||||
);
|
||||
}
|
||||
if checker.enabled(Rule::SelfAssigningVariable) {
|
||||
pylint::rules::self_assigning_variable(checker, target, value);
|
||||
}
|
||||
}
|
||||
if checker.enabled(Rule::SelfAssigningVariable) {
|
||||
pylint::rules::self_annotated_assignment(checker, assign_stmt);
|
||||
}
|
||||
if checker.enabled(Rule::UnintentionalTypeAnnotation) {
|
||||
flake8_bugbear::rules::unintentional_type_annotation(
|
||||
|
||||
@@ -37,6 +37,7 @@ use ruff_python_ast::{
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, IsolationLevel};
|
||||
use ruff_notebook::CellOffsets;
|
||||
use ruff_python_ast::all::{extract_all_names, DunderAllFlags};
|
||||
use ruff_python_ast::helpers::{
|
||||
collect_import_from_member, extract_handled_exceptions, to_module_path,
|
||||
@@ -78,6 +79,8 @@ pub(crate) struct Checker<'a> {
|
||||
module_path: Option<&'a [String]>,
|
||||
/// The [`PySourceType`] of the current file.
|
||||
pub(crate) source_type: PySourceType,
|
||||
/// The [`CellOffsets`] for the current file, if it's a Jupyter notebook.
|
||||
cell_offsets: Option<&'a CellOffsets>,
|
||||
/// The [`flags::Noqa`] for the current analysis (i.e., whether to respect suppression
|
||||
/// comments).
|
||||
noqa: flags::Noqa,
|
||||
@@ -120,6 +123,7 @@ impl<'a> Checker<'a> {
|
||||
indexer: &'a Indexer,
|
||||
importer: Importer<'a>,
|
||||
source_type: PySourceType,
|
||||
cell_offsets: Option<&'a CellOffsets>,
|
||||
) -> Checker<'a> {
|
||||
Checker {
|
||||
settings,
|
||||
@@ -137,6 +141,7 @@ impl<'a> Checker<'a> {
|
||||
deferred: Deferred::default(),
|
||||
diagnostics: Vec::default(),
|
||||
flake8_bugbear_seen: Vec::default(),
|
||||
cell_offsets,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -225,6 +230,11 @@ impl<'a> Checker<'a> {
|
||||
self.package
|
||||
}
|
||||
|
||||
/// The [`CellOffsets`] for the current file, if it's a Jupyter notebook.
|
||||
pub(crate) const fn cell_offsets(&self) -> Option<&'a CellOffsets> {
|
||||
self.cell_offsets
|
||||
}
|
||||
|
||||
/// Returns whether the given rule should be checked.
|
||||
#[inline]
|
||||
pub(crate) const fn enabled(&self, rule: Rule) -> bool {
|
||||
@@ -789,7 +799,7 @@ where
|
||||
if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = expr {
|
||||
self.deferred.string_type_definitions.push((
|
||||
expr.range(),
|
||||
value,
|
||||
value.as_str(),
|
||||
self.semantic.snapshot(),
|
||||
));
|
||||
} else {
|
||||
@@ -1014,33 +1024,54 @@ where
|
||||
if let Some(arg) = args.next() {
|
||||
self.visit_non_type_definition(arg);
|
||||
}
|
||||
|
||||
for arg in args {
|
||||
if let Expr::List(ast::ExprList { elts, .. })
|
||||
| Expr::Tuple(ast::ExprTuple { elts, .. }) = arg
|
||||
{
|
||||
for elt in elts {
|
||||
match elt {
|
||||
Expr::List(ast::ExprList { elts, .. })
|
||||
| Expr::Tuple(ast::ExprTuple { elts, .. })
|
||||
if elts.len() == 2 =>
|
||||
{
|
||||
self.visit_non_type_definition(&elts[0]);
|
||||
self.visit_type_definition(&elts[1]);
|
||||
}
|
||||
_ => {
|
||||
self.visit_non_type_definition(elt);
|
||||
match arg {
|
||||
// Ex) NamedTuple("a", [("a", int)])
|
||||
Expr::List(ast::ExprList { elts, .. })
|
||||
| Expr::Tuple(ast::ExprTuple { elts, .. }) => {
|
||||
for elt in elts {
|
||||
match elt {
|
||||
Expr::List(ast::ExprList { elts, .. })
|
||||
| Expr::Tuple(ast::ExprTuple { elts, .. })
|
||||
if elts.len() == 2 =>
|
||||
{
|
||||
self.visit_non_type_definition(&elts[0]);
|
||||
self.visit_type_definition(&elts[1]);
|
||||
}
|
||||
_ => {
|
||||
self.visit_non_type_definition(elt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.visit_non_type_definition(arg);
|
||||
_ => self.visit_non_type_definition(arg),
|
||||
}
|
||||
}
|
||||
|
||||
// Ex) NamedTuple("a", a=int)
|
||||
for keyword in keywords {
|
||||
let Keyword { value, .. } = keyword;
|
||||
self.visit_type_definition(value);
|
||||
let Keyword { arg, value, .. } = keyword;
|
||||
match (arg.as_ref(), value) {
|
||||
// Ex) NamedTuple("a", **{"a": int})
|
||||
(None, Expr::Dict(ast::ExprDict { keys, values, .. })) => {
|
||||
for (key, value) in keys.iter().zip(values) {
|
||||
if let Some(key) = key.as_ref() {
|
||||
self.visit_non_type_definition(key);
|
||||
self.visit_type_definition(value);
|
||||
} else {
|
||||
self.visit_non_type_definition(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Ex) NamedTuple("a", **obj)
|
||||
(None, _) => {
|
||||
self.visit_non_type_definition(value);
|
||||
}
|
||||
// Ex) NamedTuple("a", a=int)
|
||||
_ => {
|
||||
self.visit_type_definition(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(typing::Callable::TypedDict) => {
|
||||
@@ -1188,7 +1219,7 @@ where
|
||||
{
|
||||
self.deferred.string_type_definitions.push((
|
||||
expr.range(),
|
||||
value,
|
||||
value.as_str(),
|
||||
self.semantic.snapshot(),
|
||||
));
|
||||
}
|
||||
@@ -1272,9 +1303,9 @@ where
|
||||
|
||||
fn visit_format_spec(&mut self, format_spec: &'b Expr) {
|
||||
match format_spec {
|
||||
Expr::FString(ast::ExprFString { values, .. }) => {
|
||||
for value in values {
|
||||
self.visit_expr(value);
|
||||
Expr::FString(ast::ExprFString { value, .. }) => {
|
||||
for expr in value.elements() {
|
||||
self.visit_expr(expr);
|
||||
}
|
||||
}
|
||||
_ => unreachable!("Unexpected expression for format_spec"),
|
||||
@@ -1921,6 +1952,7 @@ pub(crate) fn check_ast(
|
||||
path: &Path,
|
||||
package: Option<&Path>,
|
||||
source_type: PySourceType,
|
||||
cell_offsets: Option<&CellOffsets>,
|
||||
) -> Vec<Diagnostic> {
|
||||
let module_path = package.and_then(|package| to_module_path(package, path));
|
||||
let module = Module {
|
||||
@@ -1949,6 +1981,7 @@ pub(crate) fn check_ast(
|
||||
indexer,
|
||||
Importer::new(python_ast, locator, stylist),
|
||||
source_type,
|
||||
cell_offsets,
|
||||
);
|
||||
checker.bind_builtins();
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::borrow::Cow;
|
||||
use std::path::Path;
|
||||
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_notebook::CellOffsets;
|
||||
use ruff_python_ast::helpers::to_module_path;
|
||||
use ruff_python_ast::imports::{ImportMap, ModuleImport};
|
||||
use ruff_python_ast::statement_visitor::StatementVisitor;
|
||||
@@ -17,7 +18,6 @@ use crate::registry::Rule;
|
||||
use crate::rules::isort;
|
||||
use crate::rules::isort::block::{Block, BlockBuilder};
|
||||
use crate::settings::LinterSettings;
|
||||
use crate::source_kind::SourceKind;
|
||||
|
||||
fn extract_import_map(path: &Path, package: Option<&Path>, blocks: &[&Block]) -> Option<ImportMap> {
|
||||
let Some(package) = package else {
|
||||
@@ -85,13 +85,13 @@ pub(crate) fn check_imports(
|
||||
stylist: &Stylist,
|
||||
path: &Path,
|
||||
package: Option<&Path>,
|
||||
source_kind: &SourceKind,
|
||||
source_type: PySourceType,
|
||||
cell_offsets: Option<&CellOffsets>,
|
||||
) -> (Vec<Diagnostic>, Option<ImportMap>) {
|
||||
// Extract all import blocks from the AST.
|
||||
let tracker = {
|
||||
let mut tracker =
|
||||
BlockBuilder::new(locator, directives, source_type.is_stub(), source_kind);
|
||||
BlockBuilder::new(locator, directives, source_type.is_stub(), cell_offsets);
|
||||
tracker.visit_body(python_ast);
|
||||
tracker
|
||||
};
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use ruff_notebook::CellOffsets;
|
||||
use ruff_python_ast::PySourceType;
|
||||
use ruff_python_parser::lexer::LexResult;
|
||||
use ruff_python_parser::Tok;
|
||||
|
||||
@@ -25,7 +27,8 @@ pub(crate) fn check_tokens(
|
||||
locator: &Locator,
|
||||
indexer: &Indexer,
|
||||
settings: &LinterSettings,
|
||||
is_stub: bool,
|
||||
source_type: PySourceType,
|
||||
cell_offsets: Option<&CellOffsets>,
|
||||
) -> Vec<Diagnostic> {
|
||||
let mut diagnostics: Vec<Diagnostic> = vec![];
|
||||
|
||||
@@ -91,10 +94,6 @@ pub(crate) fn check_tokens(
|
||||
pycodestyle::rules::tab_indentation(&mut diagnostics, tokens, locator, indexer);
|
||||
}
|
||||
|
||||
if settings.rules.enabled(Rule::UnicodeKindPrefix) {
|
||||
pyupgrade::rules::unicode_kind_prefix(&mut diagnostics, tokens);
|
||||
}
|
||||
|
||||
if settings.rules.any_enabled(&[
|
||||
Rule::InvalidCharacterBackspace,
|
||||
Rule::InvalidCharacterSub,
|
||||
@@ -112,7 +111,14 @@ pub(crate) fn check_tokens(
|
||||
Rule::MultipleStatementsOnOneLineSemicolon,
|
||||
Rule::UselessSemicolon,
|
||||
]) {
|
||||
pycodestyle::rules::compound_statements(&mut diagnostics, tokens, locator, indexer);
|
||||
pycodestyle::rules::compound_statements(
|
||||
&mut diagnostics,
|
||||
tokens,
|
||||
locator,
|
||||
indexer,
|
||||
source_type,
|
||||
cell_offsets,
|
||||
);
|
||||
}
|
||||
|
||||
if settings.rules.enabled(Rule::AvoidableEscapedQuote) && settings.flake8_quotes.avoid_escape {
|
||||
@@ -156,7 +162,7 @@ pub(crate) fn check_tokens(
|
||||
pyupgrade::rules::extraneous_parentheses(&mut diagnostics, tokens, locator);
|
||||
}
|
||||
|
||||
if is_stub && settings.rules.enabled(Rule::TypeCommentInStub) {
|
||||
if source_type.is_stub() && settings.rules.enabled(Rule::TypeCommentInStub) {
|
||||
flake8_pyi::rules::type_comment_in_stub(&mut diagnostics, locator, indexer);
|
||||
}
|
||||
|
||||
|
||||
@@ -228,6 +228,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Pylint, "E0604") => (RuleGroup::Stable, rules::pylint::rules::InvalidAllObject),
|
||||
(Pylint, "E0605") => (RuleGroup::Stable, rules::pylint::rules::InvalidAllFormat),
|
||||
(Pylint, "E0704") => (RuleGroup::Preview, rules::pylint::rules::MisplacedBareRaise),
|
||||
(Pylint, "E1132") => (RuleGroup::Preview, rules::pylint::rules::RepeatedKeywordArgument),
|
||||
(Pylint, "E1142") => (RuleGroup::Stable, rules::pylint::rules::AwaitOutsideAsync),
|
||||
(Pylint, "E1205") => (RuleGroup::Stable, rules::pylint::rules::LoggingTooManyArgs),
|
||||
(Pylint, "E1206") => (RuleGroup::Stable, rules::pylint::rules::LoggingTooFewArgs),
|
||||
@@ -594,6 +595,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8Bandit, "112") => (RuleGroup::Stable, rules::flake8_bandit::rules::TryExceptContinue),
|
||||
(Flake8Bandit, "113") => (RuleGroup::Stable, rules::flake8_bandit::rules::RequestWithoutTimeout),
|
||||
(Flake8Bandit, "201") => (RuleGroup::Preview, rules::flake8_bandit::rules::FlaskDebugTrue),
|
||||
(Flake8Bandit, "202") => (RuleGroup::Preview, rules::flake8_bandit::rules::TarfileUnsafeMembers),
|
||||
(Flake8Bandit, "301") => (RuleGroup::Stable, rules::flake8_bandit::rules::SuspiciousPickleUsage),
|
||||
(Flake8Bandit, "302") => (RuleGroup::Stable, rules::flake8_bandit::rules::SuspiciousMarshalUsage),
|
||||
(Flake8Bandit, "303") => (RuleGroup::Stable, rules::flake8_bandit::rules::SuspiciousInsecureHashUsage),
|
||||
@@ -631,6 +633,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8Bandit, "607") => (RuleGroup::Stable, rules::flake8_bandit::rules::StartProcessWithPartialPath),
|
||||
(Flake8Bandit, "608") => (RuleGroup::Stable, rules::flake8_bandit::rules::HardcodedSQLExpression),
|
||||
(Flake8Bandit, "609") => (RuleGroup::Stable, rules::flake8_bandit::rules::UnixCommandWildcardInjection),
|
||||
(Flake8Bandit, "611") => (RuleGroup::Preview, rules::flake8_bandit::rules::DjangoRawSql),
|
||||
(Flake8Bandit, "612") => (RuleGroup::Stable, rules::flake8_bandit::rules::LoggingConfigInsecureListen),
|
||||
(Flake8Bandit, "701") => (RuleGroup::Stable, rules::flake8_bandit::rules::Jinja2AutoescapeFalse),
|
||||
(Flake8Bandit, "702") => (RuleGroup::Preview, rules::flake8_bandit::rules::MakoTemplates),
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use crate::fix::codemods::CodegenStylist;
|
||||
use anyhow::{bail, Result};
|
||||
use libcst_native::{
|
||||
Arg, Attribute, Call, Comparison, CompoundStatement, Dict, Expression, FunctionDef,
|
||||
GeneratorExp, If, Import, ImportAlias, ImportFrom, ImportNames, IndentedBlock, Lambda,
|
||||
ListComp, Module, Name, SmallStatement, Statement, Suite, Tuple, With,
|
||||
Arg, Attribute, Call, Comparison, CompoundStatement, Dict, Expression, FormattedString,
|
||||
FormattedStringContent, FormattedStringExpression, FunctionDef, GeneratorExp, If, Import,
|
||||
ImportAlias, ImportFrom, ImportNames, IndentedBlock, Lambda, ListComp, Module, Name,
|
||||
SmallStatement, Statement, Suite, Tuple, With,
|
||||
};
|
||||
use ruff_python_codegen::Stylist;
|
||||
|
||||
@@ -153,6 +154,28 @@ pub(crate) fn match_lambda<'a, 'b>(expression: &'a Expression<'b>) -> Result<&'a
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn match_formatted_string<'a, 'b>(
|
||||
expression: &'a mut Expression<'b>,
|
||||
) -> Result<&'a mut FormattedString<'b>> {
|
||||
if let Expression::FormattedString(formatted_string) = expression {
|
||||
Ok(formatted_string)
|
||||
} else {
|
||||
bail!("Expected Expression::FormattedString");
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn match_formatted_string_expression<'a, 'b>(
|
||||
formatted_string_content: &'a mut FormattedStringContent<'b>,
|
||||
) -> Result<&'a mut FormattedStringExpression<'b>> {
|
||||
if let FormattedStringContent::Expression(formatted_string_expression) =
|
||||
formatted_string_content
|
||||
{
|
||||
Ok(formatted_string_expression)
|
||||
} else {
|
||||
bail!("Expected FormattedStringContent::Expression")
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn match_function_def<'a, 'b>(
|
||||
statement: &'a mut Statement<'b>,
|
||||
) -> Result<&'a mut FunctionDef<'b>> {
|
||||
|
||||
@@ -9,6 +9,7 @@ use log::error;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_notebook::Notebook;
|
||||
use ruff_python_ast::imports::ImportMap;
|
||||
use ruff_python_ast::PySourceType;
|
||||
use ruff_python_codegen::Stylist;
|
||||
@@ -107,7 +108,8 @@ pub fn check_path(
|
||||
locator,
|
||||
indexer,
|
||||
settings,
|
||||
source_type.is_stub(),
|
||||
source_type,
|
||||
source_kind.as_ipy_notebook().map(Notebook::cell_offsets),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -149,6 +151,7 @@ pub fn check_path(
|
||||
source_type.is_ipynb(),
|
||||
) {
|
||||
Ok(python_ast) => {
|
||||
let cell_offsets = source_kind.as_ipy_notebook().map(Notebook::cell_offsets);
|
||||
if use_ast {
|
||||
diagnostics.extend(check_ast(
|
||||
&python_ast,
|
||||
@@ -161,6 +164,7 @@ pub fn check_path(
|
||||
path,
|
||||
package,
|
||||
source_type,
|
||||
cell_offsets,
|
||||
));
|
||||
}
|
||||
if use_imports {
|
||||
@@ -173,8 +177,8 @@ pub fn check_path(
|
||||
stylist,
|
||||
path,
|
||||
package,
|
||||
source_kind,
|
||||
source_type,
|
||||
cell_offsets,
|
||||
);
|
||||
imports = module_imports;
|
||||
diagnostics.extend(import_diagnostics);
|
||||
|
||||
@@ -297,7 +297,6 @@ impl Rule {
|
||||
| Rule::TabIndentation
|
||||
| Rule::TrailingCommaOnBareTuple
|
||||
| Rule::TypeCommentInStub
|
||||
| Rule::UnicodeKindPrefix
|
||||
| Rule::UselessSemicolon
|
||||
| Rule::UTF8EncodingDeclaration => LintSource::Tokens,
|
||||
Rule::IOError => LintSource::Io,
|
||||
|
||||
@@ -81,7 +81,7 @@ pub(crate) fn variable_name_task_id(
|
||||
let ast::ExprStringLiteral { value: task_id, .. } = keyword.value.as_string_literal_expr()?;
|
||||
|
||||
// If the target name is the same as the task_id, no violation.
|
||||
if id == task_id {
|
||||
if task_id == id {
|
||||
return None;
|
||||
}
|
||||
|
||||
|
||||
@@ -505,15 +505,10 @@ fn check_dynamically_typed<F>(
|
||||
) where
|
||||
F: FnOnce() -> String,
|
||||
{
|
||||
if let Expr::StringLiteral(ast::ExprStringLiteral {
|
||||
range,
|
||||
value: string,
|
||||
..
|
||||
}) = annotation
|
||||
{
|
||||
if let Expr::StringLiteral(ast::ExprStringLiteral { range, value }) = annotation {
|
||||
// Quoted annotations
|
||||
if let Ok((parsed_annotation, _)) =
|
||||
parse_type_annotation(string, *range, checker.locator().contents())
|
||||
parse_type_annotation(value.as_str(), *range, checker.locator().contents())
|
||||
{
|
||||
if type_hint_resolves_to_any(
|
||||
&parsed_annotation,
|
||||
|
||||
@@ -10,7 +10,7 @@ static PASSWORD_CANDIDATE_REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||
|
||||
pub(super) fn string_literal(expr: &Expr) -> Option<&str> {
|
||||
match expr {
|
||||
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => Some(value),
|
||||
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => Some(value.as_str()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,8 @@ mod tests {
|
||||
#[test_case(Rule::UnixCommandWildcardInjection, Path::new("S609.py"))]
|
||||
#[test_case(Rule::UnsafeYAMLLoad, Path::new("S506.py"))]
|
||||
#[test_case(Rule::WeakCryptographicKey, Path::new("S505.py"))]
|
||||
#[test_case(Rule::DjangoRawSql, Path::new("S611.py"))]
|
||||
#[test_case(Rule::TarfileUnsafeMembers, Path::new("S202.py"))]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of Django's `RawSQL` function.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Django's `RawSQL` function can be used to execute arbitrary SQL queries,
|
||||
/// which can in turn lead to SQL injection vulnerabilities.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// from django.db.models.expressions import RawSQL
|
||||
/// from django.contrib.auth.models import User
|
||||
///
|
||||
/// User.objects.annotate(val=("%secure" % "nos", []))
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Django documentation: SQL injection protection](https://docs.djangoproject.com/en/dev/topics/security/#sql-injection-protection)
|
||||
/// - [Common Weakness Enumeration: CWE-89](https://cwe.mitre.org/data/definitions/89.html)
|
||||
#[violation]
|
||||
pub struct DjangoRawSql;
|
||||
|
||||
impl Violation for DjangoRawSql {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Use of `RawSQL` can lead to SQL injection vulnerabilities")
|
||||
}
|
||||
}
|
||||
|
||||
/// S611
|
||||
pub(crate) fn django_raw_sql(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
if checker
|
||||
.semantic()
|
||||
.resolve_call_path(&call.func)
|
||||
.is_some_and(|call_path| {
|
||||
matches!(
|
||||
call_path.as_slice(),
|
||||
["django", "db", "models", "expressions", "RawSQL"]
|
||||
)
|
||||
})
|
||||
{
|
||||
if !call
|
||||
.arguments
|
||||
.find_argument("sql", 0)
|
||||
.is_some_and(Expr::is_string_literal_expr)
|
||||
{
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(DjangoRawSql, call.func.range()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ impl Violation for HardcodedBindAllInterfaces {
|
||||
|
||||
/// S104
|
||||
pub(crate) fn hardcoded_bind_all_interfaces(string: &ExprStringLiteral) -> Option<Diagnostic> {
|
||||
if string.value == "0.0.0.0" {
|
||||
if string.value.as_str() == "0.0.0.0" {
|
||||
Some(Diagnostic::new(HardcodedBindAllInterfaces, string.range))
|
||||
} else {
|
||||
None
|
||||
|
||||
@@ -55,7 +55,7 @@ fn password_target(target: &Expr) -> Option<&str> {
|
||||
Expr::Name(ast::ExprName { id, .. }) => id.as_str(),
|
||||
// d["password"] = "s3cr3t"
|
||||
Expr::Subscript(ast::ExprSubscript { slice, .. }) => match slice.as_ref() {
|
||||
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => value,
|
||||
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => value.as_str(),
|
||||
_ => return None,
|
||||
},
|
||||
// obj.password = "s3cr3t"
|
||||
|
||||
@@ -5,13 +5,12 @@ use ruff_python_ast::{self as ast, Expr, Operator};
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::any_over_expr;
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_python_ast::str::raw_contents;
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
use super::super::helpers::string_literal;
|
||||
|
||||
static SQL_REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(r"(?i)\b(select\s.+\sfrom\s|delete\s+from\s|(insert|replace)\s.+\svalues\s|update\s.+\sset\s)")
|
||||
.unwrap()
|
||||
@@ -46,53 +45,77 @@ impl Violation for HardcodedSQLExpression {
|
||||
}
|
||||
}
|
||||
|
||||
fn has_string_literal(expr: &Expr) -> bool {
|
||||
string_literal(expr).is_some()
|
||||
}
|
||||
|
||||
fn matches_sql_statement(string: &str) -> bool {
|
||||
SQL_REGEX.is_match(string)
|
||||
}
|
||||
|
||||
fn matches_string_format_expression(expr: &Expr, semantic: &SemanticModel) -> bool {
|
||||
match expr {
|
||||
// "select * from table where val = " + "str" + ...
|
||||
// "select * from table where val = %s" % ...
|
||||
Expr::BinOp(ast::ExprBinOp {
|
||||
op: Operator::Add | Operator::Mod,
|
||||
..
|
||||
}) => {
|
||||
// Only evaluate the full BinOp, not the nested components.
|
||||
if semantic
|
||||
.current_expression_parent()
|
||||
.map_or(true, |parent| !parent.is_bin_op_expr())
|
||||
{
|
||||
if any_over_expr(expr, &has_string_literal) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
Expr::Call(ast::ExprCall { func, .. }) => {
|
||||
let Expr::Attribute(ast::ExprAttribute { attr, value, .. }) = func.as_ref() else {
|
||||
return false;
|
||||
};
|
||||
// "select * from table where val = {}".format(...)
|
||||
attr == "format" && string_literal(value).is_some()
|
||||
}
|
||||
// f"select * from table where val = {val}"
|
||||
Expr::FString(_) => true,
|
||||
_ => false,
|
||||
}
|
||||
/// Concatenates the contents of an f-string, without the prefix and quotes,
|
||||
/// and escapes any special characters.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// "foo" f"bar {x}" "baz"
|
||||
/// ```
|
||||
///
|
||||
/// becomes `foobar {x}baz`.
|
||||
fn concatenated_f_string(expr: &ast::ExprFString, locator: &Locator) -> String {
|
||||
expr.value
|
||||
.parts()
|
||||
.filter_map(|part| {
|
||||
raw_contents(locator.slice(part)).map(|s| s.escape_default().to_string())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// S608
|
||||
pub(crate) fn hardcoded_sql_expression(checker: &mut Checker, expr: &Expr) {
|
||||
if matches_string_format_expression(expr, checker.semantic()) {
|
||||
if matches_sql_statement(&checker.generator().expr(expr)) {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(HardcodedSQLExpression, expr.range()));
|
||||
let content = match expr {
|
||||
// "select * from table where val = " + "str" + ...
|
||||
Expr::BinOp(ast::ExprBinOp {
|
||||
op: Operator::Add, ..
|
||||
}) => {
|
||||
// Only evaluate the full BinOp, not the nested components.
|
||||
if !checker
|
||||
.semantic()
|
||||
.current_expression_parent()
|
||||
.map_or(true, |parent| !parent.is_bin_op_expr())
|
||||
{
|
||||
return;
|
||||
}
|
||||
if !any_over_expr(expr, &Expr::is_string_literal_expr) {
|
||||
return;
|
||||
}
|
||||
checker.generator().expr(expr)
|
||||
}
|
||||
// "select * from table where val = %s" % ...
|
||||
Expr::BinOp(ast::ExprBinOp {
|
||||
left,
|
||||
op: Operator::Mod,
|
||||
..
|
||||
}) => {
|
||||
let Some(string) = left.as_string_literal_expr() else {
|
||||
return;
|
||||
};
|
||||
string.value.as_str().escape_default().to_string()
|
||||
}
|
||||
Expr::Call(ast::ExprCall { func, .. }) => {
|
||||
let Expr::Attribute(ast::ExprAttribute { attr, value, .. }) = func.as_ref() else {
|
||||
return;
|
||||
};
|
||||
// "select * from table where val = {}".format(...)
|
||||
if attr != "format" {
|
||||
return;
|
||||
}
|
||||
let Some(string) = value.as_string_literal_expr() else {
|
||||
return;
|
||||
};
|
||||
string.value.as_str().escape_default().to_string()
|
||||
}
|
||||
// f"select * from table where val = {val}"
|
||||
Expr::FString(f_string) => concatenated_f_string(f_string, checker.locator()),
|
||||
_ => return,
|
||||
};
|
||||
|
||||
if SQL_REGEX.is_match(&content) {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(HardcodedSQLExpression, expr.range()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ pub(crate) fn hardcoded_tmp_directory(checker: &mut Checker, string: &ast::ExprS
|
||||
.flake8_bandit
|
||||
.hardcoded_tmp_directory
|
||||
.iter()
|
||||
.any(|prefix| string.value.starts_with(prefix))
|
||||
.any(|prefix| string.value.as_str().starts_with(prefix))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -76,7 +76,7 @@ pub(crate) fn hardcoded_tmp_directory(checker: &mut Checker, string: &ast::ExprS
|
||||
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
HardcodedTempFile {
|
||||
string: string.value.clone(),
|
||||
string: string.value.to_string(),
|
||||
},
|
||||
string.range,
|
||||
));
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pub(crate) use assert_used::*;
|
||||
pub(crate) use bad_file_permissions::*;
|
||||
pub(crate) use django_raw_sql::*;
|
||||
pub(crate) use exec_used::*;
|
||||
pub(crate) use flask_debug_true::*;
|
||||
pub(crate) use hardcoded_bind_all_interfaces::*;
|
||||
@@ -20,6 +21,7 @@ pub(crate) use snmp_insecure_version::*;
|
||||
pub(crate) use snmp_weak_cryptography::*;
|
||||
pub(crate) use ssh_no_host_key_verification::*;
|
||||
pub(crate) use suspicious_function_call::*;
|
||||
pub(crate) use tarfile_unsafe_members::*;
|
||||
pub(crate) use try_except_continue::*;
|
||||
pub(crate) use try_except_pass::*;
|
||||
pub(crate) use unsafe_yaml_load::*;
|
||||
@@ -27,6 +29,7 @@ pub(crate) use weak_cryptographic_key::*;
|
||||
|
||||
mod assert_used;
|
||||
mod bad_file_permissions;
|
||||
mod django_raw_sql;
|
||||
mod exec_used;
|
||||
mod flask_debug_true;
|
||||
mod hardcoded_bind_all_interfaces;
|
||||
@@ -47,6 +50,7 @@ mod snmp_insecure_version;
|
||||
mod snmp_weak_cryptography;
|
||||
mod ssh_no_host_key_verification;
|
||||
mod suspicious_function_call;
|
||||
mod tarfile_unsafe_members;
|
||||
mod try_except_continue;
|
||||
mod try_except_pass;
|
||||
mod unsafe_yaml_load;
|
||||
|
||||
@@ -854,11 +854,11 @@ pub(crate) fn suspicious_function_call(checker: &mut Checker, call: &ExprCall) {
|
||||
["six", "moves", "urllib", "request", "urlopen" | "urlretrieve" | "Request"] => {
|
||||
// If the `url` argument is a string literal, allow `http` and `https` schemes.
|
||||
if call.arguments.args.iter().all(|arg| !arg.is_starred_expr()) && call.arguments.keywords.iter().all(|keyword| keyword.arg.is_some()) {
|
||||
if let Some(Expr::StringLiteral(ast::ExprStringLiteral { value: url, .. })) = &call.arguments.find_argument("url", 0) {
|
||||
let url = url.trim_start();
|
||||
if url.starts_with("http://") || url.starts_with("https://") {
|
||||
return None;
|
||||
}
|
||||
if let Some(Expr::StringLiteral(ast::ExprStringLiteral { value, .. })) = &call.arguments.find_argument("url", 0) {
|
||||
let url = value.as_str().trim_start();
|
||||
if url.starts_with("http://") || url.starts_with("https://") {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(SuspiciousURLOpenUsage.into())
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
use crate::checkers::ast::Checker;
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_diagnostics::Violation;
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `tarfile.extractall`.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
///
|
||||
/// Extracting archives from untrusted sources without prior inspection is
|
||||
/// a security risk, as maliciously crafted archives may contain files that
|
||||
/// will be written outside of the target directory. For example, the archive
|
||||
/// could include files with absolute paths (e.g., `/etc/passwd`), or relative
|
||||
/// paths with parent directory references (e.g., `../etc/passwd`).
|
||||
///
|
||||
/// On Python 3.12 and later, use `filter='data'` to prevent the most dangerous
|
||||
/// security issues (see: [PEP 706]). On earlier versions, set the `members`
|
||||
/// argument to a trusted subset of the archive's members.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import tarfile
|
||||
/// import tempfile
|
||||
///
|
||||
/// tar = tarfile.open(filename)
|
||||
/// tar.extractall(path=tempfile.mkdtemp())
|
||||
/// tar.close()
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Common Weakness Enumeration: CWE-22](https://cwe.mitre.org/data/definitions/22.html)
|
||||
/// - [Python Documentation: `TarFile.extractall`](https://docs.python.org/3/library/tarfile.html#tarfile.TarFile.extractall)
|
||||
/// - [Python Documentation: Extraction filters](https://docs.python.org/3/library/tarfile.html#tarfile-extraction-filter)
|
||||
///
|
||||
/// [PEP 706]: https://peps.python.org/pep-0706/#backporting-forward-compatibility
|
||||
#[violation]
|
||||
pub struct TarfileUnsafeMembers;
|
||||
|
||||
impl Violation for TarfileUnsafeMembers {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Uses of `tarfile.extractall()`")
|
||||
}
|
||||
}
|
||||
|
||||
/// S202
|
||||
pub(crate) fn tarfile_unsafe_members(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
if !call
|
||||
.func
|
||||
.as_attribute_expr()
|
||||
.is_some_and(|attr| attr.attr.as_str() == "extractall")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if call
|
||||
.arguments
|
||||
.find_keyword("filter")
|
||||
.and_then(|keyword| keyword.value.as_string_literal_expr())
|
||||
.is_some_and(|value| matches!(value.value.as_str(), "data" | "tar"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if !checker.semantic().seen(&["tarfile"]) {
|
||||
return;
|
||||
}
|
||||
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(TarfileUnsafeMembers, call.func.range()));
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
---
|
||||
S202.py:8:5: S202 Uses of `tarfile.extractall()`
|
||||
|
|
||||
6 | def unsafe_archive_handler(filename):
|
||||
7 | tar = tarfile.open(filename)
|
||||
8 | tar.extractall(path=tempfile.mkdtemp())
|
||||
| ^^^^^^^^^^^^^^ S202
|
||||
9 | tar.close()
|
||||
|
|
||||
|
||||
S202.py:14:5: S202 Uses of `tarfile.extractall()`
|
||||
|
|
||||
12 | def managed_members_archive_handler(filename):
|
||||
13 | tar = tarfile.open(filename)
|
||||
14 | tar.extractall(path=tempfile.mkdtemp(), members=members_filter(tar))
|
||||
| ^^^^^^^^^^^^^^ S202
|
||||
15 | tar.close()
|
||||
|
|
||||
|
||||
S202.py:20:5: S202 Uses of `tarfile.extractall()`
|
||||
|
|
||||
18 | def list_members_archive_handler(filename):
|
||||
19 | tar = tarfile.open(filename)
|
||||
20 | tar.extractall(path=tempfile.mkdtemp(), members=[])
|
||||
| ^^^^^^^^^^^^^^ S202
|
||||
21 | tar.close()
|
||||
|
|
||||
|
||||
S202.py:26:5: S202 Uses of `tarfile.extractall()`
|
||||
|
|
||||
24 | def provided_members_archive_handler(filename):
|
||||
25 | tar = tarfile.open(filename)
|
||||
26 | tarfile.extractall(path=tempfile.mkdtemp(), members=tar)
|
||||
| ^^^^^^^^^^^^^^^^^^ S202
|
||||
27 | tar.close()
|
||||
|
|
||||
|
||||
S202.py:38:5: S202 Uses of `tarfile.extractall()`
|
||||
|
|
||||
36 | def filter_fully_trusted(filename):
|
||||
37 | tar = tarfile.open(filename)
|
||||
38 | tarfile.extractall(path=tempfile.mkdtemp(), filter="fully_trusted")
|
||||
| ^^^^^^^^^^^^^^^^^^ S202
|
||||
39 | tar.close()
|
||||
|
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
---
|
||||
S611.py:5:27: S611 Use of `RawSQL` can lead to SQL injection vulnerabilities
|
||||
|
|
||||
4 | User.objects.annotate(val=RawSQL('secure', []))
|
||||
5 | User.objects.annotate(val=RawSQL('%secure' % 'nos', []))
|
||||
| ^^^^^^ S611
|
||||
6 | User.objects.annotate(val=RawSQL('{}secure'.format('no'), []))
|
||||
7 | raw = '"username") AS "val" FROM "auth_user" WHERE "username"="admin" --'
|
||||
|
|
||||
|
||||
S611.py:6:27: S611 Use of `RawSQL` can lead to SQL injection vulnerabilities
|
||||
|
|
||||
4 | User.objects.annotate(val=RawSQL('secure', []))
|
||||
5 | User.objects.annotate(val=RawSQL('%secure' % 'nos', []))
|
||||
6 | User.objects.annotate(val=RawSQL('{}secure'.format('no'), []))
|
||||
| ^^^^^^ S611
|
||||
7 | raw = '"username") AS "val" FROM "auth_user" WHERE "username"="admin" --'
|
||||
8 | User.objects.annotate(val=RawSQL(raw, []))
|
||||
|
|
||||
|
||||
S611.py:8:27: S611 Use of `RawSQL` can lead to SQL injection vulnerabilities
|
||||
|
|
||||
6 | User.objects.annotate(val=RawSQL('{}secure'.format('no'), []))
|
||||
7 | raw = '"username") AS "val" FROM "auth_user" WHERE "username"="admin" --'
|
||||
8 | User.objects.annotate(val=RawSQL(raw, []))
|
||||
| ^^^^^^ S611
|
||||
9 | raw = '"username") AS "val" FROM "auth_user"' \
|
||||
10 | ' WHERE "username"="admin" OR 1=%s --'
|
||||
|
|
||||
|
||||
S611.py:11:27: S611 Use of `RawSQL` can lead to SQL injection vulnerabilities
|
||||
|
|
||||
9 | raw = '"username") AS "val" FROM "auth_user"' \
|
||||
10 | ' WHERE "username"="admin" OR 1=%s --'
|
||||
11 | User.objects.annotate(val=RawSQL(raw, [0]))
|
||||
| ^^^^^^ S611
|
||||
12 | User.objects.annotate(val=RawSQL(sql='{}secure'.format('no'), params=[]))
|
||||
13 | User.objects.annotate(val=RawSQL(params=[], sql='{}secure'.format('no')))
|
||||
|
|
||||
|
||||
S611.py:12:27: S611 Use of `RawSQL` can lead to SQL injection vulnerabilities
|
||||
|
|
||||
10 | ' WHERE "username"="admin" OR 1=%s --'
|
||||
11 | User.objects.annotate(val=RawSQL(raw, [0]))
|
||||
12 | User.objects.annotate(val=RawSQL(sql='{}secure'.format('no'), params=[]))
|
||||
| ^^^^^^ S611
|
||||
13 | User.objects.annotate(val=RawSQL(params=[], sql='{}secure'.format('no')))
|
||||
|
|
||||
|
||||
S611.py:13:27: S611 Use of `RawSQL` can lead to SQL injection vulnerabilities
|
||||
|
|
||||
11 | User.objects.annotate(val=RawSQL(raw, [0]))
|
||||
12 | User.objects.annotate(val=RawSQL(sql='{}secure'.format('no'), params=[]))
|
||||
13 | User.objects.annotate(val=RawSQL(params=[], sql='{}secure'.format('no')))
|
||||
| ^^^^^^ S611
|
||||
|
|
||||
|
||||
|
||||
28
crates/ruff_linter/src/rules/flake8_bugbear/helpers.rs
Normal file
28
crates/ruff_linter/src/rules/flake8_bugbear/helpers.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
use ruff_notebook::CellOffsets;
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
/// Return `true` if the statement containing the current expression is the last
|
||||
/// top-level expression in the cell. This assumes that the source is a Jupyter
|
||||
/// Notebook.
|
||||
pub(super) fn at_last_top_level_expression_in_cell(
|
||||
semantic: &SemanticModel,
|
||||
locator: &Locator,
|
||||
cell_offsets: Option<&CellOffsets>,
|
||||
) -> bool {
|
||||
if !semantic.at_top_level() {
|
||||
return false;
|
||||
}
|
||||
let current_statement_end = semantic.current_statement().end();
|
||||
cell_offsets
|
||||
.and_then(|cell_offsets| cell_offsets.containing_range(current_statement_end))
|
||||
.is_some_and(|cell_range| {
|
||||
SimpleTokenizer::new(
|
||||
locator.contents(),
|
||||
TextRange::new(current_statement_end, cell_range.end()),
|
||||
)
|
||||
.all(|token| token.kind() == SimpleTokenKind::Semi || token.kind().is_trivia())
|
||||
})
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
//! Rules from [flake8-bugbear](https://pypi.org/project/flake8-bugbear/).
|
||||
pub(crate) mod helpers;
|
||||
pub(crate) mod rules;
|
||||
pub mod settings;
|
||||
|
||||
@@ -54,8 +55,10 @@ mod tests {
|
||||
#[test_case(Rule::UnreliableCallableCheck, Path::new("B004.py"))]
|
||||
#[test_case(Rule::UnusedLoopControlVariable, Path::new("B007.py"))]
|
||||
#[test_case(Rule::UselessComparison, Path::new("B015.py"))]
|
||||
#[test_case(Rule::UselessComparison, Path::new("B015.ipynb"))]
|
||||
#[test_case(Rule::UselessContextlibSuppress, Path::new("B022.py"))]
|
||||
#[test_case(Rule::UselessExpression, Path::new("B018.py"))]
|
||||
#[test_case(Rule::UselessExpression, Path::new("B018.ipynb"))]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
|
||||
@@ -69,10 +69,10 @@ pub(crate) fn getattr_with_constant(
|
||||
let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = arg else {
|
||||
return;
|
||||
};
|
||||
if !is_identifier(value) {
|
||||
if !is_identifier(value.as_str()) {
|
||||
return;
|
||||
}
|
||||
if is_mangled_private(value) {
|
||||
if is_mangled_private(value.as_str()) {
|
||||
return;
|
||||
}
|
||||
if !checker.semantic().is_builtin("getattr") {
|
||||
|
||||
@@ -83,10 +83,10 @@ pub(crate) fn setattr_with_constant(
|
||||
let Expr::StringLiteral(ast::ExprStringLiteral { value: name, .. }) = name else {
|
||||
return;
|
||||
};
|
||||
if !is_identifier(name) {
|
||||
if !is_identifier(name.as_str()) {
|
||||
return;
|
||||
}
|
||||
if is_mangled_private(name) {
|
||||
if is_mangled_private(name.as_str()) {
|
||||
return;
|
||||
}
|
||||
if !checker.semantic().is_builtin("setattr") {
|
||||
@@ -104,7 +104,7 @@ pub(crate) fn setattr_with_constant(
|
||||
if expr == child.as_ref() {
|
||||
let mut diagnostic = Diagnostic::new(SetAttrWithConstant, expr.range());
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||
assignment(obj, name, value, checker.generator()),
|
||||
assignment(obj, name.as_str(), value, checker.generator()),
|
||||
expr.range(),
|
||||
)));
|
||||
checker.diagnostics.push(diagnostic);
|
||||
|
||||
@@ -5,6 +5,8 @@ use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
use super::super::helpers::at_last_top_level_expression_in_cell;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for useless comparisons.
|
||||
///
|
||||
@@ -41,6 +43,19 @@ impl Violation for UselessComparison {
|
||||
/// B015
|
||||
pub(crate) fn useless_comparison(checker: &mut Checker, expr: &Expr) {
|
||||
if expr.is_compare_expr() {
|
||||
// For Jupyter Notebooks, ignore the last top-level expression for each cell.
|
||||
// This is because it's common to have a cell that ends with an expression
|
||||
// to display it's value.
|
||||
if checker.source_type.is_ipynb()
|
||||
&& at_last_top_level_expression_in_cell(
|
||||
checker.semantic(),
|
||||
checker.locator(),
|
||||
checker.cell_offsets(),
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(UselessComparison, expr.range()));
|
||||
|
||||
@@ -6,6 +6,8 @@ use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
use super::super::helpers::at_last_top_level_expression_in_cell;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for useless expressions.
|
||||
///
|
||||
@@ -59,6 +61,19 @@ pub(crate) fn useless_expression(checker: &mut Checker, value: &Expr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For Jupyter Notebooks, ignore the last top-level expression for each cell.
|
||||
// This is because it's common to have a cell that ends with an expression
|
||||
// to display it's value.
|
||||
if checker.source_type.is_ipynb()
|
||||
&& at_last_top_level_expression_in_cell(
|
||||
checker.semantic(),
|
||||
checker.locator(),
|
||||
checker.cell_offsets(),
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore statements that have side effects.
|
||||
if contains_effect(value, |id| checker.semantic().is_builtin(id)) {
|
||||
// Flag attributes as useless expressions, even if they're attached to calls or other
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
|
||||
---
|
||||
B015.ipynb:5:1: B015 Pointless comparison. Did you mean to assign a value? Otherwise, prepend `assert` or remove it.
|
||||
|
|
||||
3 | x == 1
|
||||
4 | # Only skip the last expression
|
||||
5 | x == 1 # B018
|
||||
| ^^^^^^ B015
|
||||
6 | x == 1
|
||||
7 | # Nested expressions isn't relevant
|
||||
|
|
||||
|
||||
B015.ipynb:9:5: B015 Pointless comparison. Did you mean to assign a value? Otherwise, prepend `assert` or remove it.
|
||||
|
|
||||
7 | # Nested expressions isn't relevant
|
||||
8 | if True:
|
||||
9 | x == 1
|
||||
| ^^^^^^ B015
|
||||
10 | # Semicolons shouldn't affect the output
|
||||
11 | x == 1;
|
||||
|
|
||||
|
||||
B015.ipynb:13:1: B015 Pointless comparison. Did you mean to assign a value? Otherwise, prepend `assert` or remove it.
|
||||
|
|
||||
11 | x == 1;
|
||||
12 | # Semicolons with multiple expressions
|
||||
13 | x == 1; x == 1
|
||||
| ^^^^^^ B015
|
||||
14 | # Comments, newlines and whitespace
|
||||
15 | x == 1 # comment
|
||||
|
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
|
||||
---
|
||||
B018.ipynb:5:1: B018 Found useless expression. Either assign it to a variable or remove it.
|
||||
|
|
||||
3 | x
|
||||
4 | # Only skip the last expression
|
||||
5 | x # B018
|
||||
| ^ B018
|
||||
6 | x
|
||||
7 | # Nested expressions isn't relevant
|
||||
|
|
||||
|
||||
B018.ipynb:9:5: B018 Found useless expression. Either assign it to a variable or remove it.
|
||||
|
|
||||
7 | # Nested expressions isn't relevant
|
||||
8 | if True:
|
||||
9 | x
|
||||
| ^ B018
|
||||
10 | # Semicolons shouldn't affect the output
|
||||
11 | x;
|
||||
|
|
||||
|
||||
B018.ipynb:13:1: B018 Found useless expression. Either assign it to a variable or remove it.
|
||||
|
|
||||
11 | x;
|
||||
12 | # Semicolons with multiple expressions
|
||||
13 | x; x
|
||||
| ^ B018
|
||||
14 | # Comments, newlines and whitespace
|
||||
15 | x # comment
|
||||
|
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ pub(crate) fn call_datetime_strptime_without_zone(checker: &mut Checker, call: &
|
||||
if let Some(Expr::StringLiteral(ast::ExprStringLiteral { value: format, .. })) =
|
||||
call.arguments.args.get(1).as_ref()
|
||||
{
|
||||
if format.contains("%z") {
|
||||
if format.as_str().contains("%z") {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -93,7 +93,7 @@ fn check_log_record_attr_clash(checker: &mut Checker, extra: &Keyword) {
|
||||
for key in keys {
|
||||
if let Some(key) = &key {
|
||||
if let Expr::StringLiteral(ast::ExprStringLiteral { value: attr, .. }) = key {
|
||||
if is_reserved_attr(attr) {
|
||||
if is_reserved_attr(attr.as_str()) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
LoggingExtraAttrClash(attr.to_string()),
|
||||
key.range(),
|
||||
|
||||
@@ -21,6 +21,7 @@ mod tests {
|
||||
#[test_case(Rule::UnnecessarySpread, Path::new("PIE800.py"))]
|
||||
#[test_case(Rule::ReimplementedContainerBuiltin, Path::new("PIE807.py"))]
|
||||
#[test_case(Rule::NonUniqueEnums, Path::new("PIE796.py"))]
|
||||
#[test_case(Rule::NonUniqueEnums, Path::new("PIE796.pyi"))]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
|
||||
@@ -4,7 +4,7 @@ use ruff_diagnostics::Diagnostic;
|
||||
use ruff_diagnostics::Violation;
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::comparable::ComparableExpr;
|
||||
use ruff_python_ast::{self as ast, Expr, Stmt};
|
||||
use ruff_python_ast::{self as ast, Expr, PySourceType, Stmt};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -84,7 +84,15 @@ pub(crate) fn non_unique_enums(checker: &mut Checker, parent: &Stmt, body: &[Stm
|
||||
}
|
||||
}
|
||||
|
||||
if !seen_targets.insert(ComparableExpr::from(value)) {
|
||||
let comparable = ComparableExpr::from(value);
|
||||
|
||||
if checker.source_type == PySourceType::Stub
|
||||
&& comparable == ComparableExpr::EllipsisLiteral
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if !seen_targets.insert(comparable) {
|
||||
let diagnostic = Diagnostic::new(
|
||||
NonUniqueEnums {
|
||||
value: checker.generator().expr(value),
|
||||
|
||||
@@ -110,8 +110,8 @@ pub(crate) fn unnecessary_dict_kwargs(checker: &mut Checker, expr: &Expr, kwargs
|
||||
/// Return `Some` if a key is a valid keyword argument name, or `None` otherwise.
|
||||
fn as_kwarg(key: &Expr) -> Option<&str> {
|
||||
if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = key {
|
||||
if is_identifier(value) {
|
||||
return Some(value);
|
||||
if is_identifier(value.as_str()) {
|
||||
return Some(value.as_str());
|
||||
}
|
||||
}
|
||||
None
|
||||
|
||||
@@ -3,6 +3,7 @@ use ruff_diagnostics::{Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::whitespace::trailing_comment_start_offset;
|
||||
use ruff_python_ast::Stmt;
|
||||
use ruff_python_semantic::{ScopeKind, SemanticModel};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -93,6 +94,12 @@ pub(crate) fn unnecessary_placeholder(checker: &mut Checker, body: &[Stmt]) {
|
||||
if expr.value.is_ellipsis_literal_expr()
|
||||
&& checker.settings.preview.is_enabled() =>
|
||||
{
|
||||
// Ellipses are significant in protocol methods and abstract methods. Specifically,
|
||||
// Pyright uses the presence of an ellipsis to indicate that a method is a stub,
|
||||
// rather than a default implementation.
|
||||
if in_protocol_or_abstract_method(checker.semantic()) {
|
||||
return;
|
||||
}
|
||||
Placeholder::Ellipsis
|
||||
}
|
||||
_ => continue,
|
||||
@@ -125,3 +132,21 @@ impl std::fmt::Display for Placeholder {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return `true` if the [`SemanticModel`] is in a `typing.Protocol` subclass or an abstract
|
||||
/// method.
|
||||
fn in_protocol_or_abstract_method(semantic: &SemanticModel) -> bool {
|
||||
semantic.current_scopes().any(|scope| match scope.kind {
|
||||
ScopeKind::Class(class_def) => class_def
|
||||
.bases()
|
||||
.iter()
|
||||
.any(|base| semantic.match_typing_expr(base, "Protocol")),
|
||||
ScopeKind::Function(function_def) => {
|
||||
ruff_python_semantic::analyze::visibility::is_abstract(
|
||||
&function_def.decorator_list,
|
||||
semantic,
|
||||
)
|
||||
}
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -473,6 +473,8 @@ PIE790.py:179:5: PIE790 [*] Unnecessary `pass` statement
|
||||
178 | ...
|
||||
179 | pass
|
||||
| ^^^^ PIE790
|
||||
180 |
|
||||
181 | from typing import Protocol
|
||||
|
|
||||
= help: Remove unnecessary `pass`
|
||||
|
||||
@@ -481,5 +483,8 @@ PIE790.py:179:5: PIE790 [*] Unnecessary `pass` statement
|
||||
177 177 | for i in range(10):
|
||||
178 178 | ...
|
||||
179 |- pass
|
||||
180 179 |
|
||||
181 180 | from typing import Protocol
|
||||
182 181 |
|
||||
|
||||
|
||||
|
||||
@@ -57,4 +57,21 @@ PIE796.py:54:5: PIE796 Enum contains duplicate value: `2`
|
||||
| ^^^^^ PIE796
|
||||
|
|
||||
|
||||
PIE796.py:71:5: PIE796 Enum contains duplicate value: `...`
|
||||
|
|
||||
69 | class FakeEnum10(enum.Enum):
|
||||
70 | A = ...
|
||||
71 | B = ... # PIE796
|
||||
| ^^^^^^^ PIE796
|
||||
72 | C = ... # PIE796
|
||||
|
|
||||
|
||||
PIE796.py:72:5: PIE796 Enum contains duplicate value: `...`
|
||||
|
|
||||
70 | A = ...
|
||||
71 | B = ... # PIE796
|
||||
72 | C = ... # PIE796
|
||||
| ^^^^^^^ PIE796
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_pie/mod.rs
|
||||
---
|
||||
|
||||
@@ -634,6 +634,8 @@ PIE790.py:178:5: PIE790 [*] Unnecessary `...` literal
|
||||
177 177 | for i in range(10):
|
||||
178 |- ...
|
||||
179 178 | pass
|
||||
180 179 |
|
||||
181 180 | from typing import Protocol
|
||||
|
||||
PIE790.py:179:5: PIE790 [*] Unnecessary `pass` statement
|
||||
|
|
||||
@@ -641,6 +643,8 @@ PIE790.py:179:5: PIE790 [*] Unnecessary `pass` statement
|
||||
178 | ...
|
||||
179 | pass
|
||||
| ^^^^ PIE790
|
||||
180 |
|
||||
181 | from typing import Protocol
|
||||
|
|
||||
= help: Remove unnecessary `pass`
|
||||
|
||||
@@ -649,5 +653,23 @@ PIE790.py:179:5: PIE790 [*] Unnecessary `pass` statement
|
||||
177 177 | for i in range(10):
|
||||
178 178 | ...
|
||||
179 |- pass
|
||||
180 179 |
|
||||
181 180 | from typing import Protocol
|
||||
182 181 |
|
||||
|
||||
PIE790.py:209:9: PIE790 [*] Unnecessary `...` literal
|
||||
|
|
||||
207 | def stub(self) -> str:
|
||||
208 | """Docstring"""
|
||||
209 | ...
|
||||
| ^^^ PIE790
|
||||
|
|
||||
= help: Remove unnecessary `...`
|
||||
|
||||
ℹ Safe fix
|
||||
206 206 |
|
||||
207 207 | def stub(self) -> str:
|
||||
208 208 | """Docstring"""
|
||||
209 |- ...
|
||||
|
||||
|
||||
|
||||
@@ -15,9 +15,10 @@ use crate::checkers::ast::Checker;
|
||||
/// annotation instead of using `typing_extensions.Self`.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// If certain methods are annotated with a custom `TypeVar` type, and the
|
||||
/// class is subclassed, type checkers will not be able to infer the correct
|
||||
/// return type.
|
||||
/// While the semantics are often identical, using `typing_extensions.Self` is
|
||||
/// more intuitive and succinct (per [PEP 673]) than a custom `TypeVar`. For
|
||||
/// example, the use of `Self` will typically allow for the omission of type
|
||||
/// parameters on the `self` and `cls` arguments.
|
||||
///
|
||||
/// This check currently applies to instance methods that return `self`, class
|
||||
/// methods that return an instance of `cls`, and `__new__` methods.
|
||||
@@ -42,16 +43,18 @@ use crate::checkers::ast::Checker;
|
||||
///
|
||||
///
|
||||
/// class Foo:
|
||||
/// def __new__(cls: type[Self], *args: str, **kwargs: int) -> Self:
|
||||
/// def __new__(cls, *args: str, **kwargs: int) -> Self:
|
||||
/// ...
|
||||
///
|
||||
/// def foo(self: Self, arg: bytes) -> Self:
|
||||
/// def foo(self, arg: bytes) -> Self:
|
||||
/// ...
|
||||
///
|
||||
/// @classmethod
|
||||
/// def bar(cls: type[Self], arg: int) -> Self:
|
||||
/// def bar(cls, arg: int) -> Self:
|
||||
/// ...
|
||||
/// ```
|
||||
///
|
||||
/// [PEP 673]: https://peps.python.org/pep-0673/#motivation
|
||||
#[violation]
|
||||
pub struct CustomTypeVarReturnType {
|
||||
method_name: String,
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::call_path::CallPath;
|
||||
use ruff_python_ast::helpers::map_subscript;
|
||||
use ruff_python_ast::{
|
||||
self as ast, Arguments, Expr, Operator, ParameterWithDefault, Parameters, Stmt, UnaryOp,
|
||||
self as ast, Expr, Operator, ParameterWithDefault, Parameters, Stmt, UnaryOp,
|
||||
};
|
||||
use ruff_python_semantic::{ScopeKind, SemanticModel};
|
||||
use ruff_python_semantic::{BindingId, ScopeKind, SemanticModel};
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::importer::ImportRequest;
|
||||
|
||||
use crate::rules::flake8_pyi::rules::TypingModule;
|
||||
use crate::settings::types::PythonVersion;
|
||||
|
||||
@@ -469,21 +471,50 @@ fn is_final_assignment(annotation: &Expr, value: &Expr, semantic: &SemanticModel
|
||||
}
|
||||
|
||||
/// Returns `true` if the a class is an enum, based on its base classes.
|
||||
fn is_enum(arguments: Option<&Arguments>, semantic: &SemanticModel) -> bool {
|
||||
let Some(Arguments { args: bases, .. }) = arguments else {
|
||||
return false;
|
||||
};
|
||||
return bases.iter().any(|expr| {
|
||||
semantic.resolve_call_path(expr).is_some_and(|call_path| {
|
||||
matches!(
|
||||
call_path.as_slice(),
|
||||
[
|
||||
"enum",
|
||||
"Enum" | "Flag" | "IntEnum" | "IntFlag" | "StrEnum" | "ReprEnum"
|
||||
]
|
||||
)
|
||||
fn is_enum(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool {
|
||||
fn inner(
|
||||
class_def: &ast::StmtClassDef,
|
||||
semantic: &SemanticModel,
|
||||
seen: &mut FxHashSet<BindingId>,
|
||||
) -> bool {
|
||||
class_def.bases().iter().any(|expr| {
|
||||
// If the base class is `enum.Enum`, `enum.Flag`, etc., then this is an enum.
|
||||
if semantic
|
||||
.resolve_call_path(map_subscript(expr))
|
||||
.is_some_and(|call_path| {
|
||||
matches!(
|
||||
call_path.as_slice(),
|
||||
[
|
||||
"enum",
|
||||
"Enum" | "Flag" | "IntEnum" | "IntFlag" | "StrEnum" | "ReprEnum"
|
||||
]
|
||||
)
|
||||
})
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the base class extends `enum.Enum`, `enum.Flag`, etc., then this is an enum.
|
||||
if let Some(id) = semantic.lookup_attribute(map_subscript(expr)) {
|
||||
if seen.insert(id) {
|
||||
let binding = semantic.binding(id);
|
||||
if let Some(base_class) = binding
|
||||
.kind
|
||||
.as_class_definition()
|
||||
.map(|id| &semantic.scopes[*id])
|
||||
.and_then(|scope| scope.kind.as_class())
|
||||
{
|
||||
if inner(base_class, semantic, seen) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
inner(class_def, semantic, &mut FxHashSet::default())
|
||||
}
|
||||
|
||||
/// Returns `true` if an [`Expr`] is a value that should be annotated with `typing.TypeAlias`.
|
||||
@@ -655,10 +686,8 @@ pub(crate) fn unannotated_assignment_in_stub(
|
||||
return;
|
||||
}
|
||||
|
||||
if let ScopeKind::Class(ast::StmtClassDef { arguments, .. }) =
|
||||
checker.semantic().current_scope().kind
|
||||
{
|
||||
if is_enum(arguments.as_deref(), checker.semantic()) {
|
||||
if let ScopeKind::Class(class_def) = checker.semantic().current_scope().kind {
|
||||
if is_enum(class_def, checker.semantic()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ pub(crate) fn unrecognized_platform(checker: &mut Checker, test: &Expr) {
|
||||
if !matches!(value.as_str(), "linux" | "win32" | "cygwin" | "darwin") {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
UnrecognizedPlatformName {
|
||||
platform: value.clone(),
|
||||
platform: value.to_string(),
|
||||
},
|
||||
right.range(),
|
||||
));
|
||||
|
||||
@@ -131,4 +131,11 @@ PYI052.pyi:39:12: PYI052 Need type annotation for `field212`
|
||||
41 | field22: Final = {"foo": 5}
|
||||
|
|
||||
|
||||
PYI052.pyi:114:11: PYI052 Need type annotation for `WIZ`
|
||||
|
|
||||
113 | class Bop:
|
||||
114 | WIZ = 4
|
||||
| ^ PYI052
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -55,8 +55,13 @@ pub(super) fn is_empty_or_null_string(expr: &Expr) -> bool {
|
||||
match expr {
|
||||
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => value.is_empty(),
|
||||
Expr::NoneLiteral(_) => true,
|
||||
Expr::FString(ast::ExprFString { values, .. }) => {
|
||||
values.iter().all(is_empty_or_null_string)
|
||||
Expr::FString(ast::ExprFString { value, .. }) => {
|
||||
value.parts().all(|f_string_part| match f_string_part {
|
||||
ast::FStringPart::Literal(literal) => literal.is_empty(),
|
||||
ast::FStringPart::FString(f_string) => {
|
||||
f_string.values.iter().all(is_empty_or_null_string)
|
||||
}
|
||||
})
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
|
||||
@@ -252,7 +252,7 @@ fn elts_to_csv(elts: &[Expr], generator: Generator) -> Option<String> {
|
||||
return None;
|
||||
}
|
||||
|
||||
let node = Expr::StringLiteral(ast::ExprStringLiteral {
|
||||
let node = Expr::from(ast::StringLiteral {
|
||||
value: elts.iter().fold(String::new(), |mut acc, elt| {
|
||||
if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = elt {
|
||||
if !acc.is_empty() {
|
||||
@@ -262,9 +262,7 @@ fn elts_to_csv(elts: &[Expr], generator: Generator) -> Option<String> {
|
||||
}
|
||||
acc
|
||||
}),
|
||||
unicode: false,
|
||||
implicit_concatenated: false,
|
||||
range: TextRange::default(),
|
||||
..ast::StringLiteral::default()
|
||||
});
|
||||
Some(generator.expr(&node))
|
||||
}
|
||||
@@ -302,8 +300,8 @@ fn check_names(checker: &mut Checker, decorator: &Decorator, expr: &Expr) {
|
||||
let names_type = checker.settings.flake8_pytest_style.parametrize_names_type;
|
||||
|
||||
match expr {
|
||||
Expr::StringLiteral(ast::ExprStringLiteral { value: string, .. }) => {
|
||||
let names = split_names(string);
|
||||
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => {
|
||||
let names = split_names(value.as_str());
|
||||
if names.len() > 1 {
|
||||
match names_type {
|
||||
types::ParametrizeNameType::Tuple => {
|
||||
@@ -324,9 +322,9 @@ fn check_names(checker: &mut Checker, decorator: &Decorator, expr: &Expr) {
|
||||
elts: names
|
||||
.iter()
|
||||
.map(|name| {
|
||||
Expr::StringLiteral(ast::ExprStringLiteral {
|
||||
Expr::from(ast::StringLiteral {
|
||||
value: (*name).to_string(),
|
||||
..ast::ExprStringLiteral::default()
|
||||
..ast::StringLiteral::default()
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
@@ -357,9 +355,9 @@ fn check_names(checker: &mut Checker, decorator: &Decorator, expr: &Expr) {
|
||||
elts: names
|
||||
.iter()
|
||||
.map(|name| {
|
||||
Expr::StringLiteral(ast::ExprStringLiteral {
|
||||
Expr::from(ast::StringLiteral {
|
||||
value: (*name).to_string(),
|
||||
..ast::ExprStringLiteral::default()
|
||||
..ast::StringLiteral::default()
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
@@ -477,12 +475,11 @@ fn check_values(checker: &mut Checker, names: &Expr, values: &Expr) {
|
||||
.flake8_pytest_style
|
||||
.parametrize_values_row_type;
|
||||
|
||||
let is_multi_named =
|
||||
if let Expr::StringLiteral(ast::ExprStringLiteral { value: string, .. }) = &names {
|
||||
split_names(string).len() > 1
|
||||
} else {
|
||||
true
|
||||
};
|
||||
let is_multi_named = if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = &names {
|
||||
split_names(value.as_str()).len() > 1
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
match values {
|
||||
Expr::List(ast::ExprList { elts, .. }) => {
|
||||
|
||||
@@ -57,6 +57,7 @@ mod tests {
|
||||
|
||||
#[test_case(Rule::InDictKeys, Path::new("SIM118.py"))]
|
||||
#[test_case(Rule::IfElseBlockInsteadOfDictGet, Path::new("SIM401.py"))]
|
||||
#[test_case(Rule::DictGetWithNoneDefault, Path::new("SIM910.py"))]
|
||||
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
"preview__{}_{}",
|
||||
|
||||
@@ -4,6 +4,7 @@ use ruff_text_size::Ranged;
|
||||
use crate::fix::snippet::SourceCodeSnippet;
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_semantic::analyze::typing::is_dict;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
@@ -62,26 +63,32 @@ impl Violation for UncapitalizedEnvironmentVariables {
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Check for `dict.get()` calls that pass `None` as the default value.
|
||||
/// Checks for `dict.get()` calls that pass `None` as the default value.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// `None` is the default value for `dict.get()`, so it is redundant to pass it
|
||||
/// explicitly.
|
||||
///
|
||||
/// In [preview], this rule applies to variables that are inferred to be
|
||||
/// dictionaries; in stable, it's limited to dictionary literals (e.g.,
|
||||
/// `{"foo": 1}.get("foo", None)`).
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// ages = {"Tom": 23, "Maria": 23, "Dog": 11}
|
||||
/// age = ages.get("Cat", None) # None
|
||||
/// age = ages.get("Cat", None)
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// ages = {"Tom": 23, "Maria": 23, "Dog": 11}
|
||||
/// age = ages.get("Cat") # None
|
||||
/// age = ages.get("Cat")
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `dict.get`](https://docs.python.org/3/library/stdtypes.html#dict.get)
|
||||
///
|
||||
/// [preview]: https://docs.astral.sh/ruff/preview/
|
||||
#[violation]
|
||||
pub struct DictGetWithNoneDefault {
|
||||
expected: SourceCodeSnippet,
|
||||
@@ -154,19 +161,19 @@ pub(crate) fn use_capital_environment_variables(checker: &mut Checker, expr: &Ex
|
||||
return;
|
||||
}
|
||||
|
||||
if is_lowercase_allowed(env_var) {
|
||||
if is_lowercase_allowed(env_var.as_str()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let capital_env_var = env_var.to_ascii_uppercase();
|
||||
if &capital_env_var == env_var {
|
||||
let capital_env_var = env_var.as_str().to_ascii_uppercase();
|
||||
if capital_env_var == env_var.as_str() {
|
||||
return;
|
||||
}
|
||||
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
UncapitalizedEnvironmentVariables {
|
||||
expected: SourceCodeSnippet::new(capital_env_var),
|
||||
actual: SourceCodeSnippet::new(env_var.clone()),
|
||||
actual: SourceCodeSnippet::new(env_var.to_string()),
|
||||
},
|
||||
arg.range(),
|
||||
));
|
||||
@@ -190,35 +197,30 @@ fn check_os_environ_subscript(checker: &mut Checker, expr: &Expr) {
|
||||
if id != "os" || attr != "environ" {
|
||||
return;
|
||||
}
|
||||
let Expr::StringLiteral(ast::ExprStringLiteral {
|
||||
value: env_var,
|
||||
unicode,
|
||||
..
|
||||
}) = slice.as_ref()
|
||||
else {
|
||||
let Expr::StringLiteral(ast::ExprStringLiteral { value: env_var, .. }) = slice.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if is_lowercase_allowed(env_var) {
|
||||
if is_lowercase_allowed(env_var.as_str()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let capital_env_var = env_var.to_ascii_uppercase();
|
||||
if &capital_env_var == env_var {
|
||||
let capital_env_var = env_var.as_str().to_ascii_uppercase();
|
||||
if capital_env_var == env_var.as_str() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
UncapitalizedEnvironmentVariables {
|
||||
expected: SourceCodeSnippet::new(capital_env_var.clone()),
|
||||
actual: SourceCodeSnippet::new(env_var.clone()),
|
||||
actual: SourceCodeSnippet::new(env_var.to_string()),
|
||||
},
|
||||
slice.range(),
|
||||
);
|
||||
let node = ast::ExprStringLiteral {
|
||||
let node = ast::StringLiteral {
|
||||
value: capital_env_var,
|
||||
unicode: *unicode,
|
||||
..ast::ExprStringLiteral::default()
|
||||
unicode: env_var.is_unicode(),
|
||||
..ast::StringLiteral::default()
|
||||
};
|
||||
let new_env_var = node.into();
|
||||
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
|
||||
@@ -244,9 +246,6 @@ pub(crate) fn dict_get_with_none_default(checker: &mut Checker, expr: &Expr) {
|
||||
let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = func.as_ref() else {
|
||||
return;
|
||||
};
|
||||
if !value.is_dict_expr() {
|
||||
return;
|
||||
}
|
||||
if attr != "get" {
|
||||
return;
|
||||
}
|
||||
@@ -263,6 +262,28 @@ pub(crate) fn dict_get_with_none_default(checker: &mut Checker, expr: &Expr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the value is a dictionary.
|
||||
match value.as_ref() {
|
||||
Expr::Dict(_) | Expr::DictComp(_) => {}
|
||||
Expr::Name(name) => {
|
||||
if checker.settings.preview.is_disabled() {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(binding) = checker
|
||||
.semantic()
|
||||
.only_binding(name)
|
||||
.map(|id| checker.semantic().binding(id))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if !is_dict(binding, checker.semantic()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
_ => return,
|
||||
}
|
||||
|
||||
let expected = format!(
|
||||
"{}({})",
|
||||
checker.locator().slice(func.as_ref()),
|
||||
@@ -277,7 +298,6 @@ pub(crate) fn dict_get_with_none_default(checker: &mut Checker, expr: &Expr) {
|
||||
},
|
||||
expr.range(),
|
||||
);
|
||||
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||
expected,
|
||||
expr.range(),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use ast::Expr;
|
||||
use log::error;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Fix};
|
||||
@@ -24,6 +25,13 @@ use super::fix_with;
|
||||
/// will minimize the indentation depth of the code, making it more
|
||||
/// readable.
|
||||
///
|
||||
/// The following context managers are exempt when used as standalone
|
||||
/// statements:
|
||||
///
|
||||
/// - `anyio`.{`CancelScope`, `fail_after`, `move_on_after`}
|
||||
/// - `asyncio`.{`timeout`, `timeout_at`}
|
||||
/// - `trio`.{`fail_after`, `fail_at`, `move_on_after`, `move_on_at`}
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// with A() as a:
|
||||
@@ -73,6 +81,38 @@ fn next_with(body: &[Stmt]) -> Option<(bool, &[WithItem], &[Stmt])> {
|
||||
Some((*is_async, items, body))
|
||||
}
|
||||
|
||||
/// Check if `with_items` contains a single item which should not necessarily be
|
||||
/// grouped with other items.
|
||||
///
|
||||
/// For example:
|
||||
/// ```python
|
||||
/// async with asyncio.timeout(1):
|
||||
/// with resource1(), resource2():
|
||||
/// ...
|
||||
/// ```
|
||||
fn explicit_with_items(checker: &mut Checker, with_items: &[WithItem]) -> bool {
|
||||
let [with_item] = with_items else {
|
||||
return false;
|
||||
};
|
||||
let Expr::Call(expr_call) = &with_item.context_expr else {
|
||||
return false;
|
||||
};
|
||||
checker
|
||||
.semantic()
|
||||
.resolve_call_path(&expr_call.func)
|
||||
.is_some_and(|call_path| {
|
||||
matches!(
|
||||
call_path.as_slice(),
|
||||
["asyncio", "timeout" | "timeout_at"]
|
||||
| ["anyio", "CancelScope" | "fail_after" | "move_on_after"]
|
||||
| [
|
||||
"trio",
|
||||
"fail_after" | "fail_at" | "move_on_after" | "move_on_at"
|
||||
]
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// SIM117
|
||||
pub(crate) fn multiple_with_statements(
|
||||
checker: &mut Checker,
|
||||
@@ -111,6 +151,10 @@ pub(crate) fn multiple_with_statements(
|
||||
return;
|
||||
}
|
||||
|
||||
if explicit_with_items(checker, &with_stmt.items) || explicit_with_items(checker, items) {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(colon) = items.last().and_then(|item| {
|
||||
SimpleTokenizer::starts_at(item.end(), checker.locator().contents())
|
||||
.skip_trivia()
|
||||
|
||||
@@ -316,5 +316,28 @@ SIM117.py:126:1: SIM117 [*] Use a single `with` statement with multiple contexts
|
||||
135 134 |
|
||||
136 |- f"foo {f"bar {x}"} baz"
|
||||
135 |+ f"foo {f"bar {x}"} baz"
|
||||
137 136 |
|
||||
138 137 | # Allow cascading for some statements.
|
||||
139 138 | import anyio
|
||||
|
||||
SIM117.py:163:1: SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
|
||||
|
|
||||
162 | # Do not supress combination, if a context manager is already combined with another.
|
||||
163 | / async with asyncio.timeout(1), A():
|
||||
164 | | async with B():
|
||||
| |___________________^ SIM117
|
||||
165 | pass
|
||||
|
|
||||
= help: Combine `with` statements
|
||||
|
||||
ℹ Unsafe fix
|
||||
160 160 | pass
|
||||
161 161 |
|
||||
162 162 | # Do not supress combination, if a context manager is already combined with another.
|
||||
163 |-async with asyncio.timeout(1), A():
|
||||
164 |- async with B():
|
||||
165 |- pass
|
||||
163 |+async with asyncio.timeout(1), A(), B():
|
||||
164 |+ pass
|
||||
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user