Compare commits
100 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd7ccb4c9e | ||
|
|
ae6f38344a | ||
|
|
bbf658d4c5 | ||
|
|
1f3b0fd602 | ||
|
|
37483f3ac9 | ||
|
|
4d3a1e0581 | ||
|
|
9e5f348a17 | ||
|
|
5e91211e6d | ||
|
|
df77595426 | ||
|
|
407af6e0ae | ||
|
|
d64146683e | ||
|
|
0e7914010f | ||
|
|
cfc7d8a2b5 | ||
|
|
f5cd659292 | ||
|
|
260138b427 | ||
|
|
2da149fd7e | ||
|
|
e33887718d | ||
|
|
ba4f4f4672 | ||
|
|
b7a57ce120 | ||
|
|
82abbc7234 | ||
|
|
ba98149022 | ||
|
|
7fd44a3e12 | ||
|
|
6e8d561090 | ||
|
|
cb762f4cad | ||
|
|
eed6866b7e | ||
|
|
25a6bfa9ee | ||
|
|
b3f8f2a5c1 | ||
|
|
cc8b5a543b | ||
|
|
10d5415bcb | ||
|
|
827cbe7f97 | ||
|
|
0d84517fbc | ||
|
|
7fa1da20fb | ||
|
|
f13a161ead | ||
|
|
c4cda301aa | ||
|
|
13fda30051 | ||
|
|
a3146ab1ca | ||
|
|
c0cf87356e | ||
|
|
6c3e4ef441 | ||
|
|
6c038830a8 | ||
|
|
064a293b80 | ||
|
|
79c47e29ee | ||
|
|
be87a29a9d | ||
|
|
280dffb5e1 | ||
|
|
336993ea06 | ||
|
|
516cb10000 | ||
|
|
1cdd5e3424 | ||
|
|
bd78c6ade2 | ||
|
|
5ce35faa86 | ||
|
|
484b572e6b | ||
|
|
81805a45f0 | ||
|
|
c457752f36 | ||
|
|
289289bfd3 | ||
|
|
09274307e8 | ||
|
|
d8718dcf54 | ||
|
|
fb9eeba422 | ||
|
|
2d2630ef07 | ||
|
|
1f22e035e3 | ||
|
|
a6a7584d79 | ||
|
|
ffac4f6ec3 | ||
|
|
032a84b167 | ||
|
|
3357aaef4b | ||
|
|
d9ed43d112 | ||
|
|
e160a52bfd | ||
|
|
9067ae47d1 | ||
|
|
71e807b3be | ||
|
|
1e2df07544 | ||
|
|
860841468c | ||
|
|
ed4ecc3255 | ||
|
|
b999e4b1e2 | ||
|
|
8ce227047d | ||
|
|
523515f936 | ||
|
|
10da3bc8dd | ||
|
|
eb0dd74040 | ||
|
|
61200d2171 | ||
|
|
e8aebee3f6 | ||
|
|
210083bdd8 | ||
|
|
c33c9dc585 | ||
|
|
056c212975 | ||
|
|
381203c084 | ||
|
|
76c47a9a43 | ||
|
|
9209e57c5a | ||
|
|
333f1bd9ce | ||
|
|
002caadf9e | ||
|
|
311ba29d0f | ||
|
|
237a64d922 | ||
|
|
d4af2dd5cf | ||
|
|
a36ce585ce | ||
|
|
29ec6df24f | ||
|
|
8b17508ef1 | ||
|
|
abaf0a198d | ||
|
|
454c6d9c2f | ||
|
|
cae5503e34 | ||
|
|
34e9786a41 | ||
|
|
5467d45dfa | ||
|
|
ac87137c1c | ||
|
|
e0bccfd2d9 | ||
|
|
7b6e55a2e0 | ||
|
|
5c374b5793 | ||
|
|
ffdd0de522 | ||
|
|
5370968839 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,6 +3,7 @@
|
||||
crates/ruff/resources/test/cpython
|
||||
mkdocs.yml
|
||||
.overrides
|
||||
github_search.jsonl
|
||||
|
||||
###
|
||||
# Rust.gitignore
|
||||
|
||||
@@ -1,5 +1,40 @@
|
||||
# Breaking Changes
|
||||
|
||||
## 0.0.260
|
||||
|
||||
### Fixes are now represented as a list of edits ([#3709](https://github.com/charliermarsh/ruff/pull/3709))
|
||||
|
||||
Previously, Ruff represented each fix as a single edit, which prohibited Ruff from automatically
|
||||
fixing violations that required multiple edits across a file. As such, Ruff now represents each
|
||||
fix as a list of edits.
|
||||
|
||||
This primarily affects the JSON API. Ruff's JSON representation used to represent the `fix` field as
|
||||
a single edit, like so:
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Remove unused import: `sys`",
|
||||
"content": "",
|
||||
"location": {"row": 1, "column": 0},
|
||||
"end_location": {"row": 2, "column": 0}
|
||||
}
|
||||
```
|
||||
|
||||
The updated representation instead includes a list of edits:
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Remove unused import: `sys`",
|
||||
"edits": [
|
||||
{
|
||||
"content": "",
|
||||
"location": {"row": 1, "column": 0},
|
||||
"end_location": {"row": 2, "column": 0},
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 0.0.246
|
||||
|
||||
### `multiple-statements-on-one-line-def` (`E704`) was removed ([#2773](https://github.com/charliermarsh/ruff/pull/2773))
|
||||
|
||||
@@ -116,8 +116,7 @@ At a high level, the steps involved in adding a new lint rule are as follows:
|
||||
To define the violation, start by creating a dedicated file for your rule under the appropriate
|
||||
rule linter (e.g., `crates/ruff/src/rules/flake8_bugbear/rules/abstract_base_class.rs`). That file should
|
||||
contain a struct defined via `#[violation]`, along with a function that creates the violation
|
||||
based on any required inputs. (Many of the existing examples live in `crates/ruff/src/violations.rs`,
|
||||
but we're looking to place new rules in their own files.)
|
||||
based on any required inputs.
|
||||
|
||||
To trigger the violation, you'll likely want to augment the logic in `crates/ruff/src/checkers/ast.rs`,
|
||||
which defines the Python AST visitor, responsible for iterating over the abstract syntax tree and
|
||||
@@ -215,6 +214,20 @@ them to [PyPI](https://pypi.org/project/ruff/).
|
||||
Ruff follows the [semver](https://semver.org/) versioning standard. However, as pre-1.0 software,
|
||||
even patch releases may contain [non-backwards-compatible changes](https://semver.org/#spec-item-4).
|
||||
|
||||
## Ecosystem CI
|
||||
|
||||
GitHub Actions will run your changes against a number of real-world projects from GitHub and
|
||||
report on any diagnostic differences. You can also run those checks locally via:
|
||||
|
||||
```shell
|
||||
python scripts/check_ecosystem.py path/to/your/ruff path/to/older/ruff
|
||||
```
|
||||
|
||||
You can also run the Ecosystem CI check in a Docker container across a larger set of projects by
|
||||
downloading the [`known-github-tomls.json`](https://github.com/akx/ruff-usage-aggregate/blob/master/data/known-github-tomls.jsonl)
|
||||
as `github_search.jsonl` and following the instructions in [scripts/Dockerfile.ecosystem](scripts/Dockerfile.ecosystem).
|
||||
Note that this check will take a while to run.
|
||||
|
||||
## Benchmarks
|
||||
|
||||
First, clone [CPython](https://github.com/python/cpython). It's a large and diverse Python codebase,
|
||||
|
||||
868
Cargo.lock
generated
868
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@ members = ["crates/*"]
|
||||
|
||||
[workspace.package]
|
||||
edition = "2021"
|
||||
rust-version = "1.67"
|
||||
rust-version = "1.69"
|
||||
homepage = "https://beta.ruff.rs/docs/"
|
||||
documentation = "https://beta.ruff.rs/docs/"
|
||||
repository = "https://github.com/charliermarsh/ruff"
|
||||
@@ -11,7 +11,7 @@ authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = { version = "1.0.69" }
|
||||
bitflags = { version = "1.3.2" }
|
||||
bitflags = { version = "2.1.0" }
|
||||
chrono = { version = "0.4.23", default-features = false, features = ["clock"] }
|
||||
clap = { version = "4.1.8", features = ["derive"] }
|
||||
colored = { version = "2.0.0" }
|
||||
@@ -44,7 +44,7 @@ similar = { version = "2.2.1" }
|
||||
smallvec = { version = "1.10.0" }
|
||||
strum = { version = "0.24.1", features = ["strum_macros"] }
|
||||
strum_macros = { version = "0.24.3" }
|
||||
syn = { version = "1.0.109" }
|
||||
syn = { version = "2.0.15" }
|
||||
test-case = { version = "3.0.0" }
|
||||
textwrap = { version = "0.16.0" }
|
||||
toml = { version = "0.7.2" }
|
||||
|
||||
113
README.md
113
README.md
@@ -47,16 +47,16 @@ all while executing tens or hundreds of times faster than any individual tool.
|
||||
|
||||
Ruff is extremely actively developed and used in major open-source projects like:
|
||||
|
||||
- [pandas](https://github.com/pandas-dev/pandas)
|
||||
- [FastAPI](https://github.com/tiangolo/fastapi)
|
||||
- [Transformers (Hugging Face)](https://github.com/huggingface/transformers)
|
||||
- [Apache Airflow](https://github.com/apache/airflow)
|
||||
- [FastAPI](https://github.com/tiangolo/fastapi)
|
||||
- [Hugging Face](https://github.com/huggingface/transformers)
|
||||
- [Pandas](https://github.com/pandas-dev/pandas)
|
||||
- [SciPy](https://github.com/scipy/scipy)
|
||||
|
||||
...and many more.
|
||||
|
||||
Read the [launch blog post](https://notes.crmarsh.com/python-tooling-could-be-much-much-faster) or
|
||||
the most recent [project update](https://notes.crmarsh.com/ruff-the-first-200-releases).
|
||||
Ruff is backed by [Astral](https://astral.sh). Read the [launch post](https://astral.sh/blog/announcing-astral-the-company-behind-ruff),
|
||||
or the original [project announcement](https://notes.crmarsh.com/python-tooling-could-be-much-much-faster).
|
||||
|
||||
## Testimonials
|
||||
|
||||
@@ -137,7 +137,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com) hook:
|
||||
```yaml
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: 'v0.0.261'
|
||||
rev: 'v0.0.263'
|
||||
hooks:
|
||||
- id: ruff
|
||||
```
|
||||
@@ -332,55 +332,68 @@ Ruff is released under the MIT license.
|
||||
|
||||
## Who's Using Ruff?
|
||||
|
||||
Ruff is used in a number of major open-source projects, including:
|
||||
Ruff is used by a number of major open-source projects and companies, including:
|
||||
|
||||
- [pandas](https://github.com/pandas-dev/pandas)
|
||||
- [FastAPI](https://github.com/tiangolo/fastapi)
|
||||
- [Transformers (Hugging Face)](https://github.com/huggingface/transformers)
|
||||
- [Diffusers (Hugging Face)](https://github.com/huggingface/diffusers)
|
||||
- Amazon ([AWS SAM](https://github.com/aws/serverless-application-model))
|
||||
- [Apache Airflow](https://github.com/apache/airflow)
|
||||
- [SciPy](https://github.com/scipy/scipy)
|
||||
- [Zulip](https://github.com/zulip/zulip)
|
||||
- [Bokeh](https://github.com/bokeh/bokeh)
|
||||
- [Pydantic](https://github.com/pydantic/pydantic)
|
||||
- [PostHog](https://github.com/PostHog/posthog)
|
||||
- [Dagster](https://github.com/dagster-io/dagster)
|
||||
- [Dagger](https://github.com/dagger/dagger)
|
||||
- [Sphinx](https://github.com/sphinx-doc/sphinx)
|
||||
- [Hatch](https://github.com/pypa/hatch)
|
||||
- [PDM](https://github.com/pdm-project/pdm)
|
||||
- [Jupyter](https://github.com/jupyter-server/jupyter_server)
|
||||
- [Great Expectations](https://github.com/great-expectations/great_expectations)
|
||||
- [ONNX](https://github.com/onnx/onnx)
|
||||
- [Polars](https://github.com/pola-rs/polars)
|
||||
- [Ibis](https://github.com/ibis-project/ibis)
|
||||
- [Synapse (Matrix)](https://github.com/matrix-org/synapse)
|
||||
- [SnowCLI (Snowflake)](https://github.com/Snowflake-Labs/snowcli)
|
||||
- [Dispatch (Netflix)](https://github.com/Netflix/dispatch)
|
||||
- [Saleor](https://github.com/saleor/saleor)
|
||||
- [Pynecone](https://github.com/pynecone-io/pynecone)
|
||||
- [OpenBB](https://github.com/OpenBB-finance/OpenBBTerminal)
|
||||
- [Home Assistant](https://github.com/home-assistant/core)
|
||||
- [Pylint](https://github.com/PyCQA/pylint)
|
||||
- [Cryptography (PyCA)](https://github.com/pyca/cryptography)
|
||||
- [cibuildwheel (PyPA)](https://github.com/pypa/cibuildwheel)
|
||||
- [build (PyPA)](https://github.com/pypa/build)
|
||||
- AstraZeneca ([Magnus](https://github.com/AstraZeneca/magnus-core))
|
||||
- Benchling ([Refac](https://github.com/benchling/refac))
|
||||
- [Babel](https://github.com/python-babel/babel)
|
||||
- [featuretools](https://github.com/alteryx/featuretools)
|
||||
- [meson-python](https://github.com/mesonbuild/meson-python)
|
||||
- [ZenML](https://github.com/zenml-io/zenml)
|
||||
- [delta-rs](https://github.com/delta-io/delta-rs)
|
||||
- [Starlite](https://github.com/starlite-api/starlite)
|
||||
- [telemetry-airflow (Mozilla)](https://github.com/mozilla/telemetry-airflow)
|
||||
- [Stable Baselines3](https://github.com/DLR-RM/stable-baselines3)
|
||||
- [PaddlePaddle](https://github.com/PaddlePaddle/Paddle)
|
||||
- [nox](https://github.com/wntrblm/nox)
|
||||
- [Neon](https://github.com/neondatabase/neon)
|
||||
- [The Algorithms](https://github.com/TheAlgorithms/Python)
|
||||
- [Openverse](https://github.com/WordPress/openverse)
|
||||
- [MegaLinter](https://github.com/oxsecurity/megalinter)
|
||||
- [Bokeh](https://github.com/bokeh/bokeh)
|
||||
- [Cryptography (PyCA)](https://github.com/pyca/cryptography)
|
||||
- [Dagger](https://github.com/dagger/dagger)
|
||||
- [Dagster](https://github.com/dagster-io/dagster)
|
||||
- [DVC](https://github.com/iterative/dvc)
|
||||
- [FastAPI](https://github.com/tiangolo/fastapi)
|
||||
- [Gradio](https://github.com/gradio-app/gradio)
|
||||
- [Great Expectations](https://github.com/great-expectations/great_expectations)
|
||||
- Hugging Face ([Transformers](https://github.com/huggingface/transformers), [Datasets](https://github.com/huggingface/datasets), [Diffusers](https://github.com/huggingface/diffusers))
|
||||
- [Hatch](https://github.com/pypa/hatch)
|
||||
- [Home Assistant](https://github.com/home-assistant/core)
|
||||
- [Ibis](https://github.com/ibis-project/ibis)
|
||||
- [Jupyter](https://github.com/jupyter-server/jupyter_server)
|
||||
- [LangChain](https://github.com/hwchase17/langchain)
|
||||
- [LlamaIndex](https://github.com/jerryjliu/llama_index)
|
||||
- Matrix ([Synapse](https://github.com/matrix-org/synapse))
|
||||
- Meltano ([Meltano CLI](https://github.com/meltano/meltano), [Singer SDK](https://github.com/meltano/sdk))
|
||||
- Modern Treasury ([Python SDK](https://github.com/Modern-Treasury/modern-treasury-python-sdk))
|
||||
- Mozilla ([Firefox](https://github.com/mozilla/gecko-dev))
|
||||
- [MegaLinter](https://github.com/oxsecurity/megalinter)
|
||||
- Microsoft ([Semantic Kernel](https://github.com/microsoft/semantic-kernel), [ONNX Runtime](https://github.com/microsoft/onnxruntime))
|
||||
- Netflix ([Dispatch](https://github.com/Netflix/dispatch))
|
||||
- [Neon](https://github.com/neondatabase/neon)
|
||||
- [ONNX](https://github.com/onnx/onnx)
|
||||
- [OpenBB](https://github.com/OpenBB-finance/OpenBBTerminal)
|
||||
- [PDM](https://github.com/pdm-project/pdm)
|
||||
- [PaddlePaddle](https://github.com/PaddlePaddle/Paddle)
|
||||
- [Pandas](https://github.com/pandas-dev/pandas)
|
||||
- [Poetry](https://github.com/python-poetry/poetry)
|
||||
- [Polars](https://github.com/pola-rs/polars)
|
||||
- [PostHog](https://github.com/PostHog/posthog)
|
||||
- Prefect ([Python SDK](https://github.com/PrefectHQ/prefect), [Marvin](https://github.com/PrefectHQ/marvin))
|
||||
- [Pydantic](https://github.com/pydantic/pydantic)
|
||||
- [PyInstaller](https://github.com/pyinstaller/pyinstaller)
|
||||
- [Pylint](https://github.com/PyCQA/pylint)
|
||||
- [Pynecone](https://github.com/pynecone-io/pynecone)
|
||||
- [Robyn](https://github.com/sansyrox/robyn)
|
||||
- Scale AI ([Launch SDK](https://github.com/scaleapi/launch-python-client))
|
||||
- Snowflake ([SnowCLI](https://github.com/Snowflake-Labs/snowcli))
|
||||
- [Saleor](https://github.com/saleor/saleor)
|
||||
- [SciPy](https://github.com/scipy/scipy)
|
||||
- [Sphinx](https://github.com/sphinx-doc/sphinx)
|
||||
- [Stable Baselines3](https://github.com/DLR-RM/stable-baselines3)
|
||||
- [Starlite](https://github.com/starlite-api/starlite)
|
||||
- [The Algorithms](https://github.com/TheAlgorithms/Python)
|
||||
- [Vega-Altair](https://github.com/altair-viz/altair)
|
||||
- WordPress ([Openverse](https://github.com/WordPress/openverse))
|
||||
- [ZenML](https://github.com/zenml-io/zenml)
|
||||
- [Zulip](https://github.com/zulip/zulip)
|
||||
- [build (PyPA)](https://github.com/pypa/build)
|
||||
- [cibuildwheel (PyPA)](https://github.com/pypa/cibuildwheel)
|
||||
- [delta-rs](https://github.com/delta-io/delta-rs)
|
||||
- [featuretools](https://github.com/alteryx/featuretools)
|
||||
- [meson-python](https://github.com/mesonbuild/meson-python)
|
||||
- [nox](https://github.com/wntrblm/nox)
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -5,3 +5,4 @@ extend-exclude = ["snapshots", "black"]
|
||||
trivias = "trivias"
|
||||
hel = "hel"
|
||||
whos = "whos"
|
||||
spawnve = "spawnve"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "flake8-to-ruff"
|
||||
version = "0.0.261"
|
||||
version = "0.0.263"
|
||||
edition = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.0.261"
|
||||
version = "0.0.263"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
@@ -21,13 +21,15 @@ ruff_python_ast = { path = "../ruff_python_ast" }
|
||||
ruff_python_semantic = { path = "../ruff_python_semantic" }
|
||||
ruff_python_stdlib = { path = "../ruff_python_stdlib" }
|
||||
ruff_rustpython = { path = "../ruff_rustpython" }
|
||||
ruff_text_size = { path = "../ruff_text_size" }
|
||||
|
||||
annotate-snippets = { version = "0.9.1", features = ["color"] }
|
||||
anyhow = { workspace = true }
|
||||
bitflags = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
clap = { workspace = true, features = ["derive", "string"], optional = true }
|
||||
colored = { workspace = true }
|
||||
dirs = { version = "4.0.0" }
|
||||
dirs = { version = "5.0.0" }
|
||||
fern = { version = "0.6.1" }
|
||||
glob = { workspace = true }
|
||||
globset = { workspace = true }
|
||||
@@ -48,6 +50,7 @@ path-absolutize = { workspace = true, features = [
|
||||
] }
|
||||
pathdiff = { version = "0.2.1" }
|
||||
pep440_rs = { version = "0.3.1", features = ["serde"] }
|
||||
quick-junit = { version = "0.3.2" }
|
||||
regex = { workspace = true }
|
||||
result-like = { version = "0.4.6" }
|
||||
rustc-hash = { workspace = true }
|
||||
@@ -57,6 +60,7 @@ schemars = { workspace = true }
|
||||
semver = { version = "1.0.16" }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
similar = { workspace = true, features = ["inline"] }
|
||||
shellexpand = { workspace = true }
|
||||
smallvec = { workspace = true }
|
||||
strum = { workspace = true }
|
||||
@@ -68,9 +72,11 @@ typed-arena = { version = "2.0.2" }
|
||||
unicode-width = { version = "0.1.10" }
|
||||
|
||||
[dev-dependencies]
|
||||
insta = { workspace = true, features = ["yaml", "redactions"] }
|
||||
insta = { workspace = true }
|
||||
pretty_assertions = "1.3.0"
|
||||
test-case = { workspace = true }
|
||||
# Disable colored output in tests
|
||||
colored = { workspace = true, features = ["no-color"] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
@@ -9,6 +9,7 @@ def foo(x, y, z):
|
||||
print(x, y, z)
|
||||
|
||||
# This is a real comment.
|
||||
# # This is a (nested) comment.
|
||||
#return True
|
||||
return False
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
# Error
|
||||
assert True
|
||||
assert True # S101
|
||||
|
||||
|
||||
def fn():
|
||||
x = 1
|
||||
assert x == 1 # S101
|
||||
assert x == 2 # S101
|
||||
|
||||
# Error
|
||||
assert x == 1
|
||||
|
||||
# Error
|
||||
assert x == 2
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert True # OK
|
||||
|
||||
20
crates/ruff/resources/test/fixtures/flake8_bandit/S602.py
vendored
Normal file
20
crates/ruff/resources/test/fixtures/flake8_bandit/S602.py
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
from subprocess import Popen, call, check_call, check_output, run
|
||||
|
||||
# Check different Popen wrappers are checked.
|
||||
Popen("true", shell=True)
|
||||
call("true", shell=True)
|
||||
check_call("true", shell=True)
|
||||
check_output("true", shell=True)
|
||||
run("true", shell=True)
|
||||
|
||||
# Check values that truthy values are treated as true.
|
||||
Popen("true", shell=1)
|
||||
Popen("true", shell=[1])
|
||||
Popen("true", shell={1: 1})
|
||||
Popen("true", shell=(1,))
|
||||
|
||||
# Check command argument looks unsafe.
|
||||
var_string = "true"
|
||||
Popen(var_string, shell=True)
|
||||
Popen([var_string], shell=True)
|
||||
Popen([var_string, ""], shell=True)
|
||||
20
crates/ruff/resources/test/fixtures/flake8_bandit/S603.py
vendored
Normal file
20
crates/ruff/resources/test/fixtures/flake8_bandit/S603.py
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
from subprocess import Popen, call, check_call, check_output, run
|
||||
|
||||
# Different Popen wrappers are checked.
|
||||
Popen("true", shell=False)
|
||||
call("true", shell=False)
|
||||
check_call("true", shell=False)
|
||||
check_output("true", shell=False)
|
||||
run("true", shell=False)
|
||||
|
||||
# Values that falsey values are treated as false.
|
||||
Popen("true", shell=0)
|
||||
Popen("true", shell=[])
|
||||
Popen("true", shell={})
|
||||
Popen("true", shell=None)
|
||||
|
||||
# Unknown values are treated as falsey.
|
||||
Popen("true", shell=True if True else False)
|
||||
|
||||
# No value is also caught.
|
||||
Popen("true")
|
||||
5
crates/ruff/resources/test/fixtures/flake8_bandit/S604.py
vendored
Normal file
5
crates/ruff/resources/test/fixtures/flake8_bandit/S604.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
def foo(shell):
|
||||
pass
|
||||
|
||||
|
||||
foo(shell=True)
|
||||
25
crates/ruff/resources/test/fixtures/flake8_bandit/S605.py
vendored
Normal file
25
crates/ruff/resources/test/fixtures/flake8_bandit/S605.py
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
import os
|
||||
|
||||
import commands
|
||||
import popen2
|
||||
|
||||
# Check all shell functions.
|
||||
os.system("true")
|
||||
os.popen("true")
|
||||
os.popen2("true")
|
||||
os.popen3("true")
|
||||
os.popen4("true")
|
||||
popen2.popen2("true")
|
||||
popen2.popen3("true")
|
||||
popen2.popen4("true")
|
||||
popen2.Popen3("true")
|
||||
popen2.Popen4("true")
|
||||
commands.getoutput("true")
|
||||
commands.getstatusoutput("true")
|
||||
|
||||
|
||||
# Check command argument looks unsafe.
|
||||
var_string = "true"
|
||||
os.system(var_string)
|
||||
os.system([var_string])
|
||||
os.system([var_string, ""])
|
||||
20
crates/ruff/resources/test/fixtures/flake8_bandit/S606.py
vendored
Normal file
20
crates/ruff/resources/test/fixtures/flake8_bandit/S606.py
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
import os
|
||||
|
||||
# Check all shell functions.
|
||||
os.execl("true")
|
||||
os.execle("true")
|
||||
os.execlp("true")
|
||||
os.execlpe("true")
|
||||
os.execv("true")
|
||||
os.execve("true")
|
||||
os.execvp("true")
|
||||
os.execvpe("true")
|
||||
os.spawnl("true")
|
||||
os.spawnle("true")
|
||||
os.spawnlp("true")
|
||||
os.spawnlpe("true")
|
||||
os.spawnv("true")
|
||||
os.spawnve("true")
|
||||
os.spawnvp("true")
|
||||
os.spawnvpe("true")
|
||||
os.startfile("true")
|
||||
44
crates/ruff/resources/test/fixtures/flake8_bandit/S607.py
vendored
Normal file
44
crates/ruff/resources/test/fixtures/flake8_bandit/S607.py
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
import os
|
||||
|
||||
# Check all functions.
|
||||
subprocess.Popen("true")
|
||||
subprocess.call("true")
|
||||
subprocess.check_call("true")
|
||||
subprocess.check_output("true")
|
||||
subprocess.run("true")
|
||||
os.system("true")
|
||||
os.popen("true")
|
||||
os.popen2("true")
|
||||
os.popen3("true")
|
||||
os.popen4("true")
|
||||
popen2.popen2("true")
|
||||
popen2.popen3("true")
|
||||
popen2.popen4("true")
|
||||
popen2.Popen3("true")
|
||||
popen2.Popen4("true")
|
||||
commands.getoutput("true")
|
||||
commands.getstatusoutput("true")
|
||||
os.execl("true")
|
||||
os.execle("true")
|
||||
os.execlp("true")
|
||||
os.execlpe("true")
|
||||
os.execv("true")
|
||||
os.execve("true")
|
||||
os.execvp("true")
|
||||
os.execvpe("true")
|
||||
os.spawnl("true")
|
||||
os.spawnle("true")
|
||||
os.spawnlp("true")
|
||||
os.spawnlpe("true")
|
||||
os.spawnv("true")
|
||||
os.spawnve("true")
|
||||
os.spawnvp("true")
|
||||
os.spawnvpe("true")
|
||||
os.startfile("true")
|
||||
|
||||
# Check it does not fail for full paths.
|
||||
os.system("/bin/ls")
|
||||
os.system("./bin/ls")
|
||||
os.system(["/bin/ls"])
|
||||
os.system(["/bin/ls", "/tmp"])
|
||||
os.system(r"C:\\bin\ls")
|
||||
@@ -1,9 +1,10 @@
|
||||
"""
|
||||
Should emit:
|
||||
B017 - on lines 20
|
||||
B017 - on lines 23 and 41
|
||||
"""
|
||||
import asyncio
|
||||
import unittest
|
||||
import pytest
|
||||
|
||||
CONSTANT = True
|
||||
|
||||
@@ -34,3 +35,14 @@ class Foobar(unittest.TestCase):
|
||||
def raises_with_absolute_reference(self):
|
||||
with self.assertRaises(asyncio.CancelledError):
|
||||
Foo()
|
||||
|
||||
|
||||
def test_pytest_raises():
|
||||
with pytest.raises(Exception):
|
||||
raise ValueError("Hello")
|
||||
|
||||
with pytest.raises(Exception, "hello"):
|
||||
raise ValueError("This is fine")
|
||||
|
||||
with pytest.raises(Exception, match="hello"):
|
||||
raise ValueError("This is also fine")
|
||||
|
||||
@@ -78,6 +78,21 @@ for _section, section_items in itertools.groupby(items, key=lambda p: p[1]):
|
||||
for shopper in shoppers:
|
||||
collect_shop_items(shopper, section_items) # B031
|
||||
|
||||
for _section, section_items in itertools.groupby(items, key=lambda p: p[1]):
|
||||
_ = [collect_shop_items(shopper, section_items) for shopper in shoppers] # B031
|
||||
|
||||
for _section, section_items in itertools.groupby(items, key=lambda p: p[1]):
|
||||
# The variable is overridden, skip checking.
|
||||
_ = [_ for section_items in range(3)]
|
||||
_ = [collect_shop_items(shopper, section_items) for shopper in shoppers]
|
||||
|
||||
for _section, section_items in itertools.groupby(items, key=lambda p: p[1]):
|
||||
_ = [item for item in section_items]
|
||||
|
||||
for _section, section_items in itertools.groupby(items, key=lambda p: p[1]):
|
||||
# The iterator is being used for the second time.
|
||||
_ = [(item1, item2) for item1 in section_items for item2 in section_items] # B031
|
||||
|
||||
for _section, section_items in itertools.groupby(items, key=lambda p: p[1]):
|
||||
if _section == "greens":
|
||||
collect_shop_items(shopper, section_items)
|
||||
@@ -134,6 +149,16 @@ for group in groupby(items, key=lambda p: p[1]):
|
||||
collect_shop_items("Joe", group[1])
|
||||
|
||||
|
||||
# https://github.com/charliermarsh/ruff/issues/4050
|
||||
for _section, section_items in itertools.groupby(items, key=lambda p: p[1]):
|
||||
if _section == "greens":
|
||||
for item in section_items:
|
||||
collect_shop_items(shopper, item)
|
||||
elif _section == "frozen items":
|
||||
_ = [item for item in section_items]
|
||||
else:
|
||||
collect_shop_items(shopper, section_items)
|
||||
|
||||
# Make sure we ignore - but don't fail on more complicated invocations
|
||||
for _key, (_value1, _value2) in groupby(
|
||||
[("a", (1, 2)), ("b", (3, 4)), ("a", (5, 6))], key=lambda p: p[1]
|
||||
|
||||
@@ -7,11 +7,14 @@ set(set(x))
|
||||
set(list(x))
|
||||
set(tuple(x))
|
||||
set(sorted(x))
|
||||
set(sorted(x, key=lambda y: y))
|
||||
set(reversed(x))
|
||||
sorted(list(x))
|
||||
sorted(tuple(x))
|
||||
sorted(sorted(x))
|
||||
sorted(sorted(x, key=lambda y: y))
|
||||
sorted(reversed(x))
|
||||
sorted(list(x), key=lambda y: y)
|
||||
tuple(
|
||||
list(
|
||||
[x, 3, "hell"\
|
||||
|
||||
10
crates/ruff/resources/test/fixtures/flake8_comprehensions/C418.py
vendored
Normal file
10
crates/ruff/resources/test/fixtures/flake8_comprehensions/C418.py
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
dict({})
|
||||
dict({'a': 1})
|
||||
dict({'x': 1 for x in range(10)})
|
||||
dict(
|
||||
{'x': 1 for x in range(10)}
|
||||
)
|
||||
|
||||
dict({}, a=1)
|
||||
dict({x: 1 for x in range(1)}, a=1)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# PIE802
|
||||
any([x.id for x in bar])
|
||||
all([x.id for x in bar])
|
||||
any( # first comment
|
||||
@@ -15,5 +14,6 @@ all(x.id for x in bar)
|
||||
any(x.id for x in bar)
|
||||
all((x.id for x in bar))
|
||||
|
||||
|
||||
async def f() -> bool:
|
||||
return all([await use_greeting(greeting) for greeting in await greetings()])
|
||||
16
crates/ruff/resources/test/fixtures/flake8_import_conventions/custom_banned.py
vendored
Normal file
16
crates/ruff/resources/test/fixtures/flake8_import_conventions/custom_banned.py
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
import typing as t # banned
|
||||
import typing as ty # banned
|
||||
|
||||
import numpy as nmp # banned
|
||||
import numpy as npy # banned
|
||||
import tensorflow.keras.backend as K # banned
|
||||
import torch.nn.functional as F # banned
|
||||
from tensorflow.keras import backend as K # banned
|
||||
from torch.nn import functional as F # banned
|
||||
|
||||
from typing import Any # ok
|
||||
|
||||
import numpy as np # ok
|
||||
import tensorflow as tf # ok
|
||||
import torch.nn as nn # ok
|
||||
from tensorflow.keras import backend # ok
|
||||
10
crates/ruff/resources/test/fixtures/flake8_import_conventions/custom_banned_from.py
vendored
Normal file
10
crates/ruff/resources/test/fixtures/flake8_import_conventions/custom_banned_from.py
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
from logging.config import BaseConfigurator # banned
|
||||
from typing import Any, Dict # banned
|
||||
from typing import * # banned
|
||||
|
||||
from pandas import DataFrame # banned
|
||||
from pandas import * # banned
|
||||
|
||||
import logging.config # ok
|
||||
import typing # ok
|
||||
import pandas # ok
|
||||
@@ -11,3 +11,7 @@ _T = TypeVar("_T") # OK
|
||||
_TTuple = TypeVarTuple("_TTuple") # OK
|
||||
|
||||
_P = ParamSpec("_P") # OK
|
||||
|
||||
|
||||
def f():
|
||||
T = TypeVar("T") # OK
|
||||
|
||||
@@ -11,3 +11,6 @@ _T = TypeVar("_T") # OK
|
||||
_TTuple = TypeVarTuple("_TTuple") # OK
|
||||
|
||||
_P = ParamSpec("_P") # OK
|
||||
|
||||
def f():
|
||||
T = TypeVar("T") # OK
|
||||
|
||||
@@ -46,3 +46,48 @@ field229: dict[int, int] = {1: 2, **{3: 4}} # Y015 Only simple default values a
|
||||
field23 = "foo" + "bar" # Y015 Only simple default values are allowed for assignments
|
||||
field24 = b"foo" + b"bar" # Y015 Only simple default values are allowed for assignments
|
||||
field25 = 5 * 5 # Y015 Only simple default values are allowed for assignments
|
||||
|
||||
# We shouldn't emit Y015 within functions
|
||||
def f():
|
||||
field26: list[int] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
|
||||
|
||||
|
||||
# We shouldn't emit Y015 for __slots__ or __match_args__
|
||||
class Class1:
|
||||
__slots__ = (
|
||||
'_one',
|
||||
'_two',
|
||||
'_three',
|
||||
'_four',
|
||||
'_five',
|
||||
'_six',
|
||||
'_seven',
|
||||
'_eight',
|
||||
'_nine',
|
||||
'_ten',
|
||||
'_eleven',
|
||||
)
|
||||
|
||||
__match_args__ = (
|
||||
'one',
|
||||
'two',
|
||||
'three',
|
||||
'four',
|
||||
'five',
|
||||
'six',
|
||||
'seven',
|
||||
'eight',
|
||||
'nine',
|
||||
'ten',
|
||||
'eleven',
|
||||
)
|
||||
|
||||
# We shouldn't emit Y015 for __all__
|
||||
__all__ = ["Class1"]
|
||||
|
||||
# Ignore the following for PYI015
|
||||
field26 = typing.Sequence[int]
|
||||
field27 = list[str]
|
||||
field28 = builtins.str
|
||||
field29 = str
|
||||
field30 = str | bytes | None
|
||||
|
||||
@@ -53,3 +53,48 @@ field229: dict[int, int] = {1: 2, **{3: 4}} # Y015 Only simple default values a
|
||||
field23 = "foo" + "bar" # Y015 Only simple default values are allowed for assignments
|
||||
field24 = b"foo" + b"bar" # Y015 Only simple default values are allowed for assignments
|
||||
field25 = 5 * 5 # Y015 Only simple default values are allowed for assignments
|
||||
|
||||
# We shouldn't emit Y015 within functions
|
||||
def f():
|
||||
field26: list[int] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
|
||||
|
||||
|
||||
# We shouldn't emit Y015 for __slots__ or __match_args__
|
||||
class Class1:
|
||||
__slots__ = (
|
||||
'_one',
|
||||
'_two',
|
||||
'_three',
|
||||
'_four',
|
||||
'_five',
|
||||
'_six',
|
||||
'_seven',
|
||||
'_eight',
|
||||
'_nine',
|
||||
'_ten',
|
||||
'_eleven',
|
||||
)
|
||||
|
||||
__match_args__ = (
|
||||
'one',
|
||||
'two',
|
||||
'three',
|
||||
'four',
|
||||
'five',
|
||||
'six',
|
||||
'seven',
|
||||
'eight',
|
||||
'nine',
|
||||
'ten',
|
||||
'eleven',
|
||||
)
|
||||
|
||||
# We shouldn't emit Y015 for __all__
|
||||
__all__ = ["Class1"]
|
||||
|
||||
# Ignore the following for PYI015
|
||||
field26 = typing.Sequence[int]
|
||||
field27 = list[str]
|
||||
field28 = builtins.str
|
||||
field29 = str
|
||||
field30 = str | bytes | None
|
||||
|
||||
35
crates/ruff/resources/test/fixtures/flake8_pyi/PYI016.py
vendored
Normal file
35
crates/ruff/resources/test/fixtures/flake8_pyi/PYI016.py
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
# Shouldn't affect non-union field types.
|
||||
field1: str
|
||||
|
||||
# Should emit for duplicate field types.
|
||||
field2: str | str # PYI016: Duplicate union member `str`
|
||||
|
||||
|
||||
# Should emit for union types in arguments.
|
||||
def func1(arg1: int | int): # PYI016: Duplicate union member `int`
|
||||
print(arg1)
|
||||
|
||||
|
||||
# Should emit for unions in return types.
|
||||
def func2() -> str | str: # PYI016: Duplicate union member `str`
|
||||
return "my string"
|
||||
|
||||
|
||||
# Should emit in longer unions, even if not directly adjacent.
|
||||
field3: str | str | int # PYI016: Duplicate union member `str`
|
||||
field4: int | int | str # PYI016: Duplicate union member `int`
|
||||
field5: str | int | str # PYI016: Duplicate union member `str`
|
||||
field6: int | bool | str | int # PYI016: Duplicate union member `int`
|
||||
|
||||
# Shouldn't emit for non-type unions.
|
||||
field7 = str | str
|
||||
|
||||
# Should emit for strangely-bracketed unions.
|
||||
field8: int | (str | int) # PYI016: Duplicate union member `int`
|
||||
|
||||
# Should handle user brackets when fixing.
|
||||
field9: int | (int | str) # PYI016: Duplicate union member `int`
|
||||
field10: (str | int) | str # PYI016: Duplicate union member `str`
|
||||
|
||||
# Should emit for nested unions.
|
||||
field11: dict[int | int, str]
|
||||
32
crates/ruff/resources/test/fixtures/flake8_pyi/PYI016.pyi
vendored
Normal file
32
crates/ruff/resources/test/fixtures/flake8_pyi/PYI016.pyi
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
# Shouldn't affect non-union field types.
|
||||
field1: str
|
||||
|
||||
# Should emit for duplicate field types.
|
||||
field2: str | str # PYI016: Duplicate union member `str`
|
||||
|
||||
# Should emit for union types in arguments.
|
||||
def func1(arg1: int | int): # PYI016: Duplicate union member `int`
|
||||
print(arg1)
|
||||
|
||||
# Should emit for unions in return types.
|
||||
def func2() -> str | str: # PYI016: Duplicate union member `str`
|
||||
return "my string"
|
||||
|
||||
# Should emit in longer unions, even if not directly adjacent.
|
||||
field3: str | str | int # PYI016: Duplicate union member `str`
|
||||
field4: int | int | str # PYI016: Duplicate union member `int`
|
||||
field5: str | int | str # PYI016: Duplicate union member `str`
|
||||
field6: int | bool | str | int # PYI016: Duplicate union member `int`
|
||||
|
||||
# Shouldn't emit for non-type unions.
|
||||
field7 = str | str
|
||||
|
||||
# Should emit for strangely-bracketed unions.
|
||||
field8: int | (str | int) # PYI016: Duplicate union member `int`
|
||||
|
||||
# Should handle user brackets when fixing.
|
||||
field9: int | (int | str) # PYI016: Duplicate union member `int`
|
||||
field10: (str | int) | str # PYI016: Duplicate union member `str`
|
||||
|
||||
# Should emit for nested unions.
|
||||
field11: dict[int | int, str]
|
||||
@@ -49,3 +49,18 @@ def test_list_expressions(param1, param2):
|
||||
@pytest.mark.parametrize([some_expr, "param2"], [1, 2, 3])
|
||||
def test_list_mixed_expr_literal(param1, param2):
|
||||
...
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("param1, " "param2, " "param3"), [(1, 2, 3), (4, 5, 6)])
|
||||
def test_implicit_str_concat_with_parens(param1, param2, param3):
|
||||
...
|
||||
|
||||
|
||||
@pytest.mark.parametrize("param1, " "param2, " "param3", [(1, 2, 3), (4, 5, 6)])
|
||||
def test_implicit_str_concat_no_parens(param1, param2, param3):
|
||||
...
|
||||
|
||||
|
||||
@pytest.mark.parametrize((("param1, " "param2, " "param3")), [(1, 2, 3), (4, 5, 6)])
|
||||
def test_implicit_str_concat_with_multi_parens(param1, param2, param3):
|
||||
...
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
###
|
||||
def x():
|
||||
a = 1
|
||||
return a # error
|
||||
return a # RET504
|
||||
|
||||
|
||||
# Can be refactored false positives
|
||||
@@ -211,10 +211,10 @@ def nonlocal_assignment():
|
||||
def decorator() -> Flask:
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route('/hello')
|
||||
@app.route("/hello")
|
||||
def hello() -> str:
|
||||
"""Hello endpoint."""
|
||||
return 'Hello, World!'
|
||||
return "Hello, World!"
|
||||
|
||||
return app
|
||||
|
||||
@@ -222,12 +222,13 @@ def decorator() -> Flask:
|
||||
def default():
|
||||
y = 1
|
||||
|
||||
def f(x = y) -> X:
|
||||
def f(x=y) -> X:
|
||||
return x
|
||||
|
||||
return y
|
||||
|
||||
|
||||
# Multiple assignment
|
||||
def get_queryset(option_1, option_2):
|
||||
queryset: Any = None
|
||||
queryset = queryset.filter(a=1)
|
||||
@@ -246,4 +247,28 @@ def get_queryset():
|
||||
|
||||
def get_queryset():
|
||||
queryset = Model.filter(a=1)
|
||||
return queryset # error
|
||||
return queryset # RET504
|
||||
|
||||
|
||||
# Function arguments
|
||||
def str_to_bool(val):
|
||||
if isinstance(val, bool):
|
||||
return val
|
||||
val = val.strip().lower()
|
||||
if val in ("1", "true", "yes"):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def str_to_bool(val):
|
||||
if isinstance(val, bool):
|
||||
return val
|
||||
val = 1
|
||||
return val # RET504
|
||||
|
||||
|
||||
def str_to_bool(val):
|
||||
if isinstance(val, bool):
|
||||
return some_obj
|
||||
return val
|
||||
|
||||
@@ -59,3 +59,15 @@ def bar():
|
||||
return foo()
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def with_ellipsis():
|
||||
try:
|
||||
foo()
|
||||
except ValueError:
|
||||
...
|
||||
|
||||
def with_ellipsis_and_return():
|
||||
try:
|
||||
return foo()
|
||||
except ValueError:
|
||||
...
|
||||
|
||||
@@ -42,3 +42,113 @@ if False and f() and a and g() and b: # OK
|
||||
|
||||
if a and False and f() and b and g(): # OK
|
||||
pass
|
||||
|
||||
|
||||
a or "" or True # SIM222
|
||||
|
||||
a or "foo" or True or "bar" # SIM222
|
||||
|
||||
a or 0 or True # SIM222
|
||||
|
||||
a or 1 or True or 2 # SIM222
|
||||
|
||||
a or 0.0 or True # SIM222
|
||||
|
||||
a or 0.1 or True or 0.2 # SIM222
|
||||
|
||||
a or [] or True # SIM222
|
||||
|
||||
a or list([]) or True # SIM222
|
||||
|
||||
a or [1] or True or [2] # SIM222
|
||||
|
||||
a or list([1]) or True or list([2]) # SIM222
|
||||
|
||||
a or {} or True # SIM222
|
||||
|
||||
a or dict() or True # SIM222
|
||||
|
||||
a or {1: 1} or True or {2: 2} # SIM222
|
||||
|
||||
a or dict({1: 1}) or True or dict({2: 2}) # SIM222
|
||||
|
||||
a or set() or True # SIM222
|
||||
|
||||
a or set(set()) or True # SIM222
|
||||
|
||||
a or {1} or True or {2} # SIM222
|
||||
|
||||
a or set({1}) or True or set({2}) # SIM222
|
||||
|
||||
a or () or True # SIM222
|
||||
|
||||
a or tuple(()) or True # SIM222
|
||||
|
||||
a or (1,) or True or (2,) # SIM222
|
||||
|
||||
a or tuple((1,)) or True or tuple((2,)) # SIM222
|
||||
|
||||
a or frozenset() or True # SIM222
|
||||
|
||||
a or frozenset(frozenset()) or True # SIM222
|
||||
|
||||
a or frozenset({1}) or True or frozenset({2}) # SIM222
|
||||
|
||||
a or frozenset(frozenset({1})) or True or frozenset(frozenset({2})) # SIM222
|
||||
|
||||
|
||||
# Inside test `a` is simplified.
|
||||
|
||||
bool(a or [1] or True or [2]) # SIM222
|
||||
|
||||
assert a or [1] or True or [2] # SIM222
|
||||
|
||||
if (a or [1] or True or [2]) and (a or [1] or True or [2]): # SIM222
|
||||
pass
|
||||
|
||||
0 if a or [1] or True or [2] else 1 # SIM222
|
||||
|
||||
while a or [1] or True or [2]: # SIM222
|
||||
pass
|
||||
|
||||
[
|
||||
0
|
||||
for a in range(10)
|
||||
for b in range(10)
|
||||
if a or [1] or True or [2] # SIM222
|
||||
if b or [1] or True or [2] # SIM222
|
||||
]
|
||||
|
||||
{
|
||||
0
|
||||
for a in range(10)
|
||||
for b in range(10)
|
||||
if a or [1] or True or [2] # SIM222
|
||||
if b or [1] or True or [2] # SIM222
|
||||
}
|
||||
|
||||
{
|
||||
0: 0
|
||||
for a in range(10)
|
||||
for b in range(10)
|
||||
if a or [1] or True or [2] # SIM222
|
||||
if b or [1] or True or [2] # SIM222
|
||||
}
|
||||
|
||||
(
|
||||
0
|
||||
for a in range(10)
|
||||
for b in range(10)
|
||||
if a or [1] or True or [2] # SIM222
|
||||
if b or [1] or True or [2] # SIM222
|
||||
)
|
||||
|
||||
# Outside test `a` is not simplified.
|
||||
|
||||
a or [1] or True or [2] # SIM222
|
||||
|
||||
if (a or [1] or True or [2]) == (a or [1]): # SIM222
|
||||
pass
|
||||
|
||||
if f(a or [1] or True or [2]): # SIM222
|
||||
pass
|
||||
|
||||
@@ -37,3 +37,113 @@ if True or f() or a or g() or b: # OK
|
||||
|
||||
if a or True or f() or b or g(): # OK
|
||||
pass
|
||||
|
||||
|
||||
a and "" and False # SIM223
|
||||
|
||||
a and "foo" and False and "bar" # SIM223
|
||||
|
||||
a and 0 and False # SIM223
|
||||
|
||||
a and 1 and False and 2 # SIM223
|
||||
|
||||
a and 0.0 and False # SIM223
|
||||
|
||||
a and 0.1 and False and 0.2 # SIM223
|
||||
|
||||
a and [] and False # SIM223
|
||||
|
||||
a and list([]) and False # SIM223
|
||||
|
||||
a and [1] and False and [2] # SIM223
|
||||
|
||||
a and list([1]) and False and list([2]) # SIM223
|
||||
|
||||
a and {} and False # SIM223
|
||||
|
||||
a and dict() and False # SIM223
|
||||
|
||||
a and {1: 1} and False and {2: 2} # SIM223
|
||||
|
||||
a and dict({1: 1}) and False and dict({2: 2}) # SIM223
|
||||
|
||||
a and set() and False # SIM223
|
||||
|
||||
a and set(set()) and False # SIM223
|
||||
|
||||
a and {1} and False and {2} # SIM223
|
||||
|
||||
a and set({1}) and False and set({2}) # SIM223
|
||||
|
||||
a and () and False # SIM222
|
||||
|
||||
a and tuple(()) and False # SIM222
|
||||
|
||||
a and (1,) and False and (2,) # SIM222
|
||||
|
||||
a and tuple((1,)) and False and tuple((2,)) # SIM222
|
||||
|
||||
a and frozenset() and False # SIM222
|
||||
|
||||
a and frozenset(frozenset()) and False # SIM222
|
||||
|
||||
a and frozenset({1}) and False and frozenset({2}) # SIM222
|
||||
|
||||
a and frozenset(frozenset({1})) and False and frozenset(frozenset({2})) # SIM222
|
||||
|
||||
|
||||
# Inside test `a` is simplified.
|
||||
|
||||
bool(a and [] and False and []) # SIM223
|
||||
|
||||
assert a and [] and False and [] # SIM223
|
||||
|
||||
if (a and [] and False and []) or (a and [] and False and []): # SIM223
|
||||
pass
|
||||
|
||||
0 if a and [] and False and [] else 1 # SIM222
|
||||
|
||||
while a and [] and False and []: # SIM223
|
||||
pass
|
||||
|
||||
[
|
||||
0
|
||||
for a in range(10)
|
||||
for b in range(10)
|
||||
if a and [] and False and [] # SIM223
|
||||
if b and [] and False and [] # SIM223
|
||||
]
|
||||
|
||||
{
|
||||
0
|
||||
for a in range(10)
|
||||
for b in range(10)
|
||||
if a and [] and False and [] # SIM223
|
||||
if b and [] and False and [] # SIM223
|
||||
}
|
||||
|
||||
{
|
||||
0: 0
|
||||
for a in range(10)
|
||||
for b in range(10)
|
||||
if a and [] and False and [] # SIM223
|
||||
if b and [] and False and [] # SIM223
|
||||
}
|
||||
|
||||
(
|
||||
0
|
||||
for a in range(10)
|
||||
for b in range(10)
|
||||
if a and [] and False and [] # SIM223
|
||||
if b and [] and False and [] # SIM223
|
||||
)
|
||||
|
||||
# Outside test `a` is not simplified.
|
||||
|
||||
a and [] and False and [] # SIM223
|
||||
|
||||
if (a and [] and False and []) == (a and []): # SIM223
|
||||
pass
|
||||
|
||||
if f(a and [] and False and []): # SIM223
|
||||
pass
|
||||
|
||||
@@ -31,3 +31,6 @@ typing.TypedDict.anything()
|
||||
# import aliases are resolved
|
||||
import typing as totally_not_typing
|
||||
totally_not_typing.TypedDict
|
||||
|
||||
# relative imports are respected
|
||||
from .typing import TypedDict
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
# module members cannot be imported with that syntax
|
||||
import typing.TypedDict
|
||||
|
||||
# we don't track reassignments
|
||||
import typing, other
|
||||
typing = other
|
||||
typing.TypedDict()
|
||||
|
||||
# yet another false positive
|
||||
def foo(typing):
|
||||
typing.TypedDict()
|
||||
@@ -1,3 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def f():
|
||||
# Even in strict mode, this shouldn't rase an error, since `pkg` is used at runtime,
|
||||
# and implicitly imports `pkg.bar`.
|
||||
|
||||
4
crates/ruff/resources/test/fixtures/isort/propagate_inline_comments.py
vendored
Normal file
4
crates/ruff/resources/test/fixtures/isort/propagate_inline_comments.py
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
from mypackage.subpackage import ( # long comment that seems to be a problem
|
||||
a_long_variable_name_that_causes_problems,
|
||||
items,
|
||||
)
|
||||
3
crates/ruff/resources/test/fixtures/isort/required_imports/docstring.pyi
vendored
Normal file
3
crates/ruff/resources/test/fixtures/isort/required_imports/docstring.pyi
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
"""Hello, world!"""
|
||||
|
||||
x = 1
|
||||
7
crates/ruff/resources/test/fixtures/isort/sections.py
vendored
Normal file
7
crates/ruff/resources/test/fixtures/isort/sections.py
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
from __future__ import annotations
|
||||
import os
|
||||
import sys
|
||||
import pytz
|
||||
import django.settings
|
||||
from library import foo
|
||||
from . import local
|
||||
@@ -39,3 +39,11 @@ class Test(unittest.TestCase):
|
||||
|
||||
def testTest(self):
|
||||
assert True
|
||||
|
||||
|
||||
from typing import override
|
||||
|
||||
|
||||
@override
|
||||
def BAD_FUNC():
|
||||
pass
|
||||
|
||||
@@ -13,3 +13,11 @@ class C:
|
||||
myObj2 = namedtuple("MyObj2", ["a", "b"])
|
||||
Employee = NamedTuple('Employee', [('name', str), ('id', int)])
|
||||
Point2D = TypedDict('Point2D', {'in': int, 'x-y': int})
|
||||
|
||||
|
||||
class D(TypedDict):
|
||||
lower: int
|
||||
CONSTANT: str
|
||||
mixedCase: bool
|
||||
_mixedCase: list
|
||||
mixed_Case: set
|
||||
|
||||
@@ -13,6 +13,7 @@ f = lambda: (yield from g())
|
||||
class F:
|
||||
f = lambda x: 2 * x
|
||||
|
||||
|
||||
f = object()
|
||||
f.method = lambda: "Method"
|
||||
f = {}
|
||||
@@ -21,3 +22,30 @@ f = []
|
||||
f.append(lambda x: x**2)
|
||||
f = g = lambda x: x**2
|
||||
lambda: "no-op"
|
||||
|
||||
# Annotated
|
||||
from typing import Callable, ParamSpec
|
||||
|
||||
P = ParamSpec("P")
|
||||
|
||||
# ParamSpec cannot be used in this context, so do not preserve the annotation.
|
||||
f: Callable[P, int] = lambda *args: len(args)
|
||||
f: Callable[[], None] = lambda: None
|
||||
f: Callable[..., None] = lambda a, b: None
|
||||
f: Callable[[int], int] = lambda x: 2 * x
|
||||
|
||||
# Let's use the `Callable` type from `collections.abc` instead.
|
||||
from collections.abc import Callable
|
||||
|
||||
f: Callable[[str, int], str] = lambda a, b: a * b
|
||||
f: Callable[[str, int], tuple[str, int]] = lambda a, b: (a, b)
|
||||
f: Callable[[str, int, list[str]], list[str]] = lambda a, b, /, c: [*c, a * b]
|
||||
|
||||
|
||||
# Override `Callable`
|
||||
class Callable:
|
||||
pass
|
||||
|
||||
|
||||
# Do not copy the annotation from here on out.
|
||||
f: Callable[[str, int], str] = lambda a, b: a * b
|
||||
|
||||
@@ -97,10 +97,10 @@ if length > options.max_line_length:
|
||||
if os.path.exists(os.path.join(path, PEP8_BIN)):
|
||||
cmd = ([os.path.join(path, PEP8_BIN)] +
|
||||
self._pep8_options(targetfile))
|
||||
#: W191
|
||||
#: W191 - okay
|
||||
'''
|
||||
multiline string with tab in it'''
|
||||
#: E101 W191
|
||||
#: E101 (W191 okay)
|
||||
'''multiline string
|
||||
with tabs
|
||||
and spaces
|
||||
@@ -142,4 +142,10 @@ def test_keys(self):
|
||||
x = [
|
||||
'abc'
|
||||
]
|
||||
#:
|
||||
#: W191 - okay
|
||||
''' multiline string with tab in it, same lines'''
|
||||
""" here we're using '''different delimiters'''"""
|
||||
'''
|
||||
multiline string with tab in it, different lines
|
||||
'''
|
||||
" single line string with tab in it"
|
||||
|
||||
@@ -115,6 +115,20 @@ def f(x, *args, **kwargs):
|
||||
return x
|
||||
|
||||
|
||||
def f(x, *, y, z):
|
||||
"""Do something.
|
||||
|
||||
Args:
|
||||
x: some first value
|
||||
|
||||
Keyword Args:
|
||||
y (int): the other value
|
||||
z (int): the last value
|
||||
|
||||
"""
|
||||
return x, y, z
|
||||
|
||||
|
||||
class Test:
|
||||
def f(self, /, arg1: int) -> None:
|
||||
"""
|
||||
|
||||
@@ -11,3 +11,9 @@
|
||||
"{}".format(1, 2, 3) # F523
|
||||
"{:{}}".format(1, 2) # No issues
|
||||
"{:{}}".format(1, 2, 3) # F523
|
||||
|
||||
# With *args
|
||||
"{0}{1}".format(*args) # No issues
|
||||
"{0}{1}".format(1, *args) # No issues
|
||||
"{0}{1}".format(1, 2, *args) # No issues
|
||||
"{0}{1}".format(1, 2, 3, *args) # F523
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
x = 1 # type: ignore
|
||||
x = 1 # type ignore
|
||||
x = 1 # type:ignore
|
||||
x = 1 # type: ignore[attr-defined] # type: ignore
|
||||
|
||||
x = 1
|
||||
x = 1 # type ignore
|
||||
x = 1 # type ignore # noqa
|
||||
x = 1 # type: ignore[attr-defined]
|
||||
x = 1 # type: ignore[attr-defined, name-defined]
|
||||
x = 1 # type: ignore[attr-defined] # type: ignore[type-mismatch]
|
||||
x = 1 # type: ignore[type-mismatch] # noqa
|
||||
x = 1 # type: ignore [attr-defined]
|
||||
x = 1 # type: ignore [attr-defined, name-defined]
|
||||
x = 1 # type: ignore [type-mismatch] # noqa
|
||||
x = 1 # type: Union[int, str]
|
||||
x = 1 # type: ignoreme
|
||||
|
||||
16
crates/ruff/resources/test/fixtures/pygrep-hooks/PGH003_1.py
vendored
Normal file
16
crates/ruff/resources/test/fixtures/pygrep-hooks/PGH003_1.py
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
x = 1 # pyright: ignore
|
||||
x = 1 # pyright:ignore
|
||||
x = 1 # pyright: ignore[attr-defined] # pyright: ignore
|
||||
|
||||
x = 1
|
||||
x = 1 # pyright ignore
|
||||
x = 1 # pyright ignore # noqa
|
||||
x = 1 # pyright: ignore[attr-defined]
|
||||
x = 1 # pyright: ignore[attr-defined, name-defined]
|
||||
x = 1 # pyright: ignore[attr-defined] # pyright: ignore[type-mismatch]
|
||||
x = 1 # pyright: ignore[type-mismatch] # noqa
|
||||
x = 1 # pyright: ignore [attr-defined]
|
||||
x = 1 # pyright: ignore [attr-defined, name-defined]
|
||||
x = 1 # pyright: ignore [type-mismatch] # noqa
|
||||
x = 1 # pyright: Union[int, str]
|
||||
x = 1 # pyright: ignoreme
|
||||
Binary file not shown.
@@ -1,3 +1,6 @@
|
||||
import typing
|
||||
from typing import cast
|
||||
|
||||
# For -> for, variable reused
|
||||
for i in []:
|
||||
for i in []: # error
|
||||
@@ -43,6 +46,9 @@ for i in []:
|
||||
|
||||
# For -> assignment
|
||||
for i in []:
|
||||
# ignore typing cast
|
||||
i = cast(int, i)
|
||||
i = typing.cast(int, i)
|
||||
i = 5 # error
|
||||
|
||||
# For -> augmented assignment
|
||||
@@ -53,6 +59,10 @@ for i in []:
|
||||
for i in []:
|
||||
i: int = 5 # error
|
||||
|
||||
# For -> annotated assignment without value
|
||||
for i in []:
|
||||
i: int # no error
|
||||
|
||||
# Async for -> for, variable reused
|
||||
async for i in []:
|
||||
for i in []: # error
|
||||
|
||||
51
crates/ruff/resources/test/fixtures/pylint/unexpected_special_method_signature.py
vendored
Normal file
51
crates/ruff/resources/test/fixtures/pylint/unexpected_special_method_signature.py
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
class TestClass:
|
||||
def __bool__(self):
|
||||
...
|
||||
|
||||
def __bool__(self, x): # too many mandatory args
|
||||
...
|
||||
|
||||
def __bool__(self, x=1): # additional optional args OK
|
||||
...
|
||||
|
||||
def __bool__(self, *args): # varargs OK
|
||||
...
|
||||
|
||||
def __bool__(): # ignored; should be caughty by E0211/N805
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def __bool__():
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def __bool__(x): # too many mandatory args
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def __bool__(x=1): # additional optional args OK
|
||||
...
|
||||
|
||||
def __eq__(self, other): # multiple args
|
||||
...
|
||||
|
||||
def __eq__(self, other=1): # expected arg is optional
|
||||
...
|
||||
|
||||
def __eq__(self): # too few mandatory args
|
||||
...
|
||||
|
||||
def __eq__(self, other, other_other): # too many mandatory args
|
||||
...
|
||||
|
||||
def __round__(self): # allow zero additional args.
|
||||
...
|
||||
|
||||
def __round__(self, x): # allow one additional args.
|
||||
...
|
||||
|
||||
def __round__(self, x, y): # disallow 2 args
|
||||
...
|
||||
|
||||
def __round__(self, x, y, z=2): # disallow 3 args even when one is optional
|
||||
...
|
||||
@@ -83,3 +83,26 @@ print('Hello %s (%s)' % bar['bop'])
|
||||
print('Hello %(arg)s' % bar)
|
||||
print('Hello %(arg)s' % bar.baz)
|
||||
print('Hello %(arg)s' % bar['bop'])
|
||||
|
||||
# Hanging modulos
|
||||
(
|
||||
"foo %s "
|
||||
"bar %s"
|
||||
) % (x, y)
|
||||
|
||||
(
|
||||
"foo %(foo)s "
|
||||
"bar %(bar)s"
|
||||
) % {"foo": x, "bar": y}
|
||||
|
||||
(
|
||||
"""foo %s"""
|
||||
% (x,)
|
||||
)
|
||||
|
||||
(
|
||||
"""
|
||||
foo %s
|
||||
"""
|
||||
% (x,)
|
||||
)
|
||||
|
||||
@@ -34,28 +34,6 @@ pytest.param('"%8s" % (None,)', id="unsafe width-string conversion"),
|
||||
"%(and)s" % {"and": 2}
|
||||
|
||||
# OK (arguably false negatives)
|
||||
(
|
||||
"foo %s "
|
||||
"bar %s"
|
||||
) % (x, y)
|
||||
|
||||
(
|
||||
"foo %(foo)s "
|
||||
"bar %(bar)s"
|
||||
) % {"foo": x, "bar": y}
|
||||
|
||||
(
|
||||
"""foo %s"""
|
||||
% (x,)
|
||||
)
|
||||
|
||||
(
|
||||
"""
|
||||
foo %s
|
||||
"""
|
||||
% (x,)
|
||||
)
|
||||
|
||||
'Hello %s' % bar
|
||||
|
||||
'Hello %s' % bar.baz
|
||||
|
||||
27
crates/ruff/resources/test/fixtures/ruff/RUF008.py
vendored
Normal file
27
crates/ruff/resources/test/fixtures/ruff/RUF008.py
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
import typing
|
||||
from dataclasses import dataclass, field
|
||||
from typing import ClassVar, Sequence
|
||||
|
||||
KNOWINGLY_MUTABLE_DEFAULT = []
|
||||
|
||||
|
||||
@dataclass()
|
||||
class A:
|
||||
mutable_default: list[int] = []
|
||||
immutable_annotation: typing.Sequence[int] = []
|
||||
without_annotation = []
|
||||
ignored_via_comment: list[int] = [] # noqa: RUF008
|
||||
correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT
|
||||
perfectly_fine: list[int] = field(default_factory=list)
|
||||
class_variable: typing.ClassVar[list[int]] = []
|
||||
|
||||
|
||||
@dataclass
|
||||
class B:
|
||||
mutable_default: list[int] = []
|
||||
immutable_annotation: Sequence[int] = []
|
||||
without_annotation = []
|
||||
ignored_via_comment: list[int] = [] # noqa: RUF008
|
||||
correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT
|
||||
perfectly_fine: list[int] = field(default_factory=list)
|
||||
class_variable: ClassVar[list[int]] = []
|
||||
31
crates/ruff/resources/test/fixtures/ruff/RUF009.py
vendored
Normal file
31
crates/ruff/resources/test/fixtures/ruff/RUF009.py
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
import typing
|
||||
from dataclasses import dataclass
|
||||
from typing import ClassVar, NamedTuple
|
||||
|
||||
|
||||
def default_function() -> list[int]:
|
||||
return []
|
||||
|
||||
|
||||
class ImmutableType(NamedTuple):
|
||||
something: int = 8
|
||||
|
||||
|
||||
@dataclass()
|
||||
class A:
|
||||
hidden_mutable_default: list[int] = default_function()
|
||||
class_variable: typing.ClassVar[list[int]] = default_function()
|
||||
another_class_var: ClassVar[list[int]] = default_function()
|
||||
|
||||
|
||||
DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES = ImmutableType(40)
|
||||
DEFAULT_A_FOR_ALL_DATACLASSES = A([1, 2, 3])
|
||||
|
||||
|
||||
@dataclass
|
||||
class B:
|
||||
hidden_mutable_default: list[int] = default_function()
|
||||
another_dataclass: A = A()
|
||||
not_optimal: ImmutableType = ImmutableType(20)
|
||||
good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES
|
||||
okay_variant: A = DEFAULT_A_FOR_ALL_DATACLASSES
|
||||
@@ -103,7 +103,7 @@ fn is_lone_child(child: &Stmt, parent: &Stmt, deleted: &[&Stmt]) -> Result<bool>
|
||||
/// Return the location of a trailing semicolon following a `Stmt`, if it's part
|
||||
/// of a multi-statement line.
|
||||
fn trailing_semicolon(stmt: &Stmt, locator: &Locator) -> Option<Location> {
|
||||
let contents = locator.skip(stmt.end_location.unwrap());
|
||||
let contents = locator.after(stmt.end_location.unwrap());
|
||||
for (row, line) in NewlineWithTrailingNewline::from(contents).enumerate() {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.starts_with(';') {
|
||||
@@ -126,7 +126,7 @@ fn trailing_semicolon(stmt: &Stmt, locator: &Locator) -> Option<Location> {
|
||||
/// Find the next valid break for a `Stmt` after a semicolon.
|
||||
fn next_stmt_break(semicolon: Location, locator: &Locator) -> Location {
|
||||
let start_location = Location::new(semicolon.row(), semicolon.column() + 1);
|
||||
let contents = locator.skip(start_location);
|
||||
let contents = locator.after(start_location);
|
||||
for (row, line) in NewlineWithTrailingNewline::from(contents).enumerate() {
|
||||
let trimmed = line.trim();
|
||||
// Skip past any continuations.
|
||||
@@ -158,7 +158,7 @@ fn next_stmt_break(semicolon: Location, locator: &Locator) -> Location {
|
||||
|
||||
/// Return `true` if a `Stmt` occurs at the end of a file.
|
||||
fn is_end_of_file(stmt: &Stmt, locator: &Locator) -> bool {
|
||||
let contents = locator.skip(stmt.end_location.unwrap());
|
||||
let contents = locator.after(stmt.end_location.unwrap());
|
||||
contents.is_empty()
|
||||
}
|
||||
|
||||
@@ -361,7 +361,7 @@ pub fn remove_argument(
|
||||
remove_parentheses: bool,
|
||||
) -> Result<Edit> {
|
||||
// TODO(sbrugman): Preserve trailing comments.
|
||||
let contents = locator.skip(call_at);
|
||||
let contents = locator.after(call_at);
|
||||
|
||||
let mut fix_start = None;
|
||||
let mut fix_end = None;
|
||||
|
||||
@@ -15,10 +15,15 @@ pub mod actions;
|
||||
|
||||
/// Auto-fix errors in a file, and write the fixed source code to disk.
|
||||
pub fn fix_file(diagnostics: &[Diagnostic], locator: &Locator) -> Option<(String, FixTable)> {
|
||||
if diagnostics.iter().all(|check| check.fix.is_empty()) {
|
||||
let mut with_fixes = diagnostics
|
||||
.iter()
|
||||
.filter(|diag| !diag.fix.is_empty())
|
||||
.peekable();
|
||||
|
||||
if with_fixes.peek().is_none() {
|
||||
None
|
||||
} else {
|
||||
Some(apply_fixes(diagnostics.iter(), locator))
|
||||
Some(apply_fixes(with_fixes, locator))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +57,7 @@ fn apply_fixes<'a>(
|
||||
// Best-effort approach: if this fix overlaps with a fix we've already applied,
|
||||
// skip it.
|
||||
if last_pos.map_or(false, |last_pos| {
|
||||
fix.location()
|
||||
fix.min_location()
|
||||
.map_or(false, |fix_location| last_pos >= fix_location)
|
||||
}) {
|
||||
continue;
|
||||
@@ -60,14 +65,14 @@ fn apply_fixes<'a>(
|
||||
|
||||
for edit in fix.edits() {
|
||||
// Add all contents from `last_pos` to `fix.location`.
|
||||
let slice = locator.slice(Range::new(last_pos.unwrap_or_default(), edit.location));
|
||||
let slice = locator.slice(Range::new(last_pos.unwrap_or_default(), edit.location()));
|
||||
output.push_str(slice);
|
||||
|
||||
// Add the patch itself.
|
||||
output.push_str(&edit.content);
|
||||
output.push_str(edit.content().unwrap_or_default());
|
||||
|
||||
// Track that the edit was applied.
|
||||
last_pos = Some(edit.end_location);
|
||||
last_pos = Some(edit.end_location());
|
||||
applied.insert(edit);
|
||||
}
|
||||
|
||||
@@ -75,7 +80,7 @@ fn apply_fixes<'a>(
|
||||
}
|
||||
|
||||
// Add the remaining content.
|
||||
let slice = locator.skip(last_pos.unwrap_or_default());
|
||||
let slice = locator.after(last_pos.unwrap_or_default());
|
||||
output.push_str(slice);
|
||||
|
||||
(output, fixed)
|
||||
@@ -83,8 +88,8 @@ fn apply_fixes<'a>(
|
||||
|
||||
/// Compare two fixes.
|
||||
fn cmp_fix(rule1: Rule, rule2: Rule, fix1: &Fix, fix2: &Fix) -> std::cmp::Ordering {
|
||||
fix1.location()
|
||||
.cmp(&fix2.location())
|
||||
fix1.min_location()
|
||||
.cmp(&fix2.min_location())
|
||||
.then_with(|| match (&rule1, &rule2) {
|
||||
// Apply `EndsInPeriod` fixes before `NewLineAfterLastParagraph` fixes.
|
||||
(Rule::EndsInPeriod, Rule::NewLineAfterLastParagraph) => std::cmp::Ordering::Less,
|
||||
@@ -109,8 +114,8 @@ mod tests {
|
||||
.map(|edit| Diagnostic {
|
||||
// The choice of rule here is arbitrary.
|
||||
kind: MissingNewlineAtEndOfFile.into(),
|
||||
location: edit.location,
|
||||
end_location: edit.end_location,
|
||||
location: edit.location(),
|
||||
end_location: edit.end_location(),
|
||||
fix: edit.into(),
|
||||
parent: None,
|
||||
})
|
||||
@@ -135,11 +140,11 @@ class A(object):
|
||||
"#
|
||||
.trim(),
|
||||
);
|
||||
let diagnostics = create_diagnostics([Edit {
|
||||
content: "Bar".to_string(),
|
||||
location: Location::new(1, 8),
|
||||
end_location: Location::new(1, 14),
|
||||
}]);
|
||||
let diagnostics = create_diagnostics([Edit::replacement(
|
||||
"Bar".to_string(),
|
||||
Location::new(1, 8),
|
||||
Location::new(1, 14),
|
||||
)]);
|
||||
let (contents, fixed) = apply_fixes(diagnostics.iter(), &locator);
|
||||
assert_eq!(
|
||||
contents,
|
||||
@@ -161,11 +166,8 @@ class A(object):
|
||||
"#
|
||||
.trim(),
|
||||
);
|
||||
let diagnostics = create_diagnostics([Edit {
|
||||
content: String::new(),
|
||||
location: Location::new(1, 7),
|
||||
end_location: Location::new(1, 15),
|
||||
}]);
|
||||
let diagnostics =
|
||||
create_diagnostics([Edit::deletion(Location::new(1, 7), Location::new(1, 15))]);
|
||||
let (contents, fixed) = apply_fixes(diagnostics.iter(), &locator);
|
||||
assert_eq!(
|
||||
contents,
|
||||
@@ -188,16 +190,8 @@ class A(object, object, object):
|
||||
.trim(),
|
||||
);
|
||||
let diagnostics = create_diagnostics([
|
||||
Edit {
|
||||
content: String::new(),
|
||||
location: Location::new(1, 8),
|
||||
end_location: Location::new(1, 16),
|
||||
},
|
||||
Edit {
|
||||
content: String::new(),
|
||||
location: Location::new(1, 22),
|
||||
end_location: Location::new(1, 30),
|
||||
},
|
||||
Edit::deletion(Location::new(1, 8), Location::new(1, 16)),
|
||||
Edit::deletion(Location::new(1, 22), Location::new(1, 30)),
|
||||
]);
|
||||
let (contents, fixed) = apply_fixes(diagnostics.iter(), &locator);
|
||||
|
||||
@@ -222,16 +216,12 @@ class A(object):
|
||||
.trim(),
|
||||
);
|
||||
let diagnostics = create_diagnostics([
|
||||
Edit {
|
||||
content: String::new(),
|
||||
location: Location::new(1, 7),
|
||||
end_location: Location::new(1, 15),
|
||||
},
|
||||
Edit {
|
||||
content: "ignored".to_string(),
|
||||
location: Location::new(1, 9),
|
||||
end_location: Location::new(1, 11),
|
||||
},
|
||||
Edit::deletion(Location::new(1, 7), Location::new(1, 15)),
|
||||
Edit::replacement(
|
||||
"ignored".to_string(),
|
||||
Location::new(1, 9),
|
||||
Location::new(1, 11),
|
||||
),
|
||||
]);
|
||||
let (contents, fixed) = apply_fixes(diagnostics.iter(), &locator);
|
||||
assert_eq!(
|
||||
|
||||
@@ -161,6 +161,18 @@ macro_rules! visit_non_type_definition {
|
||||
}};
|
||||
}
|
||||
|
||||
/// Visit an [`Expr`], and treat it as a boolean test. This is useful for detecting whether an
|
||||
/// expressions return value is significant, or whether the calling context only relies on
|
||||
/// its truthiness.
|
||||
macro_rules! visit_boolean_test {
|
||||
($self:ident, $expr:expr) => {{
|
||||
let prev_in_boolean_test = $self.ctx.in_boolean_test;
|
||||
$self.ctx.in_boolean_test = true;
|
||||
$self.visit_expr($expr);
|
||||
$self.ctx.in_boolean_test = prev_in_boolean_test;
|
||||
}};
|
||||
}
|
||||
|
||||
impl<'a, 'b> Visitor<'b> for Checker<'a>
|
||||
where
|
||||
'b: 'a,
|
||||
@@ -344,6 +356,7 @@ where
|
||||
|expr| self.ctx.resolve_call_path(expr),
|
||||
));
|
||||
}
|
||||
|
||||
if self.settings.rules.enabled(Rule::AmbiguousFunctionName) {
|
||||
if let Some(diagnostic) =
|
||||
pycodestyle::rules::ambiguous_function_name(name, || {
|
||||
@@ -358,7 +371,9 @@ where
|
||||
if let Some(diagnostic) = pep8_naming::rules::invalid_function_name(
|
||||
stmt,
|
||||
name,
|
||||
decorator_list,
|
||||
&self.settings.pep8_naming.ignore_names,
|
||||
&self.ctx,
|
||||
self.locator,
|
||||
) {
|
||||
self.diagnostics.push(diagnostic);
|
||||
@@ -587,6 +602,21 @@ where
|
||||
);
|
||||
}
|
||||
|
||||
if self
|
||||
.settings
|
||||
.rules
|
||||
.enabled(Rule::UnexpectedSpecialMethodSignature)
|
||||
{
|
||||
pylint::rules::unexpected_special_method_signature(
|
||||
self,
|
||||
stmt,
|
||||
name,
|
||||
decorator_list,
|
||||
args,
|
||||
self.locator,
|
||||
);
|
||||
}
|
||||
|
||||
self.check_builtin_shadowing(name, stmt, true);
|
||||
|
||||
// Visit the decorators and arguments, but avoid the body, which will be
|
||||
@@ -595,14 +625,9 @@ where
|
||||
self.visit_expr(expr);
|
||||
}
|
||||
|
||||
// If we're in a class or module scope, then the annotation needs to be
|
||||
// available at runtime.
|
||||
// See: https://docs.python.org/3/reference/simple_stmts.html#annotated-assignment-statements
|
||||
let runtime_annotation = !self.ctx.annotations_future_enabled
|
||||
&& matches!(
|
||||
self.ctx.scope().kind,
|
||||
ScopeKind::Class(..) | ScopeKind::Module
|
||||
);
|
||||
// Function annotations are always evaluated at runtime, unless future annotations
|
||||
// are enabled.
|
||||
let runtime_annotation = !self.ctx.annotations_future_enabled;
|
||||
|
||||
for arg in &args.posonlyargs {
|
||||
if let Some(expr) = &arg.node.annotation {
|
||||
@@ -816,6 +841,24 @@ where
|
||||
flake8_pie::rules::non_unique_enums(self, stmt, body);
|
||||
}
|
||||
|
||||
if self.settings.rules.any_enabled(&[
|
||||
Rule::MutableDataclassDefault,
|
||||
Rule::FunctionCallInDataclassDefaultArgument,
|
||||
]) && ruff::rules::is_dataclass(self, decorator_list)
|
||||
{
|
||||
if self.settings.rules.enabled(Rule::MutableDataclassDefault) {
|
||||
ruff::rules::mutable_dataclass_default(self, body);
|
||||
}
|
||||
|
||||
if self
|
||||
.settings
|
||||
.rules
|
||||
.enabled(Rule::FunctionCallInDataclassDefaultArgument)
|
||||
{
|
||||
ruff::rules::function_call_in_dataclass_defaults(self, body);
|
||||
}
|
||||
}
|
||||
|
||||
self.check_builtin_shadowing(name, stmt, false);
|
||||
|
||||
for expr in bases {
|
||||
@@ -949,15 +992,11 @@ where
|
||||
|
||||
// flake8_tidy_imports
|
||||
if self.settings.rules.enabled(Rule::BannedApi) {
|
||||
if let Some(diagnostic) =
|
||||
flake8_tidy_imports::banned_api::name_or_parent_is_banned(
|
||||
alias,
|
||||
&alias.node.name,
|
||||
&self.settings.flake8_tidy_imports.banned_api,
|
||||
)
|
||||
{
|
||||
self.diagnostics.push(diagnostic);
|
||||
}
|
||||
flake8_tidy_imports::banned_api::name_or_parent_is_banned(
|
||||
self,
|
||||
&alias.node.name,
|
||||
alias,
|
||||
);
|
||||
}
|
||||
|
||||
// pylint
|
||||
@@ -1045,7 +1084,7 @@ where
|
||||
|
||||
if self.settings.rules.enabled(Rule::UnconventionalImportAlias) {
|
||||
if let Some(diagnostic) =
|
||||
flake8_import_conventions::rules::check_conventional_import(
|
||||
flake8_import_conventions::rules::conventional_import_alias(
|
||||
stmt,
|
||||
&alias.node.name,
|
||||
alias.node.asname.as_deref(),
|
||||
@@ -1056,6 +1095,21 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
if self.settings.rules.enabled(Rule::BannedImportAlias) {
|
||||
if let Some(asname) = &alias.node.asname {
|
||||
if let Some(diagnostic) =
|
||||
flake8_import_conventions::rules::banned_import_alias(
|
||||
stmt,
|
||||
&alias.node.name,
|
||||
asname,
|
||||
&self.settings.flake8_import_conventions.banned_aliases,
|
||||
)
|
||||
{
|
||||
self.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self
|
||||
.settings
|
||||
.rules
|
||||
@@ -1123,26 +1177,24 @@ where
|
||||
}
|
||||
|
||||
if self.settings.rules.enabled(Rule::BannedApi) {
|
||||
if let Some(module) = module {
|
||||
for name in names {
|
||||
if let Some(diagnostic) =
|
||||
flake8_tidy_imports::banned_api::name_is_banned(
|
||||
module,
|
||||
name,
|
||||
&self.settings.flake8_tidy_imports.banned_api,
|
||||
)
|
||||
{
|
||||
self.diagnostics.push(diagnostic);
|
||||
if let Some(module) = helpers::resolve_imported_module_path(
|
||||
*level,
|
||||
module.as_deref(),
|
||||
self.module_path.as_deref(),
|
||||
) {
|
||||
flake8_tidy_imports::banned_api::name_or_parent_is_banned(
|
||||
self, &module, stmt,
|
||||
);
|
||||
|
||||
for alias in names {
|
||||
if alias.node.name == "*" {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if let Some(diagnostic) =
|
||||
flake8_tidy_imports::banned_api::name_or_parent_is_banned(
|
||||
stmt,
|
||||
module,
|
||||
&self.settings.flake8_tidy_imports.banned_api,
|
||||
)
|
||||
{
|
||||
self.diagnostics.push(diagnostic);
|
||||
flake8_tidy_imports::banned_api::name_is_banned(
|
||||
self,
|
||||
format!("{module}.{}", alias.node.name),
|
||||
alias,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1281,7 +1333,7 @@ where
|
||||
stmt,
|
||||
*level,
|
||||
module.as_deref(),
|
||||
self.module_path.as_ref(),
|
||||
self.module_path.as_deref(),
|
||||
&self.settings.flake8_tidy_imports.ban_relative_imports,
|
||||
)
|
||||
{
|
||||
@@ -1307,7 +1359,7 @@ where
|
||||
&alias.node.name,
|
||||
);
|
||||
if let Some(diagnostic) =
|
||||
flake8_import_conventions::rules::check_conventional_import(
|
||||
flake8_import_conventions::rules::conventional_import_alias(
|
||||
stmt,
|
||||
&full_name,
|
||||
alias.node.asname.as_deref(),
|
||||
@@ -1318,6 +1370,26 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
if self.settings.rules.enabled(Rule::BannedImportAlias) {
|
||||
if let Some(asname) = &alias.node.asname {
|
||||
let full_name = helpers::format_import_from_member(
|
||||
*level,
|
||||
module.as_deref(),
|
||||
&alias.node.name,
|
||||
);
|
||||
if let Some(diagnostic) =
|
||||
flake8_import_conventions::rules::banned_import_alias(
|
||||
stmt,
|
||||
&full_name,
|
||||
asname,
|
||||
&self.settings.flake8_import_conventions.banned_aliases,
|
||||
)
|
||||
{
|
||||
self.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(asname) = &alias.node.asname {
|
||||
if self
|
||||
.settings
|
||||
@@ -1412,6 +1484,16 @@ where
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.settings.rules.enabled(Rule::BannedImportFrom) {
|
||||
if let Some(diagnostic) = flake8_import_conventions::rules::banned_import_from(
|
||||
stmt,
|
||||
&helpers::format_import_from(*level, module.as_deref()),
|
||||
&self.settings.flake8_import_conventions.banned_from,
|
||||
) {
|
||||
self.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
StmtKind::Raise { exc, .. } => {
|
||||
if self.settings.rules.enabled(Rule::RaiseNotImplemented) {
|
||||
@@ -1547,20 +1629,20 @@ where
|
||||
}
|
||||
}
|
||||
StmtKind::Assert { test, msg } => {
|
||||
if !self.ctx.in_type_checking_block {
|
||||
if self.settings.rules.enabled(Rule::Assert) {
|
||||
self.diagnostics
|
||||
.push(flake8_bandit::rules::assert_used(stmt));
|
||||
}
|
||||
}
|
||||
if self.settings.rules.enabled(Rule::AssertTuple) {
|
||||
pyflakes::rules::assert_tuple(self, stmt, test);
|
||||
}
|
||||
if self.settings.rules.enabled(Rule::AssertFalse) {
|
||||
flake8_bugbear::rules::assert_false(self, stmt, test, msg.as_deref());
|
||||
}
|
||||
if self.settings.rules.enabled(Rule::Assert) {
|
||||
self.diagnostics
|
||||
.push(flake8_bandit::rules::assert_used(stmt));
|
||||
}
|
||||
if self.settings.rules.enabled(Rule::PytestAssertAlwaysFalse) {
|
||||
if let Some(diagnostic) = flake8_pytest_style::rules::assert_falsy(stmt, test) {
|
||||
self.diagnostics.push(diagnostic);
|
||||
}
|
||||
flake8_pytest_style::rules::assert_falsy(self, stmt, test);
|
||||
}
|
||||
if self.settings.rules.enabled(Rule::PytestCompositeAssertion) {
|
||||
flake8_pytest_style::rules::composite_condition(
|
||||
@@ -1570,7 +1652,6 @@ where
|
||||
msg.as_deref(),
|
||||
);
|
||||
}
|
||||
|
||||
if self.settings.rules.enabled(Rule::AssertOnStringLiteral) {
|
||||
pylint::rules::assert_on_string_literal(self, test);
|
||||
}
|
||||
@@ -1729,7 +1810,7 @@ where
|
||||
StmtKind::Assign { targets, value, .. } => {
|
||||
if self.settings.rules.enabled(Rule::LambdaAssignment) {
|
||||
if let [target] = &targets[..] {
|
||||
pycodestyle::rules::lambda_assignment(self, target, value, stmt);
|
||||
pycodestyle::rules::lambda_assignment(self, target, value, None, stmt);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1745,12 +1826,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
if self.is_stub {
|
||||
if self.settings.rules.enabled(Rule::UnprefixedTypeParam) {
|
||||
flake8_pyi::rules::prefix_type_params(self, value, targets);
|
||||
}
|
||||
}
|
||||
|
||||
if self.settings.rules.enabled(Rule::GlobalStatement) {
|
||||
for target in targets.iter() {
|
||||
if let ExprKind::Name { id, .. } = &target.node {
|
||||
@@ -1791,8 +1866,20 @@ where
|
||||
}
|
||||
|
||||
if self.is_stub {
|
||||
if self.settings.rules.enabled(Rule::AssignmentDefaultInStub) {
|
||||
flake8_pyi::rules::assignment_default_in_stub(self, value, None);
|
||||
if self
|
||||
.settings
|
||||
.rules
|
||||
.any_enabled(&[Rule::UnprefixedTypeParam, Rule::AssignmentDefaultInStub])
|
||||
{
|
||||
// Ignore assignments in function bodies; those are covered by other rules.
|
||||
if !self.ctx.scopes().any(|scope| scope.kind.is_function()) {
|
||||
if self.settings.rules.enabled(Rule::UnprefixedTypeParam) {
|
||||
flake8_pyi::rules::prefix_type_params(self, value, targets);
|
||||
}
|
||||
if self.settings.rules.enabled(Rule::AssignmentDefaultInStub) {
|
||||
flake8_pyi::rules::assignment_default_in_stub(self, targets, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1804,7 +1891,13 @@ where
|
||||
} => {
|
||||
if self.settings.rules.enabled(Rule::LambdaAssignment) {
|
||||
if let Some(value) = value {
|
||||
pycodestyle::rules::lambda_assignment(self, target, value, stmt);
|
||||
pycodestyle::rules::lambda_assignment(
|
||||
self,
|
||||
target,
|
||||
value,
|
||||
Some(annotation),
|
||||
stmt,
|
||||
);
|
||||
}
|
||||
}
|
||||
if self
|
||||
@@ -1822,11 +1915,12 @@ where
|
||||
if self.is_stub {
|
||||
if let Some(value) = value {
|
||||
if self.settings.rules.enabled(Rule::AssignmentDefaultInStub) {
|
||||
flake8_pyi::rules::assignment_default_in_stub(
|
||||
self,
|
||||
value,
|
||||
Some(annotation),
|
||||
);
|
||||
// Ignore assignments in function bodies; those are covered by other rules.
|
||||
if !self.ctx.scopes().any(|scope| scope.kind.is_function()) {
|
||||
flake8_pyi::rules::annotated_assignment_default_in_stub(
|
||||
self, target, value, annotation,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2087,8 +2181,19 @@ where
|
||||
}
|
||||
self.visit_expr(target);
|
||||
}
|
||||
StmtKind::Assert { test, msg } => {
|
||||
visit_boolean_test!(self, test);
|
||||
if let Some(expr) = msg {
|
||||
self.visit_expr(expr);
|
||||
}
|
||||
}
|
||||
StmtKind::While { test, body, orelse } => {
|
||||
visit_boolean_test!(self, test);
|
||||
self.visit_body(body);
|
||||
self.visit_body(orelse);
|
||||
}
|
||||
StmtKind::If { test, body, orelse } => {
|
||||
self.visit_expr(test);
|
||||
visit_boolean_test!(self, test);
|
||||
|
||||
if flake8_type_checking::helpers::is_type_checking_block(&self.ctx, test) {
|
||||
if self.settings.rules.enabled(Rule::EmptyTypeCheckingBlock) {
|
||||
@@ -2175,6 +2280,11 @@ where
|
||||
|
||||
let prev_in_literal = self.ctx.in_literal;
|
||||
let prev_in_type_definition = self.ctx.in_type_definition;
|
||||
let prev_in_boolean_test = self.ctx.in_boolean_test;
|
||||
|
||||
if !matches!(expr.node, ExprKind::BoolOp { .. }) {
|
||||
self.ctx.in_boolean_test = false;
|
||||
}
|
||||
|
||||
// Pre-visit.
|
||||
match &expr.node {
|
||||
@@ -2528,13 +2638,6 @@ where
|
||||
if self.settings.rules.enabled(Rule::UnnecessaryDictKwargs) {
|
||||
flake8_pie::rules::unnecessary_dict_kwargs(self, expr, keywords);
|
||||
}
|
||||
if self
|
||||
.settings
|
||||
.rules
|
||||
.enabled(Rule::UnnecessaryComprehensionAnyAll)
|
||||
{
|
||||
flake8_pie::rules::unnecessary_comprehension_any_all(self, expr, func, args);
|
||||
}
|
||||
|
||||
// flake8-bandit
|
||||
if self.settings.rules.enabled(Rule::ExecBuiltin) {
|
||||
@@ -2594,6 +2697,16 @@ where
|
||||
self, func, args, keywords,
|
||||
);
|
||||
}
|
||||
if self.settings.rules.any_enabled(&[
|
||||
Rule::SubprocessWithoutShellEqualsTrue,
|
||||
Rule::SubprocessPopenWithShellEqualsTrue,
|
||||
Rule::CallWithShellEqualsTrue,
|
||||
Rule::StartProcessWithAShell,
|
||||
Rule::StartProcessWithNoShell,
|
||||
Rule::StartProcessWithPartialPath,
|
||||
]) {
|
||||
flake8_bandit::rules::shell_injection(self, func, args, keywords);
|
||||
}
|
||||
|
||||
// flake8-comprehensions
|
||||
if self.settings.rules.enabled(Rule::UnnecessaryGeneratorList) {
|
||||
@@ -2665,7 +2778,7 @@ where
|
||||
.enabled(Rule::UnnecessaryLiteralWithinTupleCall)
|
||||
{
|
||||
flake8_comprehensions::rules::unnecessary_literal_within_tuple_call(
|
||||
self, expr, func, args,
|
||||
self, expr, func, args, keywords,
|
||||
);
|
||||
}
|
||||
if self
|
||||
@@ -2674,7 +2787,16 @@ where
|
||||
.enabled(Rule::UnnecessaryLiteralWithinListCall)
|
||||
{
|
||||
flake8_comprehensions::rules::unnecessary_literal_within_list_call(
|
||||
self, expr, func, args,
|
||||
self, expr, func, args, keywords,
|
||||
);
|
||||
}
|
||||
if self
|
||||
.settings
|
||||
.rules
|
||||
.enabled(Rule::UnnecessaryLiteralWithinDictCall)
|
||||
{
|
||||
flake8_comprehensions::rules::unnecessary_literal_within_dict_call(
|
||||
self, expr, func, args, keywords,
|
||||
);
|
||||
}
|
||||
if self.settings.rules.enabled(Rule::UnnecessaryListCall) {
|
||||
@@ -2716,6 +2838,15 @@ where
|
||||
args,
|
||||
);
|
||||
}
|
||||
if self
|
||||
.settings
|
||||
.rules
|
||||
.enabled(Rule::UnnecessaryComprehensionAnyAll)
|
||||
{
|
||||
flake8_comprehensions::rules::unnecessary_comprehension_any_all(
|
||||
self, expr, func, args, keywords,
|
||||
);
|
||||
}
|
||||
|
||||
// flake8-boolean-trap
|
||||
if self
|
||||
@@ -3178,7 +3309,7 @@ where
|
||||
}
|
||||
|
||||
if self.settings.rules.enabled(Rule::PrintfStringFormatting) {
|
||||
pyupgrade::rules::printf_string_formatting(self, expr, left, right);
|
||||
pyupgrade::rules::printf_string_formatting(self, expr, right);
|
||||
}
|
||||
if self.settings.rules.enabled(Rule::BadStringFormatType) {
|
||||
pylint::rules::bad_string_format_type(self, expr, right);
|
||||
@@ -3211,6 +3342,27 @@ where
|
||||
flake8_bandit::rules::hardcoded_sql_expression(self, expr);
|
||||
}
|
||||
}
|
||||
ExprKind::BinOp {
|
||||
op: Operator::BitOr,
|
||||
..
|
||||
} => {
|
||||
if self.is_stub {
|
||||
if self.settings.rules.enabled(Rule::DuplicateUnionMember)
|
||||
&& self.ctx.in_type_definition
|
||||
&& self.ctx.current_expr_parent().map_or(true, |parent| {
|
||||
!matches!(
|
||||
parent.node,
|
||||
ExprKind::BinOp {
|
||||
op: Operator::BitOr,
|
||||
..
|
||||
}
|
||||
)
|
||||
})
|
||||
{
|
||||
flake8_pyi::rules::duplicate_union_member(self, expr);
|
||||
}
|
||||
}
|
||||
}
|
||||
ExprKind::UnaryOp { op, operand } => {
|
||||
let check_not_in = self.settings.rules.enabled(Rule::NotInTest);
|
||||
let check_not_is = self.settings.rules.enabled(Rule::NotIsTest);
|
||||
@@ -3473,6 +3625,11 @@ where
|
||||
(self.ctx.scope_stack.clone(), self.ctx.parents.clone()),
|
||||
));
|
||||
}
|
||||
ExprKind::IfExp { test, body, orelse } => {
|
||||
visit_boolean_test!(self, test);
|
||||
self.visit_expr(body);
|
||||
self.visit_expr(orelse);
|
||||
}
|
||||
ExprKind::Call {
|
||||
func,
|
||||
args,
|
||||
@@ -3501,11 +3658,22 @@ where
|
||||
.any(|target| call_path.as_slice() == ["mypy_extensions", target])
|
||||
{
|
||||
Some(Callable::MypyExtension)
|
||||
} else if call_path.as_slice() == ["", "bool"] {
|
||||
Some(Callable::Bool)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
match callable {
|
||||
Some(Callable::Bool) => {
|
||||
self.visit_expr(func);
|
||||
if !args.is_empty() {
|
||||
visit_boolean_test!(self, &args[0]);
|
||||
}
|
||||
for expr in args.iter().skip(1) {
|
||||
self.visit_expr(expr);
|
||||
}
|
||||
}
|
||||
Some(Callable::Cast) => {
|
||||
self.visit_expr(func);
|
||||
if !args.is_empty() {
|
||||
@@ -3704,6 +3872,7 @@ where
|
||||
|
||||
self.ctx.in_type_definition = prev_in_type_definition;
|
||||
self.ctx.in_literal = prev_in_literal;
|
||||
self.ctx.in_boolean_test = prev_in_boolean_test;
|
||||
|
||||
self.ctx.pop_expr();
|
||||
}
|
||||
@@ -3716,7 +3885,11 @@ where
|
||||
&comprehension.iter,
|
||||
);
|
||||
}
|
||||
visitor::walk_comprehension(self, comprehension);
|
||||
self.visit_expr(&comprehension.iter);
|
||||
self.visit_expr(&comprehension.target);
|
||||
for expr in &comprehension.ifs {
|
||||
visit_boolean_test!(self, expr);
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_excepthandler(&mut self, excepthandler: &'b Excepthandler) {
|
||||
@@ -4355,8 +4528,14 @@ impl<'a> Checker<'a> {
|
||||
.rules
|
||||
.enabled(Rule::MixedCaseVariableInClassScope)
|
||||
{
|
||||
if matches!(self.ctx.scope().kind, ScopeKind::Class(..)) {
|
||||
pep8_naming::rules::mixed_case_variable_in_class_scope(self, expr, parent, id);
|
||||
if let ScopeKind::Class(class) = &self.ctx.scope().kind {
|
||||
pep8_naming::rules::mixed_case_variable_in_class_scope(
|
||||
self,
|
||||
expr,
|
||||
parent,
|
||||
id,
|
||||
class.bases,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4387,7 +4566,6 @@ impl<'a> Checker<'a> {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO(charlie): Include comprehensions here.
|
||||
if matches!(
|
||||
parent.node,
|
||||
StmtKind::For { .. } | StmtKind::AsyncFor { .. }
|
||||
@@ -5083,7 +5261,7 @@ impl<'a> Checker<'a> {
|
||||
self.stylist,
|
||||
) {
|
||||
Ok(fix) => {
|
||||
if fix.content.is_empty() || fix.content == "pass" {
|
||||
if fix.is_deletion() || fix.content() == Some("pass") {
|
||||
self.deletions.insert(*defined_by);
|
||||
}
|
||||
Some(fix)
|
||||
|
||||
@@ -9,6 +9,7 @@ use ruff_python_ast::helpers::to_module_path;
|
||||
use ruff_python_ast::imports::{ImportMap, ModuleImport};
|
||||
use ruff_python_ast::source_code::{Indexer, Locator, Stylist};
|
||||
use ruff_python_ast::visitor::Visitor;
|
||||
use ruff_python_stdlib::path::is_python_stub_file;
|
||||
|
||||
use crate::directives::IsortDirectives;
|
||||
use crate::registry::Rule;
|
||||
@@ -88,9 +89,11 @@ pub fn check_imports(
|
||||
path: &Path,
|
||||
package: Option<&Path>,
|
||||
) -> (Vec<Diagnostic>, Option<ImportMap>) {
|
||||
let is_stub = is_python_stub_file(path);
|
||||
|
||||
// Extract all imports from the AST.
|
||||
let tracker = {
|
||||
let mut tracker = ImportTracker::new(locator, directives, path);
|
||||
let mut tracker = ImportTracker::new(locator, directives, is_stub);
|
||||
tracker.visit_body(python_ast);
|
||||
tracker
|
||||
};
|
||||
@@ -111,7 +114,7 @@ pub fn check_imports(
|
||||
}
|
||||
if settings.rules.enabled(Rule::MissingRequiredImport) {
|
||||
diagnostics.extend(isort::rules::add_required_imports(
|
||||
&blocks, python_ast, locator, stylist, settings, autofix,
|
||||
&blocks, python_ast, locator, stylist, settings, autofix, is_stub,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -183,7 +183,7 @@ pub fn check_logical_lines(
|
||||
if settings.rules.enabled(kind.rule()) {
|
||||
diagnostics.push(Diagnostic {
|
||||
kind,
|
||||
location,
|
||||
location: Location::new(start_loc.row(), 0),
|
||||
end_location: location,
|
||||
fix: Fix::empty(),
|
||||
parent: None,
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::path::Path;
|
||||
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_python_ast::newlines::StrExt;
|
||||
use ruff_python_ast::source_code::{Locator, Stylist};
|
||||
use ruff_python_ast::source_code::{Indexer, Locator, Stylist};
|
||||
|
||||
use crate::registry::Rule;
|
||||
use crate::rules::flake8_executable::helpers::{extract_shebang, ShebangDirective};
|
||||
@@ -24,7 +24,7 @@ pub fn check_physical_lines(
|
||||
path: &Path,
|
||||
locator: &Locator,
|
||||
stylist: &Stylist,
|
||||
commented_lines: &[usize],
|
||||
indexer: &Indexer,
|
||||
doc_lines: &[usize],
|
||||
settings: &Settings,
|
||||
autofix: flags::Autofix,
|
||||
@@ -55,8 +55,11 @@ pub fn check_physical_lines(
|
||||
let fix_shebang_whitespace =
|
||||
autofix.into() && settings.rules.should_fix(Rule::ShebangLeadingWhitespace);
|
||||
|
||||
let mut commented_lines_iter = commented_lines.iter().peekable();
|
||||
let mut commented_lines_iter = indexer.commented_lines().iter().peekable();
|
||||
let mut doc_lines_iter = doc_lines.iter().peekable();
|
||||
|
||||
let string_lines = indexer.string_ranges();
|
||||
|
||||
for (index, line) in locator.contents().universal_newlines().enumerate() {
|
||||
while commented_lines_iter
|
||||
.next_if(|lineno| &(index + 1) == *lineno)
|
||||
@@ -73,15 +76,11 @@ pub fn check_physical_lines(
|
||||
}
|
||||
|
||||
if enforce_blanket_type_ignore {
|
||||
if let Some(diagnostic) = blanket_type_ignore(index, line) {
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
blanket_type_ignore(&mut diagnostics, index, line);
|
||||
}
|
||||
|
||||
if enforce_blanket_noqa {
|
||||
if let Some(diagnostic) = blanket_noqa(index, line) {
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
blanket_noqa(&mut diagnostics, index, line);
|
||||
}
|
||||
|
||||
if enforce_shebang_missing
|
||||
@@ -155,7 +154,7 @@ pub fn check_physical_lines(
|
||||
}
|
||||
|
||||
if enforce_tab_indentation {
|
||||
if let Some(diagnostic) = tab_indentation(index, line) {
|
||||
if let Some(diagnostic) = tab_indentation(index + 1, line, string_lines) {
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
@@ -186,7 +185,7 @@ mod tests {
|
||||
use rustpython_parser::Mode;
|
||||
use std::path::Path;
|
||||
|
||||
use ruff_python_ast::source_code::{Locator, Stylist};
|
||||
use ruff_python_ast::source_code::{Indexer, Locator, Stylist};
|
||||
|
||||
use crate::registry::Rule;
|
||||
use crate::settings::{flags, Settings};
|
||||
@@ -198,6 +197,7 @@ mod tests {
|
||||
let line = "'\u{4e9c}' * 2"; // 7 in UTF-32, 9 in UTF-8.
|
||||
let locator = Locator::new(line);
|
||||
let tokens: Vec<_> = lex(line, Mode::Module).collect();
|
||||
let indexer: Indexer = tokens.as_slice().into();
|
||||
let stylist = Stylist::from_tokens(&tokens, &locator);
|
||||
|
||||
let check_with_max_line_length = |line_length: usize| {
|
||||
@@ -205,7 +205,7 @@ mod tests {
|
||||
Path::new("foo.py"),
|
||||
&locator,
|
||||
&stylist,
|
||||
&[],
|
||||
&indexer,
|
||||
&[],
|
||||
&Settings {
|
||||
line_length,
|
||||
|
||||
@@ -202,6 +202,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {
|
||||
(Pylint, "W0711") => Rule::BinaryOpException,
|
||||
(Pylint, "W1508") => Rule::InvalidEnvvarDefault,
|
||||
(Pylint, "W2901") => Rule::RedefinedLoopName,
|
||||
(Pylint, "E0302") => Rule::UnexpectedSpecialMethodSignature,
|
||||
|
||||
// flake8-builtins
|
||||
(Flake8Builtins, "001") => Rule::BuiltinVariableShadowing,
|
||||
@@ -263,6 +264,8 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {
|
||||
(Flake8Comprehensions, "15") => Rule::UnnecessarySubscriptReversal,
|
||||
(Flake8Comprehensions, "16") => Rule::UnnecessaryComprehension,
|
||||
(Flake8Comprehensions, "17") => Rule::UnnecessaryMap,
|
||||
(Flake8Comprehensions, "18") => Rule::UnnecessaryLiteralWithinDictCall,
|
||||
(Flake8Comprehensions, "19") => Rule::UnnecessaryComprehensionAnyAll,
|
||||
|
||||
// flake8-debugger
|
||||
(Flake8Debugger, "0") => Rule::Debugger,
|
||||
@@ -507,6 +510,12 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {
|
||||
(Flake8Bandit, "506") => Rule::UnsafeYAMLLoad,
|
||||
(Flake8Bandit, "508") => Rule::SnmpInsecureVersion,
|
||||
(Flake8Bandit, "509") => Rule::SnmpWeakCryptography,
|
||||
(Flake8Bandit, "602") => Rule::SubprocessPopenWithShellEqualsTrue,
|
||||
(Flake8Bandit, "603") => Rule::SubprocessWithoutShellEqualsTrue,
|
||||
(Flake8Bandit, "604") => Rule::CallWithShellEqualsTrue,
|
||||
(Flake8Bandit, "605") => Rule::StartProcessWithAShell,
|
||||
(Flake8Bandit, "606") => Rule::StartProcessWithNoShell,
|
||||
(Flake8Bandit, "607") => Rule::StartProcessWithPartialPath,
|
||||
(Flake8Bandit, "608") => Rule::HardcodedSQLExpression,
|
||||
(Flake8Bandit, "612") => Rule::LoggingConfigInsecureListen,
|
||||
(Flake8Bandit, "701") => Rule::Jinja2AutoescapeFalse,
|
||||
@@ -525,6 +534,8 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {
|
||||
|
||||
// flake8-import-conventions
|
||||
(Flake8ImportConventions, "001") => Rule::UnconventionalImportAlias,
|
||||
(Flake8ImportConventions, "002") => Rule::BannedImportAlias,
|
||||
(Flake8ImportConventions, "003") => Rule::BannedImportFrom,
|
||||
|
||||
// flake8-datetimez
|
||||
(Flake8Datetimez, "001") => Rule::CallDatetimeWithoutTzinfo,
|
||||
@@ -573,6 +584,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {
|
||||
(Flake8Pyi, "012") => Rule::PassInClassBody,
|
||||
(Flake8Pyi, "014") => Rule::ArgumentDefaultInStub,
|
||||
(Flake8Pyi, "015") => Rule::AssignmentDefaultInStub,
|
||||
(Flake8Pyi, "016") => Rule::DuplicateUnionMember,
|
||||
(Flake8Pyi, "021") => Rule::DocstringInStub,
|
||||
(Flake8Pyi, "033") => Rule::TypeCommentInStub,
|
||||
|
||||
@@ -608,7 +620,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {
|
||||
(Flake8Pie, "794") => Rule::DuplicateClassFieldDefinition,
|
||||
(Flake8Pie, "796") => Rule::NonUniqueEnums,
|
||||
(Flake8Pie, "800") => Rule::UnnecessarySpread,
|
||||
(Flake8Pie, "802") => Rule::UnnecessaryComprehensionAnyAll,
|
||||
(Flake8Pie, "804") => Rule::UnnecessaryDictKwargs,
|
||||
(Flake8Pie, "807") => Rule::ReimplementedListBuiltin,
|
||||
(Flake8Pie, "810") => Rule::MultipleStartsEndsWith,
|
||||
@@ -700,6 +711,8 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {
|
||||
(Ruff, "005") => Rule::CollectionLiteralConcatenation,
|
||||
(Ruff, "006") => Rule::AsyncioDanglingTask,
|
||||
(Ruff, "007") => Rule::PairwiseOverZipped,
|
||||
(Ruff, "008") => Rule::MutableDataclassDefault,
|
||||
(Ruff, "009") => Rule::FunctionCallInDataclassDefaultArgument,
|
||||
(Ruff, "100") => Rule::UnusedNOQA,
|
||||
|
||||
// flake8-django
|
||||
|
||||
@@ -9,7 +9,8 @@ use rustpython_parser::Tok;
|
||||
use crate::settings::Settings;
|
||||
|
||||
bitflags! {
|
||||
pub struct Flags: u32 {
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct Flags: u8 {
|
||||
const NOQA = 0b0000_0001;
|
||||
const ISORT = 0b0000_0010;
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ pub(crate) static GOOGLE_SECTIONS: &[SectionKind] = &[
|
||||
SectionKind::KeywordArguments,
|
||||
SectionKind::Note,
|
||||
SectionKind::Notes,
|
||||
SectionKind::OtherArgs,
|
||||
SectionKind::OtherArguments,
|
||||
SectionKind::Return,
|
||||
SectionKind::Tip,
|
||||
SectionKind::Todo,
|
||||
|
||||
@@ -14,6 +14,7 @@ pub(crate) static NUMPY_SECTIONS: &[SectionKind] = &[
|
||||
SectionKind::Yields,
|
||||
// NumPy-only
|
||||
SectionKind::ExtendedSummary,
|
||||
SectionKind::OtherParams,
|
||||
SectionKind::OtherParameters,
|
||||
SectionKind::Parameters,
|
||||
SectionKind::ShortSummary,
|
||||
|
||||
@@ -22,6 +22,9 @@ pub enum SectionKind {
|
||||
Methods,
|
||||
Note,
|
||||
Notes,
|
||||
OtherArgs,
|
||||
OtherArguments,
|
||||
OtherParams,
|
||||
OtherParameters,
|
||||
Parameters,
|
||||
Raises,
|
||||
@@ -59,6 +62,9 @@ impl SectionKind {
|
||||
"methods" => Some(Self::Methods),
|
||||
"note" => Some(Self::Note),
|
||||
"notes" => Some(Self::Notes),
|
||||
"other args" => Some(Self::OtherArgs),
|
||||
"other arguments" => Some(Self::OtherArguments),
|
||||
"other params" => Some(Self::OtherParams),
|
||||
"other parameters" => Some(Self::OtherParameters),
|
||||
"parameters" => Some(Self::Parameters),
|
||||
"raises" => Some(Self::Raises),
|
||||
@@ -97,6 +103,9 @@ impl SectionKind {
|
||||
Self::Methods => "Methods",
|
||||
Self::Note => "Note",
|
||||
Self::Notes => "Notes",
|
||||
Self::OtherArgs => "Other Args",
|
||||
Self::OtherArguments => "Other Arguments",
|
||||
Self::OtherParams => "Other Params",
|
||||
Self::OtherParameters => "Other Parameters",
|
||||
Self::Parameters => "Parameters",
|
||||
Self::Raises => "Raises",
|
||||
|
||||
@@ -1,36 +1,21 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use globset::GlobMatcher;
|
||||
use log::debug;
|
||||
use path_absolutize::{path_dedot, Absolutize};
|
||||
|
||||
use crate::registry::RuleSet;
|
||||
|
||||
/// Extract the absolute path and basename (as strings) from a Path.
|
||||
pub fn extract_path_names(path: &Path) -> Result<(&str, &str)> {
|
||||
let file_path = path
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow!("Unable to parse filename: {:?}", path))?;
|
||||
let file_basename = path
|
||||
.file_name()
|
||||
.ok_or_else(|| anyhow!("Unable to parse filename: {:?}", path))?
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow!("Unable to parse filename: {:?}", path))?;
|
||||
Ok((file_path, file_basename))
|
||||
}
|
||||
|
||||
/// Create a set with codes matching the pattern/code pairs.
|
||||
pub(crate) fn ignores_from_path(
|
||||
path: &Path,
|
||||
pattern_code_pairs: &[(GlobMatcher, GlobMatcher, RuleSet)],
|
||||
) -> RuleSet {
|
||||
let (file_path, file_basename) = extract_path_names(path).expect("Unable to parse filename");
|
||||
|
||||
let file_name = path.file_name().expect("Unable to parse filename");
|
||||
pattern_code_pairs
|
||||
.iter()
|
||||
.filter_map(|(absolute, basename, rules)| {
|
||||
if basename.is_match(file_basename) {
|
||||
if basename.is_match(file_name) {
|
||||
debug!(
|
||||
"Adding per-file ignores for {:?} due to basename match on {:?}: {:?}",
|
||||
path,
|
||||
@@ -38,7 +23,7 @@ pub(crate) fn ignores_from_path(
|
||||
rules
|
||||
);
|
||||
Some(rules)
|
||||
} else if absolute.is_match(file_path) {
|
||||
} else if absolute.is_match(path) {
|
||||
debug!(
|
||||
"Adding per-file ignores for {:?} due to absolute match on {:?}: {:?}",
|
||||
path,
|
||||
|
||||
@@ -174,7 +174,7 @@ fn match_docstring_end(body: &[Stmt]) -> Option<Location> {
|
||||
/// along with a trailing newline suffix.
|
||||
fn end_of_statement_insertion(stmt: &Stmt, locator: &Locator, stylist: &Stylist) -> Insertion {
|
||||
let location = stmt.end_location.unwrap();
|
||||
let mut tokens = lexer::lex_located(locator.skip(location), Mode::Module, location).flatten();
|
||||
let mut tokens = lexer::lex_located(locator.after(location), Mode::Module, location).flatten();
|
||||
if let Some((.., Tok::Semi, end)) = tokens.next() {
|
||||
// If the first token after the docstring is a semicolon, insert after the semicolon as an
|
||||
// inline statement;
|
||||
@@ -207,7 +207,7 @@ fn top_of_file_insertion(body: &[Stmt], locator: &Locator, stylist: &Stylist) ->
|
||||
let mut location = if let Some(location) = match_docstring_end(body) {
|
||||
// If the first token after the docstring is a semicolon, insert after the semicolon as an
|
||||
// inline statement;
|
||||
let first_token = lexer::lex_located(locator.skip(location), Mode::Module, location)
|
||||
let first_token = lexer::lex_located(locator.after(location), Mode::Module, location)
|
||||
.flatten()
|
||||
.next();
|
||||
if let Some((.., Tok::Semi, end)) = first_token {
|
||||
@@ -222,7 +222,7 @@ fn top_of_file_insertion(body: &[Stmt], locator: &Locator, stylist: &Stylist) ->
|
||||
|
||||
// Skip over any comments and empty lines.
|
||||
for (.., tok, end) in
|
||||
lexer::lex_located(locator.skip(location), Mode::Module, location).flatten()
|
||||
lexer::lex_located(locator.after(location), Mode::Module, location).flatten()
|
||||
{
|
||||
if matches!(tok, Tok::Comment(..) | Tok::Newline) {
|
||||
location = Location::new(end.row() + 1, 0);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::borrow::Cow;
|
||||
use std::ops::Deref;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
@@ -10,7 +11,7 @@ use rustpython_parser::ParseError;
|
||||
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_python_ast::imports::ImportMap;
|
||||
use ruff_python_ast::source_code::{Indexer, Locator, Stylist};
|
||||
use ruff_python_ast::source_code::{Indexer, Locator, SourceFileBuilder, Stylist};
|
||||
use ruff_python_stdlib::path::is_python_stub_file;
|
||||
|
||||
use crate::autofix::fix_file;
|
||||
@@ -22,7 +23,7 @@ use crate::checkers::physical_lines::check_physical_lines;
|
||||
use crate::checkers::tokens::check_tokens;
|
||||
use crate::directives::Directives;
|
||||
use crate::doc_lines::{doc_lines_from_ast, doc_lines_from_tokens};
|
||||
use crate::message::{Message, Source};
|
||||
use crate::message::Message;
|
||||
use crate::noqa::add_noqa;
|
||||
use crate::registry::{AsRule, Rule};
|
||||
use crate::rules::pycodestyle;
|
||||
@@ -196,13 +197,7 @@ pub fn check_path(
|
||||
.any(|rule_code| rule_code.lint_source().is_physical_lines())
|
||||
{
|
||||
diagnostics.extend(check_physical_lines(
|
||||
path,
|
||||
locator,
|
||||
stylist,
|
||||
indexer.commented_lines(),
|
||||
&doc_lines,
|
||||
settings,
|
||||
autofix,
|
||||
path, locator, stylist, indexer, &doc_lines, settings, autofix,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -359,28 +354,41 @@ pub fn lint_only(
|
||||
autofix,
|
||||
);
|
||||
|
||||
// Convert from diagnostics to messages.
|
||||
let path_lossy = path.to_string_lossy();
|
||||
result.map(|(messages, imports)| {
|
||||
result.map(|(diagnostics, imports)| {
|
||||
(
|
||||
messages
|
||||
.into_iter()
|
||||
.map(|diagnostic| {
|
||||
let source = if settings.show_source {
|
||||
Some(Source::from_diagnostic(&diagnostic, &locator))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let lineno = diagnostic.location.row();
|
||||
let noqa_row = *directives.noqa_line_for.get(&lineno).unwrap_or(&lineno);
|
||||
Message::from_diagnostic(diagnostic, path_lossy.to_string(), source, noqa_row)
|
||||
})
|
||||
.collect(),
|
||||
diagnostics_to_messages(diagnostics, path, settings, &locator, &directives),
|
||||
imports,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Convert from diagnostics to messages.
|
||||
fn diagnostics_to_messages(
|
||||
diagnostics: Vec<Diagnostic>,
|
||||
path: &Path,
|
||||
settings: &Settings,
|
||||
locator: &Locator,
|
||||
directives: &Directives,
|
||||
) -> Vec<Message> {
|
||||
let file = once_cell::unsync::Lazy::new(|| {
|
||||
let mut builder = SourceFileBuilder::new(&path.to_string_lossy());
|
||||
if settings.show_source {
|
||||
builder.set_source_code(&locator.to_source_code());
|
||||
}
|
||||
|
||||
builder.finish()
|
||||
});
|
||||
|
||||
diagnostics
|
||||
.into_iter()
|
||||
.map(|diagnostic| {
|
||||
let lineno = diagnostic.location.row();
|
||||
let noqa_row = *directives.noqa_line_for.get(&lineno).unwrap_or(&lineno);
|
||||
Message::from_diagnostic(diagnostic, file.deref().clone(), noqa_row)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Generate `Diagnostic`s from source code content, iteratively autofixing
|
||||
/// until stable.
|
||||
pub fn lint_fix<'a>(
|
||||
@@ -502,30 +510,10 @@ This indicates a bug in `{}`. If you could open an issue at:
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to messages.
|
||||
let path_lossy = path.to_string_lossy();
|
||||
return Ok(FixerResult {
|
||||
result: result.map(|(messages, imports)| {
|
||||
result: result.map(|(diagnostics, imports)| {
|
||||
(
|
||||
messages
|
||||
.into_iter()
|
||||
.map(|diagnostic| {
|
||||
let source = if settings.show_source {
|
||||
Some(Source::from_diagnostic(&diagnostic, &locator))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let lineno = diagnostic.location.row();
|
||||
let noqa_row =
|
||||
*directives.noqa_line_for.get(&lineno).unwrap_or(&lineno);
|
||||
Message::from_diagnostic(
|
||||
diagnostic,
|
||||
path_lossy.to_string(),
|
||||
source,
|
||||
noqa_row,
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
diagnostics_to_messages(diagnostics, path, settings, &locator, &directives),
|
||||
imports,
|
||||
)
|
||||
}),
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
pub use rustpython_parser::ast::Location;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, DiagnosticKind, Fix};
|
||||
use ruff_python_ast::source_code::Locator;
|
||||
use ruff_python_ast::types::Range;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Message {
|
||||
pub kind: DiagnosticKind,
|
||||
pub location: Location,
|
||||
pub end_location: Location,
|
||||
pub fix: Fix,
|
||||
pub filename: String,
|
||||
pub source: Option<Source>,
|
||||
pub noqa_row: usize,
|
||||
}
|
||||
|
||||
impl Message {
|
||||
pub fn from_diagnostic(
|
||||
diagnostic: Diagnostic,
|
||||
filename: String,
|
||||
source: Option<Source>,
|
||||
noqa_row: usize,
|
||||
) -> Self {
|
||||
Self {
|
||||
kind: diagnostic.kind,
|
||||
location: Location::new(diagnostic.location.row(), diagnostic.location.column() + 1),
|
||||
end_location: Location::new(
|
||||
diagnostic.end_location.row(),
|
||||
diagnostic.end_location.column() + 1,
|
||||
),
|
||||
fix: diagnostic.fix,
|
||||
filename,
|
||||
source,
|
||||
noqa_row,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for Message {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
(&self.filename, self.location.row(), self.location.column()).cmp(&(
|
||||
&other.filename,
|
||||
other.location.row(),
|
||||
other.location.column(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Message {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Source {
|
||||
pub contents: String,
|
||||
pub range: (usize, usize),
|
||||
}
|
||||
|
||||
impl Source {
|
||||
pub fn from_diagnostic(diagnostic: &Diagnostic, locator: &Locator) -> Self {
|
||||
let location = Location::new(diagnostic.location.row(), 0);
|
||||
// Diagnostics can already extend one-past-the-end. If they do, though, then
|
||||
// they'll end at the start of a line. We need to avoid extending by yet another
|
||||
// line past-the-end.
|
||||
let end_location = if diagnostic.end_location.column() == 0 {
|
||||
diagnostic.end_location
|
||||
} else {
|
||||
Location::new(diagnostic.end_location.row() + 1, 0)
|
||||
};
|
||||
let source = locator.slice(Range::new(location, end_location));
|
||||
let num_chars_in_range = locator
|
||||
.slice(Range::new(diagnostic.location, diagnostic.end_location))
|
||||
.chars()
|
||||
.count();
|
||||
Source {
|
||||
contents: source.to_string(),
|
||||
range: (
|
||||
diagnostic.location.column(),
|
||||
diagnostic.location.column() + num_chars_in_range,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
53
crates/ruff/src/message/azure.rs
Normal file
53
crates/ruff/src/message/azure.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use crate::message::{Emitter, EmitterContext, Message};
|
||||
use crate::registry::AsRule;
|
||||
use std::io::Write;
|
||||
|
||||
/// Generate error logging commands for Azure Pipelines format.
|
||||
/// See [documentation](https://learn.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands?view=azure-devops&tabs=bash#logissue-log-an-error-or-warning)
|
||||
#[derive(Default)]
|
||||
pub struct AzureEmitter;
|
||||
|
||||
impl Emitter for AzureEmitter {
|
||||
fn emit(
|
||||
&mut self,
|
||||
writer: &mut dyn Write,
|
||||
messages: &[Message],
|
||||
context: &EmitterContext,
|
||||
) -> anyhow::Result<()> {
|
||||
for message in messages {
|
||||
let (line, col) = if context.is_jupyter_notebook(message.filename()) {
|
||||
// We can't give a reasonable location for the structured formats,
|
||||
// so we show one that's clearly a fallback
|
||||
(1, 0)
|
||||
} else {
|
||||
(message.location.row(), message.location.column())
|
||||
};
|
||||
|
||||
writeln!(
|
||||
writer,
|
||||
"##vso[task.logissue type=error\
|
||||
;sourcepath={filename};linenumber={line};columnnumber={col};code={code};]{body}",
|
||||
filename = message.filename(),
|
||||
code = message.kind.rule().noqa_code(),
|
||||
body = message.kind.body,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::message::tests::{capture_emitter_output, create_messages};
|
||||
use crate::message::AzureEmitter;
|
||||
use insta::assert_snapshot;
|
||||
|
||||
#[test]
|
||||
fn output() {
|
||||
let mut emitter = AzureEmitter::default();
|
||||
let content = capture_emitter_output(&mut emitter, &create_messages());
|
||||
|
||||
assert_snapshot!(content);
|
||||
}
|
||||
}
|
||||
194
crates/ruff/src/message/diff.rs
Normal file
194
crates/ruff/src/message/diff.rs
Normal file
@@ -0,0 +1,194 @@
|
||||
use crate::message::Message;
|
||||
use colored::{Color, ColoredString, Colorize, Styles};
|
||||
use ruff_diagnostics::Fix;
|
||||
use ruff_python_ast::source_code::{OneIndexed, SourceCode};
|
||||
use ruff_python_ast::types::Range;
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
use similar::{ChangeTag, TextDiff};
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
/// Renders a diff that shows the code fixes.
|
||||
///
|
||||
/// The implementation isn't fully fledged out and only used by tests. Before using in production, try
|
||||
/// * Improve layout
|
||||
/// * Replace tabs with spaces for a consistent experience across terminals
|
||||
/// * Replace zero-width whitespaces
|
||||
/// * Print a simpler diff if only a single line has changed
|
||||
/// * Compute the diff from the [`Edit`] because diff calculation is expensive.
|
||||
pub(super) struct Diff<'a> {
|
||||
fix: &'a Fix,
|
||||
source_code: SourceCode<'a, 'a>,
|
||||
}
|
||||
|
||||
impl<'a> Diff<'a> {
|
||||
pub fn from_message(message: &'a Message) -> Option<Diff> {
|
||||
match message.file.source_code() {
|
||||
Some(source_code) if !message.fix.is_empty() => Some(Diff {
|
||||
source_code,
|
||||
fix: &message.fix,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Diff<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
let mut output = String::with_capacity(self.source_code.text().len());
|
||||
let mut last_end = TextSize::default();
|
||||
|
||||
for edit in self.fix.edits() {
|
||||
let edit_range = self
|
||||
.source_code
|
||||
.text_range(Range::new(edit.location(), edit.end_location()));
|
||||
output.push_str(&self.source_code.text()[TextRange::new(last_end, edit_range.start())]);
|
||||
output.push_str(edit.content().unwrap_or_default());
|
||||
last_end = edit_range.end();
|
||||
}
|
||||
|
||||
output.push_str(&self.source_code.text()[usize::from(last_end)..]);
|
||||
|
||||
let diff = TextDiff::from_lines(self.source_code.text(), &output);
|
||||
|
||||
writeln!(f, "{}", "ℹ Suggested fix".blue())?;
|
||||
|
||||
let (largest_old, largest_new) = diff
|
||||
.ops()
|
||||
.last()
|
||||
.map(|op| (op.old_range().start, op.new_range().start))
|
||||
.unwrap_or_default();
|
||||
|
||||
let digit_with =
|
||||
calculate_print_width(OneIndexed::from_zero_indexed(largest_new.max(largest_old)));
|
||||
|
||||
for (idx, group) in diff.grouped_ops(3).iter().enumerate() {
|
||||
if idx > 0 {
|
||||
writeln!(f, "{:-^1$}", "-", 80)?;
|
||||
}
|
||||
for op in group {
|
||||
for change in diff.iter_inline_changes(op) {
|
||||
let sign = match change.tag() {
|
||||
ChangeTag::Delete => "-",
|
||||
ChangeTag::Insert => "+",
|
||||
ChangeTag::Equal => " ",
|
||||
};
|
||||
|
||||
let line_style = LineStyle::from(change.tag());
|
||||
|
||||
let old_index = change.old_index().map(OneIndexed::from_zero_indexed);
|
||||
let new_index = change.new_index().map(OneIndexed::from_zero_indexed);
|
||||
|
||||
write!(
|
||||
f,
|
||||
"{} {} |{}",
|
||||
Line {
|
||||
index: old_index,
|
||||
width: digit_with
|
||||
},
|
||||
Line {
|
||||
index: new_index,
|
||||
width: digit_with
|
||||
},
|
||||
line_style.apply_to(sign).bold()
|
||||
)?;
|
||||
|
||||
for (emphasized, value) in change.iter_strings_lossy() {
|
||||
if emphasized {
|
||||
write!(f, "{}", line_style.apply_to(&value).underline().on_black())?;
|
||||
} else {
|
||||
write!(f, "{}", line_style.apply_to(&value))?;
|
||||
}
|
||||
}
|
||||
if change.missing_newline() {
|
||||
writeln!(f)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct LineStyle {
|
||||
fgcolor: Option<Color>,
|
||||
style: Option<Styles>,
|
||||
}
|
||||
|
||||
impl LineStyle {
|
||||
fn apply_to(&self, input: &str) -> ColoredString {
|
||||
let mut colored = ColoredString::from(input);
|
||||
if let Some(color) = self.fgcolor {
|
||||
colored = colored.color(color);
|
||||
}
|
||||
|
||||
if let Some(style) = self.style {
|
||||
match style {
|
||||
Styles::Clear => colored.clear(),
|
||||
Styles::Bold => colored.bold(),
|
||||
Styles::Dimmed => colored.dimmed(),
|
||||
Styles::Underline => colored.underline(),
|
||||
Styles::Reversed => colored.reversed(),
|
||||
Styles::Italic => colored.italic(),
|
||||
Styles::Blink => colored.blink(),
|
||||
Styles::Hidden => colored.hidden(),
|
||||
Styles::Strikethrough => colored.strikethrough(),
|
||||
}
|
||||
} else {
|
||||
colored
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ChangeTag> for LineStyle {
|
||||
fn from(value: ChangeTag) -> Self {
|
||||
match value {
|
||||
ChangeTag::Equal => LineStyle {
|
||||
fgcolor: None,
|
||||
style: Some(Styles::Dimmed),
|
||||
},
|
||||
ChangeTag::Delete => LineStyle {
|
||||
fgcolor: Some(Color::Red),
|
||||
style: None,
|
||||
},
|
||||
ChangeTag::Insert => LineStyle {
|
||||
fgcolor: Some(Color::Green),
|
||||
style: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Line {
|
||||
index: Option<OneIndexed>,
|
||||
width: NonZeroUsize,
|
||||
}
|
||||
|
||||
impl Display for Line {
|
||||
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
|
||||
match self.index {
|
||||
None => {
|
||||
for _ in 0..self.width.get() {
|
||||
f.write_str(" ")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Some(idx) => write!(f, "{:<width$}", idx, width = self.width.get()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate the length of the string representation of `value`
|
||||
pub(super) fn calculate_print_width(mut value: OneIndexed) -> NonZeroUsize {
|
||||
const TEN: OneIndexed = OneIndexed::from_zero_indexed(9);
|
||||
|
||||
let mut width = OneIndexed::ONE;
|
||||
|
||||
while value >= TEN {
|
||||
value = OneIndexed::new(value.get() / 10).unwrap_or(OneIndexed::MIN);
|
||||
width = width.checked_add(1).unwrap();
|
||||
}
|
||||
|
||||
width
|
||||
}
|
||||
65
crates/ruff/src/message/github.rs
Normal file
65
crates/ruff/src/message/github.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
use crate::fs::relativize_path;
|
||||
use crate::message::{Emitter, EmitterContext, Message};
|
||||
use crate::registry::AsRule;
|
||||
use std::io::Write;
|
||||
|
||||
/// Generate error workflow command in GitHub Actions format.
|
||||
/// See: [GitHub documentation](https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message)
|
||||
#[derive(Default)]
|
||||
pub struct GithubEmitter;
|
||||
|
||||
impl Emitter for GithubEmitter {
|
||||
fn emit(
|
||||
&mut self,
|
||||
writer: &mut dyn Write,
|
||||
messages: &[Message],
|
||||
context: &EmitterContext,
|
||||
) -> anyhow::Result<()> {
|
||||
for message in messages {
|
||||
let (row, column) = if context.is_jupyter_notebook(message.filename()) {
|
||||
// We can't give a reasonable location for the structured formats,
|
||||
// so we show one that's clearly a fallback
|
||||
(1, 0)
|
||||
} else {
|
||||
(message.location.row(), message.location.column())
|
||||
};
|
||||
|
||||
write!(
|
||||
writer,
|
||||
"::error title=Ruff \
|
||||
({code}),file={file},line={row},col={column},endLine={end_row},endColumn={end_column}::",
|
||||
code = message.kind.rule().noqa_code(),
|
||||
file = message.filename(),
|
||||
row = message.location.row(),
|
||||
column = message.location.column(),
|
||||
end_row = message.end_location.row(),
|
||||
end_column = message.end_location.column(),
|
||||
)?;
|
||||
|
||||
writeln!(
|
||||
writer,
|
||||
"{path}:{row}:{column}: {code} {body}",
|
||||
path = relativize_path(message.filename()),
|
||||
code = message.kind.rule().noqa_code(),
|
||||
body = message.kind.body,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::message::tests::{capture_emitter_output, create_messages};
|
||||
use crate::message::GithubEmitter;
|
||||
use insta::assert_snapshot;
|
||||
|
||||
#[test]
|
||||
fn output() {
|
||||
let mut emitter = GithubEmitter::default();
|
||||
let content = capture_emitter_output(&mut emitter, &create_messages());
|
||||
|
||||
assert_snapshot!(content);
|
||||
}
|
||||
}
|
||||
153
crates/ruff/src/message/gitlab.rs
Normal file
153
crates/ruff/src/message/gitlab.rs
Normal file
@@ -0,0 +1,153 @@
|
||||
use crate::fs::{relativize_path, relativize_path_to};
|
||||
use crate::message::{Emitter, EmitterContext, Message};
|
||||
use crate::registry::AsRule;
|
||||
use serde::ser::SerializeSeq;
|
||||
use serde::{Serialize, Serializer};
|
||||
use serde_json::json;
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::io::Write;
|
||||
|
||||
/// Generate JSON with violations in GitLab CI format
|
||||
// https://docs.gitlab.com/ee/ci/testing/code_quality.html#implement-a-custom-tool
|
||||
pub struct GitlabEmitter {
|
||||
project_dir: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for GitlabEmitter {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
project_dir: std::env::var("CI_PROJECT_DIR").ok(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Emitter for GitlabEmitter {
|
||||
fn emit(
|
||||
&mut self,
|
||||
writer: &mut dyn Write,
|
||||
messages: &[Message],
|
||||
context: &EmitterContext,
|
||||
) -> anyhow::Result<()> {
|
||||
serde_json::to_writer_pretty(
|
||||
writer,
|
||||
&SerializedMessages {
|
||||
messages,
|
||||
context,
|
||||
project_dir: self.project_dir.as_deref(),
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct SerializedMessages<'a> {
|
||||
messages: &'a [Message],
|
||||
context: &'a EmitterContext<'a>,
|
||||
project_dir: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl Serialize for SerializedMessages<'_> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut s = serializer.serialize_seq(Some(self.messages.len()))?;
|
||||
|
||||
for message in self.messages {
|
||||
let lines = if self.context.is_jupyter_notebook(message.filename()) {
|
||||
// We can't give a reasonable location for the structured formats,
|
||||
// so we show one that's clearly a fallback
|
||||
json!({
|
||||
"begin": 1,
|
||||
"end": 1
|
||||
})
|
||||
} else {
|
||||
json!({
|
||||
"begin": message.location.row(),
|
||||
"end": message.end_location.row()
|
||||
})
|
||||
};
|
||||
|
||||
let path = self.project_dir.as_ref().map_or_else(
|
||||
|| relativize_path(message.filename()),
|
||||
|project_dir| relativize_path_to(message.filename(), project_dir),
|
||||
);
|
||||
|
||||
let value = json!({
|
||||
"description": format!("({}) {}", message.kind.rule().noqa_code(), message.kind.body),
|
||||
"severity": "major",
|
||||
"fingerprint": fingerprint(message),
|
||||
"location": {
|
||||
"path": path,
|
||||
"lines": lines
|
||||
}
|
||||
});
|
||||
|
||||
s.serialize_element(&value)?;
|
||||
}
|
||||
|
||||
s.end()
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a unique fingerprint to identify a violation.
|
||||
fn fingerprint(message: &Message) -> String {
|
||||
let Message {
|
||||
kind,
|
||||
location,
|
||||
end_location,
|
||||
fix: _fix,
|
||||
file,
|
||||
noqa_row: _noqa_row,
|
||||
} = message;
|
||||
|
||||
let mut hasher = DefaultHasher::new();
|
||||
|
||||
kind.rule().hash(&mut hasher);
|
||||
location.row().hash(&mut hasher);
|
||||
location.column().hash(&mut hasher);
|
||||
end_location.row().hash(&mut hasher);
|
||||
end_location.column().hash(&mut hasher);
|
||||
file.name().hash(&mut hasher);
|
||||
|
||||
format!("{:x}", hasher.finish())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::message::tests::{capture_emitter_output, create_messages};
|
||||
use crate::message::GitlabEmitter;
|
||||
use insta::assert_snapshot;
|
||||
|
||||
#[test]
|
||||
fn output() {
|
||||
let mut emitter = GitlabEmitter::default();
|
||||
let content = capture_emitter_output(&mut emitter, &create_messages());
|
||||
|
||||
assert_snapshot!(redact_fingerprint(&content));
|
||||
}
|
||||
|
||||
// Redact the fingerprint because the default hasher isn't stable across platforms.
|
||||
fn redact_fingerprint(content: &str) -> String {
|
||||
static FINGERPRINT_HAY_KEY: &str = r#""fingerprint": ""#;
|
||||
|
||||
let mut output = String::with_capacity(content.len());
|
||||
let mut last = 0;
|
||||
|
||||
for (start, _) in content.match_indices(FINGERPRINT_HAY_KEY) {
|
||||
let fingerprint_hash_start = start + FINGERPRINT_HAY_KEY.len();
|
||||
output.push_str(&content[last..fingerprint_hash_start]);
|
||||
output.push_str("<redacted>");
|
||||
last = fingerprint_hash_start
|
||||
+ content[fingerprint_hash_start..]
|
||||
.find('"')
|
||||
.expect("Expected terminating quote");
|
||||
}
|
||||
|
||||
output.push_str(&content[last..]);
|
||||
|
||||
output
|
||||
}
|
||||
}
|
||||
190
crates/ruff/src/message/grouped.rs
Normal file
190
crates/ruff/src/message/grouped.rs
Normal file
@@ -0,0 +1,190 @@
|
||||
use crate::fs::relativize_path;
|
||||
use crate::jupyter::JupyterIndex;
|
||||
use crate::message::text::{MessageCodeFrame, RuleCodeAndBody};
|
||||
use crate::message::{group_messages_by_filename, Emitter, EmitterContext, Message};
|
||||
use colored::Colorize;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::io::Write;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct GroupedEmitter {
|
||||
show_fix_status: bool,
|
||||
}
|
||||
|
||||
impl GroupedEmitter {
|
||||
#[must_use]
|
||||
pub fn with_show_fix_status(mut self, show_fix_status: bool) -> Self {
|
||||
self.show_fix_status = show_fix_status;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Emitter for GroupedEmitter {
|
||||
fn emit(
|
||||
&mut self,
|
||||
writer: &mut dyn Write,
|
||||
messages: &[Message],
|
||||
context: &EmitterContext,
|
||||
) -> anyhow::Result<()> {
|
||||
for (filename, messages) in group_messages_by_filename(messages) {
|
||||
// Compute the maximum number of digits in the row and column, for messages in
|
||||
// this file.
|
||||
let row_length = num_digits(
|
||||
messages
|
||||
.iter()
|
||||
.map(|message| message.location.row())
|
||||
.max()
|
||||
.unwrap(),
|
||||
);
|
||||
let column_length = num_digits(
|
||||
messages
|
||||
.iter()
|
||||
.map(|message| message.location.column())
|
||||
.max()
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
// Print the filename.
|
||||
writeln!(writer, "{}:", relativize_path(filename).underline())?;
|
||||
|
||||
// Print each message.
|
||||
for message in messages {
|
||||
write!(
|
||||
writer,
|
||||
"{}",
|
||||
DisplayGroupedMessage {
|
||||
message,
|
||||
show_fix_status: self.show_fix_status,
|
||||
row_length,
|
||||
column_length,
|
||||
jupyter_index: context.jupyter_index(message.filename()),
|
||||
}
|
||||
)?;
|
||||
}
|
||||
writeln!(writer)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct DisplayGroupedMessage<'a> {
|
||||
message: &'a Message,
|
||||
show_fix_status: bool,
|
||||
row_length: usize,
|
||||
column_length: usize,
|
||||
jupyter_index: Option<&'a JupyterIndex>,
|
||||
}
|
||||
|
||||
impl Display for DisplayGroupedMessage<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
let message = self.message;
|
||||
|
||||
write!(
|
||||
f,
|
||||
" {row_padding}",
|
||||
row_padding = " ".repeat(self.row_length - num_digits(message.location.row()))
|
||||
)?;
|
||||
|
||||
// Check if we're working on a jupyter notebook and translate positions with cell accordingly
|
||||
let (row, col) = if let Some(jupyter_index) = self.jupyter_index {
|
||||
write!(
|
||||
f,
|
||||
"cell {cell}{sep}",
|
||||
cell = jupyter_index.row_to_cell[message.location.row()],
|
||||
sep = ":".cyan()
|
||||
)?;
|
||||
(
|
||||
jupyter_index.row_to_row_in_cell[message.location.row()] as usize,
|
||||
message.location.column(),
|
||||
)
|
||||
} else {
|
||||
(message.location.row(), message.location.column())
|
||||
};
|
||||
|
||||
writeln!(
|
||||
f,
|
||||
"{row}{sep}{col}{col_padding} {code_and_body}",
|
||||
sep = ":".cyan(),
|
||||
col_padding = " ".repeat(self.column_length - num_digits(message.location.column())),
|
||||
code_and_body = RuleCodeAndBody {
|
||||
message_kind: &message.kind,
|
||||
show_fix_status: self.show_fix_status
|
||||
},
|
||||
)?;
|
||||
|
||||
{
|
||||
use std::fmt::Write;
|
||||
let mut padded = PadAdapter::new(f);
|
||||
write!(padded, "{}", MessageCodeFrame { message })?;
|
||||
}
|
||||
|
||||
writeln!(f)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn num_digits(n: usize) -> usize {
|
||||
std::iter::successors(Some(n), |n| {
|
||||
let next = n / 10;
|
||||
|
||||
(next > 0).then_some(next)
|
||||
})
|
||||
.count()
|
||||
.max(1)
|
||||
}
|
||||
|
||||
/// Adapter that adds a ' ' at the start of every line without the need to copy the string.
|
||||
/// Inspired by Rust's `debug_struct()` internal implementation that also uses a `PadAdapter`.
|
||||
struct PadAdapter<'buf> {
|
||||
buf: &'buf mut (dyn std::fmt::Write + 'buf),
|
||||
on_newline: bool,
|
||||
}
|
||||
|
||||
impl<'buf> PadAdapter<'buf> {
|
||||
fn new(buf: &'buf mut (dyn std::fmt::Write + 'buf)) -> Self {
|
||||
Self {
|
||||
buf,
|
||||
on_newline: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Write for PadAdapter<'_> {
|
||||
fn write_str(&mut self, s: &str) -> std::fmt::Result {
|
||||
for s in s.split_inclusive('\n') {
|
||||
if self.on_newline {
|
||||
self.buf.write_str(" ")?;
|
||||
}
|
||||
|
||||
self.on_newline = s.ends_with('\n');
|
||||
self.buf.write_str(s)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::message::tests::{capture_emitter_output, create_messages};
|
||||
use crate::message::GroupedEmitter;
|
||||
use insta::assert_snapshot;
|
||||
|
||||
#[test]
|
||||
fn default() {
|
||||
let mut emitter = GroupedEmitter::default();
|
||||
let content = capture_emitter_output(&mut emitter, &create_messages());
|
||||
|
||||
assert_snapshot!(content);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fix_status() {
|
||||
let mut emitter = GroupedEmitter::default().with_show_fix_status(true);
|
||||
let content = capture_emitter_output(&mut emitter, &create_messages());
|
||||
|
||||
assert_snapshot!(content);
|
||||
}
|
||||
}
|
||||
101
crates/ruff/src/message/json.rs
Normal file
101
crates/ruff/src/message/json.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
use crate::message::{Emitter, EmitterContext, Message};
|
||||
use crate::registry::AsRule;
|
||||
use ruff_diagnostics::Edit;
|
||||
use serde::ser::SerializeSeq;
|
||||
use serde::{Serialize, Serializer};
|
||||
use serde_json::json;
|
||||
use std::io::Write;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct JsonEmitter;
|
||||
|
||||
impl Emitter for JsonEmitter {
|
||||
fn emit(
|
||||
&mut self,
|
||||
writer: &mut dyn Write,
|
||||
messages: &[Message],
|
||||
_context: &EmitterContext,
|
||||
) -> anyhow::Result<()> {
|
||||
serde_json::to_writer_pretty(writer, &ExpandedMessages { messages })?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct ExpandedMessages<'a> {
|
||||
messages: &'a [Message],
|
||||
}
|
||||
|
||||
impl Serialize for ExpandedMessages<'_> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut s = serializer.serialize_seq(Some(self.messages.len()))?;
|
||||
|
||||
for message in self.messages {
|
||||
let fix = if message.fix.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(json!({
|
||||
"message": message.kind.suggestion.as_deref(),
|
||||
"edits": &ExpandedEdits { edits: message.fix.edits() },
|
||||
}))
|
||||
};
|
||||
|
||||
let value = json!({
|
||||
"code": message.kind.rule().noqa_code().to_string(),
|
||||
"message": message.kind.body,
|
||||
"fix": fix,
|
||||
"location": message.location,
|
||||
"end_location": message.end_location,
|
||||
"filename": message.filename(),
|
||||
"noqa_row": message.noqa_row
|
||||
});
|
||||
|
||||
s.serialize_element(&value)?;
|
||||
}
|
||||
|
||||
s.end()
|
||||
}
|
||||
}
|
||||
|
||||
struct ExpandedEdits<'a> {
|
||||
edits: &'a [Edit],
|
||||
}
|
||||
|
||||
impl Serialize for ExpandedEdits<'_> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut s = serializer.serialize_seq(Some(self.edits.len()))?;
|
||||
|
||||
for edit in self.edits {
|
||||
let value = json!({
|
||||
"content": edit.content().unwrap_or_default(),
|
||||
"location": edit.location(),
|
||||
"end_location": edit.end_location()
|
||||
});
|
||||
|
||||
s.serialize_element(&value)?;
|
||||
}
|
||||
|
||||
s.end()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::message::tests::{capture_emitter_output, create_messages};
|
||||
use crate::message::JsonEmitter;
|
||||
use insta::assert_snapshot;
|
||||
|
||||
#[test]
|
||||
fn output() {
|
||||
let mut emitter = JsonEmitter::default();
|
||||
let content = capture_emitter_output(&mut emitter, &create_messages());
|
||||
|
||||
assert_snapshot!(content);
|
||||
}
|
||||
}
|
||||
74
crates/ruff/src/message/junit.rs
Normal file
74
crates/ruff/src/message/junit.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
use crate::message::{group_messages_by_filename, Emitter, EmitterContext, Message};
|
||||
use crate::registry::AsRule;
|
||||
use quick_junit::{NonSuccessKind, Report, TestCase, TestCaseStatus, TestSuite};
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct JunitEmitter;
|
||||
|
||||
impl Emitter for JunitEmitter {
|
||||
fn emit(
|
||||
&mut self,
|
||||
writer: &mut dyn Write,
|
||||
messages: &[Message],
|
||||
context: &EmitterContext,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut report = Report::new("ruff");
|
||||
|
||||
for (filename, messages) in group_messages_by_filename(messages) {
|
||||
let mut test_suite = TestSuite::new(filename);
|
||||
test_suite
|
||||
.extra
|
||||
.insert("package".to_string(), "org.ruff".to_string());
|
||||
|
||||
for message in messages {
|
||||
let mut status = TestCaseStatus::non_success(NonSuccessKind::Failure);
|
||||
status.set_message(message.kind.body.clone());
|
||||
let (row, col) = if context.is_jupyter_notebook(message.filename()) {
|
||||
// We can't give a reasonable location for the structured formats,
|
||||
// so we show one that's clearly a fallback
|
||||
(1, 0)
|
||||
} else {
|
||||
(message.location.row(), message.location.column())
|
||||
};
|
||||
|
||||
status.set_description(format!("line {row}, col {col}, {}", message.kind.body));
|
||||
let mut case = TestCase::new(
|
||||
format!("org.ruff.{}", message.kind.rule().noqa_code()),
|
||||
status,
|
||||
);
|
||||
let file_path = Path::new(filename);
|
||||
let file_stem = file_path.file_stem().unwrap().to_str().unwrap();
|
||||
let classname = file_path.parent().unwrap().join(file_stem);
|
||||
case.set_classname(classname.to_str().unwrap());
|
||||
case.extra
|
||||
.insert("line".to_string(), message.location.row().to_string());
|
||||
case.extra
|
||||
.insert("column".to_string(), message.location.column().to_string());
|
||||
|
||||
test_suite.add_test_case(case);
|
||||
}
|
||||
report.add_test_suite(test_suite);
|
||||
}
|
||||
|
||||
report.serialize(writer)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::message::tests::{capture_emitter_output, create_messages};
|
||||
use crate::message::JunitEmitter;
|
||||
use insta::assert_snapshot;
|
||||
|
||||
#[test]
|
||||
fn output() {
|
||||
let mut emitter = JunitEmitter::default();
|
||||
let content = capture_emitter_output(&mut emitter, &create_messages());
|
||||
|
||||
assert_snapshot!(content);
|
||||
}
|
||||
}
|
||||
198
crates/ruff/src/message/mod.rs
Normal file
198
crates/ruff/src/message/mod.rs
Normal file
@@ -0,0 +1,198 @@
|
||||
mod azure;
|
||||
mod diff;
|
||||
mod github;
|
||||
mod gitlab;
|
||||
mod grouped;
|
||||
mod json;
|
||||
mod junit;
|
||||
mod pylint;
|
||||
mod text;
|
||||
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::BTreeMap;
|
||||
use std::io::Write;
|
||||
|
||||
pub use azure::AzureEmitter;
|
||||
pub use github::GithubEmitter;
|
||||
pub use gitlab::GitlabEmitter;
|
||||
pub use grouped::GroupedEmitter;
|
||||
pub use json::JsonEmitter;
|
||||
pub use junit::JunitEmitter;
|
||||
pub use pylint::PylintEmitter;
|
||||
pub use rustpython_parser::ast::Location;
|
||||
pub use text::TextEmitter;
|
||||
|
||||
use crate::jupyter::JupyterIndex;
|
||||
use ruff_diagnostics::{Diagnostic, DiagnosticKind, Fix};
|
||||
use ruff_python_ast::source_code::SourceFile;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct Message {
|
||||
pub kind: DiagnosticKind,
|
||||
pub location: Location,
|
||||
pub end_location: Location,
|
||||
pub fix: Fix,
|
||||
pub file: SourceFile,
|
||||
pub noqa_row: usize,
|
||||
}
|
||||
|
||||
impl Message {
|
||||
pub fn from_diagnostic(diagnostic: Diagnostic, file: SourceFile, noqa_row: usize) -> Self {
|
||||
Self {
|
||||
kind: diagnostic.kind,
|
||||
location: Location::new(diagnostic.location.row(), diagnostic.location.column() + 1),
|
||||
end_location: Location::new(
|
||||
diagnostic.end_location.row(),
|
||||
diagnostic.end_location.column() + 1,
|
||||
),
|
||||
fix: diagnostic.fix,
|
||||
file,
|
||||
noqa_row,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn filename(&self) -> &str {
|
||||
self.file.name()
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for Message {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
(self.filename(), self.location.row(), self.location.column()).cmp(&(
|
||||
other.filename(),
|
||||
other.location.row(),
|
||||
other.location.column(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Message {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
fn group_messages_by_filename(messages: &[Message]) -> BTreeMap<&str, Vec<&Message>> {
|
||||
let mut grouped_messages = BTreeMap::default();
|
||||
for message in messages {
|
||||
grouped_messages
|
||||
.entry(message.filename())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(message);
|
||||
}
|
||||
grouped_messages
|
||||
}
|
||||
|
||||
/// Display format for a [`Message`]s.
|
||||
///
|
||||
/// The emitter serializes a slice of [`Message`]'s and writes them to a [`Write`].
|
||||
pub trait Emitter {
|
||||
/// Serializes the `messages` and writes the output to `writer`.
|
||||
fn emit(
|
||||
&mut self,
|
||||
writer: &mut dyn Write,
|
||||
messages: &[Message],
|
||||
context: &EmitterContext,
|
||||
) -> anyhow::Result<()>;
|
||||
}
|
||||
|
||||
/// Context passed to [`Emitter`].
|
||||
pub struct EmitterContext<'a> {
|
||||
jupyter_indices: &'a FxHashMap<String, JupyterIndex>,
|
||||
}
|
||||
|
||||
impl<'a> EmitterContext<'a> {
|
||||
pub fn new(jupyter_indices: &'a FxHashMap<String, JupyterIndex>) -> Self {
|
||||
Self { jupyter_indices }
|
||||
}
|
||||
|
||||
/// Tests if the file with `name` is a jupyter notebook.
|
||||
pub fn is_jupyter_notebook(&self, name: &str) -> bool {
|
||||
self.jupyter_indices.contains_key(name)
|
||||
}
|
||||
|
||||
/// Returns the file's [`JupyterIndex`] if the file `name` is a jupyter notebook.
|
||||
pub fn jupyter_index(&self, name: &str) -> Option<&JupyterIndex> {
|
||||
self.jupyter_indices.get(name)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::message::{Emitter, EmitterContext, Location, Message};
|
||||
use crate::rules::pyflakes::rules::{UndefinedName, UnusedImport, UnusedVariable};
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix};
|
||||
use ruff_python_ast::source_code::SourceFileBuilder;
|
||||
use ruff_python_ast::types::Range;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
pub(super) fn create_messages() -> Vec<Message> {
|
||||
let fib = r#"import os
|
||||
|
||||
|
||||
def fibonacci(n):
|
||||
"""Compute the nth number in the Fibonacci sequence."""
|
||||
x = 1
|
||||
if n == 0:
|
||||
return 0
|
||||
elif n == 1:
|
||||
return 1
|
||||
else:
|
||||
return fibonacci(n - 1) + fibonacci(n - 2)
|
||||
"#;
|
||||
|
||||
let unused_import = Diagnostic::new(
|
||||
UnusedImport {
|
||||
name: "os".to_string(),
|
||||
context: None,
|
||||
multiple: false,
|
||||
},
|
||||
Range::new(Location::new(1, 7), Location::new(1, 9)),
|
||||
);
|
||||
|
||||
let fib_source = SourceFileBuilder::new("fib.py").source_text(fib).finish();
|
||||
|
||||
let unused_variable = Diagnostic::new(
|
||||
UnusedVariable {
|
||||
name: "x".to_string(),
|
||||
},
|
||||
Range::new(Location::new(6, 4), Location::new(6, 5)),
|
||||
)
|
||||
.with_fix(Fix::new(vec![Edit::deletion(
|
||||
Location::new(6, 4),
|
||||
Location::new(6, 9),
|
||||
)]));
|
||||
|
||||
let file_2 = r#"if a == 1: pass"#;
|
||||
|
||||
let undefined_name = Diagnostic::new(
|
||||
UndefinedName {
|
||||
name: "a".to_string(),
|
||||
},
|
||||
Range::new(Location::new(1, 3), Location::new(1, 4)),
|
||||
);
|
||||
|
||||
let file_2_source = SourceFileBuilder::new("undef.py")
|
||||
.source_text(file_2)
|
||||
.finish();
|
||||
|
||||
vec![
|
||||
Message::from_diagnostic(unused_import, fib_source.clone(), 1),
|
||||
Message::from_diagnostic(unused_variable, fib_source, 1),
|
||||
Message::from_diagnostic(undefined_name, file_2_source, 1),
|
||||
]
|
||||
}
|
||||
|
||||
pub(super) fn capture_emitter_output(
|
||||
emitter: &mut dyn Emitter,
|
||||
messages: &[Message],
|
||||
) -> String {
|
||||
let indices = FxHashMap::default();
|
||||
let context = EmitterContext::new(&indices);
|
||||
let mut output: Vec<u8> = Vec::new();
|
||||
emitter.emit(&mut output, messages, &context).unwrap();
|
||||
|
||||
String::from_utf8(output).expect("Output to be valid UTF-8")
|
||||
}
|
||||
}
|
||||
53
crates/ruff/src/message/pylint.rs
Normal file
53
crates/ruff/src/message/pylint.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use crate::fs::relativize_path;
|
||||
use crate::message::{Emitter, EmitterContext, Message};
|
||||
use crate::registry::AsRule;
|
||||
use std::io::Write;
|
||||
|
||||
/// Generate violations in Pylint format.
|
||||
/// See: [Flake8 documentation](https://flake8.pycqa.org/en/latest/internal/formatters.html#pylint-formatter)
|
||||
#[derive(Default)]
|
||||
pub struct PylintEmitter;
|
||||
|
||||
impl Emitter for PylintEmitter {
|
||||
fn emit(
|
||||
&mut self,
|
||||
writer: &mut dyn Write,
|
||||
messages: &[Message],
|
||||
context: &EmitterContext,
|
||||
) -> anyhow::Result<()> {
|
||||
for message in messages {
|
||||
let row = if context.is_jupyter_notebook(message.filename()) {
|
||||
// We can't give a reasonable location for the structured formats,
|
||||
// so we show one that's clearly a fallback
|
||||
1
|
||||
} else {
|
||||
message.location.row()
|
||||
};
|
||||
|
||||
writeln!(
|
||||
writer,
|
||||
"{path}:{row}: [{code}] {body}",
|
||||
path = relativize_path(message.filename()),
|
||||
code = message.kind.rule().noqa_code(),
|
||||
body = message.kind.body,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::message::tests::{capture_emitter_output, create_messages};
|
||||
use crate::message::PylintEmitter;
|
||||
use insta::assert_snapshot;
|
||||
|
||||
#[test]
|
||||
fn output() {
|
||||
let mut emitter = PylintEmitter::default();
|
||||
let content = capture_emitter_output(&mut emitter, &create_messages());
|
||||
|
||||
assert_snapshot!(content);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
source: crates/ruff/src/message/azure.rs
|
||||
expression: content
|
||||
---
|
||||
##vso[task.logissue type=error;sourcepath=fib.py;linenumber=1;columnnumber=8;code=F401;]`os` imported but unused
|
||||
##vso[task.logissue type=error;sourcepath=fib.py;linenumber=6;columnnumber=5;code=F841;]Local variable `x` is assigned to but never used
|
||||
##vso[task.logissue type=error;sourcepath=undef.py;linenumber=1;columnnumber=4;code=F821;]Undefined name `a`
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
source: crates/ruff/src/message/github.rs
|
||||
expression: content
|
||||
---
|
||||
::error title=Ruff (F401),file=fib.py,line=1,col=8,endLine=1,endColumn=10::fib.py:1:8: F401 `os` imported but unused
|
||||
::error title=Ruff (F841),file=fib.py,line=6,col=5,endLine=6,endColumn=6::fib.py:6:5: F841 Local variable `x` is assigned to but never used
|
||||
::error title=Ruff (F821),file=undef.py,line=1,col=4,endLine=1,endColumn=5::undef.py:1:4: F821 Undefined name `a`
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
source: crates/ruff/src/message/gitlab.rs
|
||||
expression: redact_fingerprint(&content)
|
||||
---
|
||||
[
|
||||
{
|
||||
"description": "(F401) `os` imported but unused",
|
||||
"severity": "major",
|
||||
"fingerprint": "<redacted>",
|
||||
"location": {
|
||||
"path": "fib.py",
|
||||
"lines": {
|
||||
"begin": 1,
|
||||
"end": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "(F841) Local variable `x` is assigned to but never used",
|
||||
"severity": "major",
|
||||
"fingerprint": "<redacted>",
|
||||
"location": {
|
||||
"path": "fib.py",
|
||||
"lines": {
|
||||
"begin": 6,
|
||||
"end": 6
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "(F821) Undefined name `a`",
|
||||
"severity": "major",
|
||||
"fingerprint": "<redacted>",
|
||||
"location": {
|
||||
"path": "undef.py",
|
||||
"lines": {
|
||||
"begin": 1,
|
||||
"end": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
source: crates/ruff/src/message/grouped.rs
|
||||
expression: content
|
||||
---
|
||||
fib.py:
|
||||
1:8 F401 `os` imported but unused
|
||||
|
|
||||
1 | import os
|
||||
| ^^ F401
|
||||
|
|
||||
= help: Remove unused import: `os`
|
||||
|
||||
6:5 F841 Local variable `x` is assigned to but never used
|
||||
|
|
||||
6 | def fibonacci(n):
|
||||
7 | """Compute the nth number in the Fibonacci sequence."""
|
||||
8 | x = 1
|
||||
| ^ F841
|
||||
9 | if n == 0:
|
||||
10 | return 0
|
||||
|
|
||||
= help: Remove assignment to unused variable `x`
|
||||
|
||||
|
||||
undef.py:
|
||||
1:4 F821 Undefined name `a`
|
||||
|
|
||||
1 | if a == 1: pass
|
||||
| ^ F821
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
source: crates/ruff/src/message/grouped.rs
|
||||
expression: content
|
||||
---
|
||||
fib.py:
|
||||
1:8 F401 [*] `os` imported but unused
|
||||
|
|
||||
1 | import os
|
||||
| ^^ F401
|
||||
|
|
||||
= help: Remove unused import: `os`
|
||||
|
||||
6:5 F841 [*] Local variable `x` is assigned to but never used
|
||||
|
|
||||
6 | def fibonacci(n):
|
||||
7 | """Compute the nth number in the Fibonacci sequence."""
|
||||
8 | x = 1
|
||||
| ^ F841
|
||||
9 | if n == 0:
|
||||
10 | return 0
|
||||
|
|
||||
= help: Remove assignment to unused variable `x`
|
||||
|
||||
|
||||
undef.py:
|
||||
1:4 F821 Undefined name `a`
|
||||
|
|
||||
1 | if a == 1: pass
|
||||
| ^ F821
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
---
|
||||
source: crates/ruff/src/message/json.rs
|
||||
expression: content
|
||||
---
|
||||
[
|
||||
{
|
||||
"code": "F401",
|
||||
"message": "`os` imported but unused",
|
||||
"fix": null,
|
||||
"location": {
|
||||
"row": 1,
|
||||
"column": 8
|
||||
},
|
||||
"end_location": {
|
||||
"row": 1,
|
||||
"column": 10
|
||||
},
|
||||
"filename": "fib.py",
|
||||
"noqa_row": 1
|
||||
},
|
||||
{
|
||||
"code": "F841",
|
||||
"message": "Local variable `x` is assigned to but never used",
|
||||
"fix": {
|
||||
"message": "Remove assignment to unused variable `x`",
|
||||
"edits": [
|
||||
{
|
||||
"content": "",
|
||||
"location": {
|
||||
"row": 6,
|
||||
"column": 4
|
||||
},
|
||||
"end_location": {
|
||||
"row": 6,
|
||||
"column": 9
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"location": {
|
||||
"row": 6,
|
||||
"column": 5
|
||||
},
|
||||
"end_location": {
|
||||
"row": 6,
|
||||
"column": 6
|
||||
},
|
||||
"filename": "fib.py",
|
||||
"noqa_row": 1
|
||||
},
|
||||
{
|
||||
"code": "F821",
|
||||
"message": "Undefined name `a`",
|
||||
"fix": null,
|
||||
"location": {
|
||||
"row": 1,
|
||||
"column": 4
|
||||
},
|
||||
"end_location": {
|
||||
"row": 1,
|
||||
"column": 5
|
||||
},
|
||||
"filename": "undef.py",
|
||||
"noqa_row": 1
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
source: crates/ruff/src/message/junit.rs
|
||||
expression: content
|
||||
---
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<testsuites name="ruff" tests="3" failures="3" errors="0">
|
||||
<testsuite name="fib.py" tests="2" disabled="0" errors="0" failures="2" package="org.ruff">
|
||||
<testcase name="org.ruff.F401" classname="fib" line="1" column="8">
|
||||
<failure message="`os` imported but unused">line 1, col 8, `os` imported but unused</failure>
|
||||
</testcase>
|
||||
<testcase name="org.ruff.F841" classname="fib" line="6" column="5">
|
||||
<failure message="Local variable `x` is assigned to but never used">line 6, col 5, Local variable `x` is assigned to but never used</failure>
|
||||
</testcase>
|
||||
</testsuite>
|
||||
<testsuite name="undef.py" tests="1" disabled="0" errors="0" failures="1" package="org.ruff">
|
||||
<testcase name="org.ruff.F821" classname="undef" line="1" column="4">
|
||||
<failure message="Undefined name `a`">line 1, col 4, Undefined name `a`</failure>
|
||||
</testcase>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user