Compare commits

...

97 Commits

Author SHA1 Message Date
Charlie Marsh
eed6866b7e Add relative-path tests for banned-api (#4033) 2023-04-19 16:04:22 -04:00
Charlie Marsh
25a6bfa9ee Bump version to 0.0.262 (#4032) 2023-04-19 15:49:28 -04:00
Charlie Marsh
b3f8f2a5c1 Remove TODO in handle_node_store (#4031) 2023-04-19 15:28:56 -04:00
Charlie Marsh
cc8b5a543b Ignore stub file assignments to value-requiring targets (#4030) 2023-04-19 15:26:00 -04:00
Charlie Marsh
10d5415bcb Ignore certain flake8-pyi errors within function bodies (#4029) 2023-04-19 15:10:29 -04:00
Charlie Marsh
827cbe7f97 Treat non-future function annotations as required-at-runtime (#4028) 2023-04-19 14:43:55 -04:00
Charlie Marsh
0d84517fbc Use module path resolver for relative autofix (#4027) 2023-04-19 14:43:45 -04:00
Charlie Marsh
7fa1da20fb Support relative imports in banned-api enforcement (#4025) 2023-04-19 14:30:13 -04:00
Francesco Nuzzo
f13a161ead remove unnecessary f-string formatting (#4026) 2023-04-19 18:14:33 +00:00
Charlie Marsh
c4cda301aa Ignore relative imports in banned-api rules (#4024) 2023-04-19 13:30:08 -04:00
Charlie Marsh
13fda30051 Refactor flake8_tidy_imports rules to consistently take Checker (#4023) 2023-04-19 16:42:15 +00:00
Micha Reiser
a3146ab1ca Fix (doc-)line-too-long start location (#4006) 2023-04-19 08:42:28 +02:00
Micha Reiser
c0cf87356e Set non-empty range for indentation diagnostics (#4005) 2023-04-18 16:26:13 +02:00
Andrei Grazhdankov
6c3e4ef441 Add Robyn to user list (#4008) 2023-04-18 09:51:20 -04:00
Charlie Marsh
6c038830a8 Ignore argument assignments when enforcing RET504 (#4004) 2023-04-18 03:22:38 +00:00
Charlie Marsh
064a293b80 Fix defaults for section-order (#4003) 2023-04-18 03:00:17 +00:00
Charlie Marsh
79c47e29ee Avoid short-circuiting when detecting RET rules (#4002) 2023-04-17 22:52:26 -04:00
Charlie Marsh
be87a29a9d Respect typing-modules when evaluating no-return functions (#4001) 2023-04-17 20:25:44 +00:00
Micha Reiser
280dffb5e1 Add parser benchmark (#3990) 2023-04-17 16:43:59 +02:00
Charlie Marsh
336993ea06 Change Alpha trove classifier to Beta (#3995) 2023-04-17 13:55:49 +00:00
Tom Kuson
516cb10000 Add more documentation for flake8-type-checking (#3994) 2023-04-17 09:51:54 -04:00
Charlie Marsh
1cdd5e3424 Remove autofix behavior for uncapitalized-environment-variables (SIM112) (#3988) 2023-04-16 23:19:05 +00:00
Dhruv Manilawala
bd78c6ade2 Preserve type annotations when fixing E731 (#3983) 2023-04-16 23:15:38 +00:00
Dhruv Manilawala
5ce35faa86 Do not consider nested comment as part of code (#3984) 2023-04-16 19:11:01 -04:00
Justin Chu
484b572e6b Add ONNX Runtime to user list (#3982) 2023-04-16 18:21:46 -04:00
Charlie Marsh
81805a45f0 Add some additional users (#3975) 2023-04-14 12:41:22 -04:00
Charlie Marsh
c457752f36 Redirect PIE802 to C419 (#3971) 2023-04-13 22:12:32 -04:00
Charlie Marsh
289289bfd3 Implement unnecessary-literal-within-dict-call (C418) (#3969) 2023-04-14 01:39:35 +00:00
Charlie Marsh
09274307e8 Add multi-edit change to BREAKING_CHANGES.md (#3968) 2023-04-13 23:12:00 +00:00
Charlie Marsh
d8718dcf54 Remove extraneous debug and TODO (#3967) 2023-04-13 18:45:18 -04:00
Charlie Marsh
fb9eeba422 Move user-defined section validation into Settings (#3966) 2023-04-13 22:40:05 +00:00
Paul
2d2630ef07 Implement isort custom sections and ordering (#2419) (#3900) 2023-04-13 21:28:22 +00:00
Charlie Marsh
1f22e035e3 Add 'or if cond' to E712 message (#3962) 2023-04-13 19:02:23 +00:00
Rob Young
a6a7584d79 Implement flake8-bandit shell injection rules (#3924) 2023-04-13 14:45:27 -04:00
Charlie Marsh
ffac4f6ec3 Ignore assert errors (S101) in TYPE_CHECKING blocks (#3960) 2023-04-13 18:20:44 +00:00
Dhruv Manilawala
032a84b167 Check for parenthesis in implicit str concat in PT006 (#3955) 2023-04-13 17:56:18 +00:00
Charlie Marsh
3357aaef4b Add docs for assert rule (S101) (#3959) 2023-04-13 13:43:00 -04:00
Charlie Marsh
d9ed43d112 Clarify some isort differences in FAQ (#3954) 2023-04-13 04:05:28 +00:00
Charlie Marsh
e160a52bfd Raise percent-format upgrade rule (UP031) for hanging modulos (#3953) 2023-04-12 23:59:20 -04:00
Charlie Marsh
9067ae47d1 Allow typing_extensions.TypeVar assignments in .pyi files (#3951) 2023-04-12 17:30:15 -04:00
Charlie Marsh
71e807b3be Add Prefect to user list (#3949) 2023-04-12 12:09:36 -04:00
Charlie Marsh
1e2df07544 Use identifier range for pytest rules (#3948) 2023-04-12 15:28:25 +00:00
USER-5
860841468c [flake8-pyi] Implement duplicate types in unions (PYI016) (#3922) 2023-04-12 04:06:09 +00:00
Charlie Marsh
ed4ecc3255 Remove unused import (#3944) 2023-04-12 03:55:38 +00:00
Charlie Marsh
b999e4b1e2 Allow users to extend the set of included files via include (#3914) 2023-04-11 23:39:43 -04:00
Charlie Marsh
8ce227047d Tidy up some pygrep-hooks rules (#3942) 2023-04-12 03:35:15 +00:00
Daniel Stancl
523515f936 [flake8-import-conventions] Add a rule for BannedImportAlias (#3926) 2023-04-12 03:29:24 +00:00
Charlie Marsh
10da3bc8dd Support pyright: ignore comments (#3941) 2023-04-12 03:10:29 +00:00
Charlie Marsh
eb0dd74040 Avoid adding required imports to stub files (#3940) 2023-04-11 22:31:20 -04:00
Micha Reiser
61200d2171 lint snapshots: Use filename only to avoid platform specific separators (#3930) 2023-04-11 11:40:51 +02:00
Micha Reiser
e8aebee3f6 Pretty print Diagnostics in snapshot tests (#3906) 2023-04-11 09:03:00 +00:00
Micha Reiser
210083bdd8 Order Edits by Locations (#3905) 2023-04-11 08:56:41 +00:00
Micha Reiser
c33c9dc585 Introduce SourceFile to avoid cloning the message filename (#3904) 2023-04-11 08:28:55 +00:00
Micha Reiser
056c212975 Render code frame with context (#3901) 2023-04-11 10:22:11 +02:00
Micha Reiser
381203c084 Store source code on message (#3897) 2023-04-11 07:57:36 +00:00
Micha Reiser
76c47a9a43 Cheap cloneable LineIndex (#3896) 2023-04-11 07:33:40 +00:00
Micha Reiser
9209e57c5a Extract message emitters from Printer (#3895) 2023-04-11 07:24:25 +00:00
Leiser Fernández Gallo
333f1bd9ce Extend SIM105 to match also 'Ellipsis only' bodies in exception handlers (#3925) 2023-04-10 09:55:02 -04:00
Leiser Fernández Gallo
002caadf9e [flake8-simplify] Add autofix for contextlib.suppress (SIM105) (#3915) 2023-04-09 22:45:19 +00:00
Dhruv Manilawala
311ba29d0f Do not skip analysis if *args present for F523 (#3923) 2023-04-09 18:34:52 -04:00
Dhruv Manilawala
237a64d922 Check for arguments in inner/outer call for C414 (#3916) 2023-04-09 18:33:11 -04:00
Moritz Sauter
d4af2dd5cf [ruff] Add checks for mutable defaults in dataclasses (#3877) 2023-04-09 02:46:28 +00:00
Charlie Marsh
a36ce585ce Remove extract_path_names helper (#3920) 2023-04-08 11:14:42 -04:00
Charlie Marsh
29ec6df24f Avoid N802 violations for @override methods (#3912) 2023-04-08 03:11:50 +00:00
Evan Rittenhouse
8b17508ef1 Remove old documentation (#3911) 2023-04-07 22:51:19 -04:00
Evan Rittenhouse
abaf0a198d Ensure that tab characters aren't in multi-line strings before throwing a violation (#3837) 2023-04-06 22:25:40 -04:00
konstin
454c6d9c2f Extended ecosystem check with scraped data (#3858) 2023-04-06 22:39:48 +00:00
konstin
cae5503e34 [pylint] Fix unicode handling in PLE2515 (#3898) 2023-04-06 13:54:52 -04:00
Dhruv Manilawala
34e9786a41 Visit comprehension to detect group name usage/overrides (#3887) 2023-04-05 18:03:11 -04:00
Dhruv Manilawala
5467d45dfa Ignore PLW2901 when using typing cast (#3891) 2023-04-05 18:02:32 -04:00
Charlie Marsh
ac87137c1c Avoid printing docs on cargo dev generate-all (#3890) 2023-04-05 14:18:33 -04:00
Charlie Marsh
e0bccfd2d9 Allow legacy C and T selectors in JSON schema (#3889) 2023-04-05 17:58:36 +00:00
Tom Kuson
7b6e55a2e0 Add documentation for flake8-type-checking (#3886) 2023-04-05 17:30:25 +00:00
brucearctor
5c374b5793 Consistent Style/Levels in Usage (#3884) 2023-04-05 03:06:43 +00:00
Edgar R. M
ffdd0de522 Add Meltano to users (#3883) 2023-04-04 23:05:53 -04:00
Charlie Marsh
5370968839 Add some additional users and alphabetize (#3882) 2023-04-05 02:40:02 +00:00
Charlie Marsh
255b094b33 Bump version to 0.0.261 (#3881) 2023-04-04 22:31:01 -04:00
Dhruv Manilawala
b6155232ac Consider logger candidate from logging module only (#3878) 2023-04-04 19:52:57 +00:00
kyoto7250
390d7dcf39 Supports more cases in SIM112 (#3876) 2023-04-04 15:49:24 -04:00
Charlie Marsh
251340a246 Add LangChain and LlamaIndex (#3879) 2023-04-04 19:36:31 +00:00
Charlie Marsh
d919adc13c Introduce a ruff_python_semantic crate (#3865) 2023-04-04 16:50:47 +00:00
kyoto7250
46bcb1f725 [flake8-simplify] Implement dict-get-with-none-default (SIM910) (#3874) 2023-04-04 03:52:10 +00:00
Dhruv Manilawala
2b21effa77 fixup! Support mutually exclusive branches for B031 (#3844) (#3875) 2023-04-03 23:34:11 -04:00
Chris Chan
10504eb9ed Generate ImportMap from module path to imported dependencies (#3243) 2023-04-04 03:31:37 +00:00
Dhruv Manilawala
76e111c874 Support mutually exclusive branches for B031 (#3844) 2023-04-04 02:33:17 +00:00
brucearctor
e006b922a6 Add documentation for ruff-action (GitHub Action!) (#3857) 2023-04-03 23:47:26 +00:00
Charlie Marsh
60f6a8571a Allow starred arguments in B030 (#3871) 2023-04-03 23:20:34 +00:00
Charlie Marsh
f4173b2a93 Move shadow tracking into Scope directly (#3854) 2023-04-03 15:33:44 -04:00
Charlie Marsh
449e08ed08 Rename autofix::helpers to autofix::actions (#3866) 2023-04-03 13:34:49 -04:00
Charlie Marsh
5625410936 Remove uses_magic_variable_access dependence on Checker (#3864) 2023-04-03 12:22:06 -04:00
Charlie Marsh
3744e9ab3f Remove contains_effect's dependency on Context (#3855) 2023-04-03 12:08:13 -04:00
Nicolas Vuillamy
b52cb93e58 Add thank you in README.md + usage in MegaLinter (#3848) 2023-04-03 15:45:25 +00:00
Nazia Povey
849091d846 When checking module visibility, don't check entire ancestry (#3835) 2023-04-03 11:38:41 -04:00
Ran Benita
d2f2544f6e flake8-pyi: fix PYI015 false positive on assignment of TypeVar & friends (#3861) 2023-04-03 11:28:46 -04:00
Charlie Marsh
25771cd4b9 Use references for Export binding type (#3853) 2023-04-03 15:26:42 +00:00
Charlie Marsh
924bebbb4a Change "indexes" to "indices" in various contexts (#3856) 2023-04-02 23:08:03 +00:00
Charlie Marsh
08e5b3fa61 Make collect_call_path return an Option (#3849) 2023-04-01 22:29:32 -04:00
1200 changed files with 65137 additions and 68817 deletions

1
.gitignore vendored
View File

@@ -3,6 +3,7 @@
crates/ruff/resources/test/cpython
mkdocs.yml
.overrides
github_search.jsonl
###
# Rust.gitignore

View File

@@ -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))

View File

@@ -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,

152
Cargo.lock generated
View File

@@ -153,15 +153,6 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "block-buffer"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e"
dependencies = [
"generic-array",
]
[[package]]
name = "bstr"
version = "0.2.17"
@@ -446,15 +437,6 @@ version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
[[package]]
name = "cpufeatures"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320"
dependencies = [
"libc",
]
[[package]]
name = "crc32fast"
version = "1.3.2"
@@ -549,16 +531,6 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "ctor"
version = "0.1.26"
@@ -625,16 +597,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
[[package]]
name = "digest"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "dirs"
version = "4.0.0"
@@ -774,7 +736,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.260"
version = "0.0.262"
dependencies = [
"anyhow",
"clap 4.1.8",
@@ -825,16 +787,6 @@ dependencies = [
"libc",
]
[[package]]
name = "generic-array"
version = "0.14.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.2.8"
@@ -1018,9 +970,6 @@ dependencies = [
"console",
"lazy_static",
"linked-hash-map",
"pest",
"pest_derive",
"serde",
"similar",
"yaml-rust",
]
@@ -1560,50 +1509,6 @@ version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
[[package]]
name = "pest"
version = "2.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "028accff104c4e513bad663bbcd2ad7cfd5304144404c31ed0a77ac103d00660"
dependencies = [
"thiserror",
"ucd-trie",
]
[[package]]
name = "pest_derive"
version = "2.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ac3922aac69a40733080f53c1ce7f91dcf57e1a5f6c52f421fadec7fbdc4b69"
dependencies = [
"pest",
"pest_generator",
]
[[package]]
name = "pest_generator"
version = "2.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d06646e185566b5961b4058dd107e0a7f56e77c3f484549fb119867773c0f202"
dependencies = [
"pest",
"pest_meta",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "pest_meta"
version = "2.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6f60b2ba541577e2a0c307c8f39d1439108120eb7903adeb6497fa880c59616"
dependencies = [
"once_cell",
"pest",
"sha2",
]
[[package]]
name = "petgraph"
version = "0.6.3"
@@ -1977,8 +1882,9 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.260"
version = "0.0.262"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
"bitflags",
"chrono",
@@ -2004,14 +1910,17 @@ dependencies = [
"pathdiff",
"pep440_rs",
"pretty_assertions",
"quick-junit",
"regex",
"result-like",
"ruff_cache",
"ruff_diagnostics",
"ruff_macros",
"ruff_python_ast",
"ruff_python_semantic",
"ruff_python_stdlib",
"ruff_rustpython",
"ruff_text_size",
"rustc-hash",
"rustpython-common",
"rustpython-parser",
@@ -2020,6 +1929,7 @@ dependencies = [
"serde",
"serde_json",
"shellexpand",
"similar",
"smallvec",
"strum",
"strum_macros",
@@ -2039,6 +1949,8 @@ dependencies = [
"mimalloc",
"once_cell",
"ruff",
"ruff_python_ast",
"rustpython-parser",
"serde",
"serde_json",
"tikv-jemallocator",
@@ -2059,7 +1971,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.0.260"
version = "0.0.262"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -2081,12 +1993,12 @@ dependencies = [
"mimalloc",
"notify",
"path-absolutize",
"quick-junit",
"rayon",
"regex",
"ruff",
"ruff_cache",
"ruff_diagnostics",
"ruff_python_ast",
"ruff_python_stdlib",
"rustc-hash",
"serde",
@@ -2169,16 +2081,16 @@ dependencies = [
"is-macro",
"itertools",
"log",
"nohash-hasher",
"num-bigint",
"num-traits",
"once_cell",
"regex",
"ruff_python_stdlib",
"ruff_rustpython",
"ruff_text_size",
"rustc-hash",
"rustpython-common",
"rustpython-parser",
"serde",
"smallvec",
]
@@ -2194,7 +2106,6 @@ dependencies = [
"once_cell",
"ruff_formatter",
"ruff_python_ast",
"ruff_python_stdlib",
"ruff_rustpython",
"ruff_testing_macros",
"ruff_text_size",
@@ -2205,6 +2116,20 @@ dependencies = [
"test-case",
]
[[package]]
name = "ruff_python_semantic"
version = "0.0.0"
dependencies = [
"bitflags",
"is-macro",
"nohash-hasher",
"ruff_python_ast",
"ruff_python_stdlib",
"rustc-hash",
"rustpython-parser",
"smallvec",
]
[[package]]
name = "ruff_python_stdlib"
version = "0.0.0"
@@ -2529,17 +2454,6 @@ dependencies = [
"serde",
]
[[package]]
name = "sha2"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "shellexpand"
version = "3.0.0"
@@ -2922,18 +2836,6 @@ version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
[[package]]
name = "typenum"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
[[package]]
name = "ucd-trie"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81"
[[package]]
name = "unic-char-property"
version = "0.9.0"

View File

@@ -24,6 +24,7 @@ is-macro = { version = "0.2.2" }
itertools = { version = "0.10.5" }
libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "80e4c1399f95e5beb532fdd1e209ad2dbb470438" }
log = { version = "0.4.17" }
nohash-hasher = { version = "0.2.0" }
once_cell = { version = "1.17.1" }
path-absolutize = { version = "3.0.14" }
proc-macro2 = { version = "1.0.51" }
@@ -40,6 +41,7 @@ serde = { version = "1.0.152", features = ["derive"] }
serde_json = { version = "1.0.93", features = ["preserve_order"] }
shellexpand = { version = "3.0.0" }
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" }

117
README.md
View File

@@ -47,10 +47,10 @@ 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.
@@ -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.260'
rev: 'v0.0.262'
hooks:
- id: ruff
```
@@ -145,6 +145,20 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com) hook:
Ruff can also be used as a [VS Code extension](https://github.com/charliermarsh/ruff-vscode) or
alongside any other editor through the [Ruff LSP](https://github.com/charliermarsh/ruff-lsp).
Ruff can also be used as a [GitHub Action](https://github.com/features/actions) via
[`ruff-action`](https://github.com/chartboost/ruff-action):
```yaml
name: Ruff
on: [ push, pull_request ]
jobs:
ruff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: chartboost/ruff-action@v1
```
### Configuration
Ruff can be configured through a `pyproject.toml`, `ruff.toml`, or `.ruff.toml` file (see:
@@ -318,52 +332,67 @@ 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)
- [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)
- [Polars](https://github.com/pola-rs/polars)
- [PostHog](https://github.com/PostHog/posthog)
- 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)
- [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)
## License

View File

@@ -5,3 +5,4 @@ extend-exclude = ["snapshots", "black"]
trivias = "trivias"
hel = "hel"
whos = "whos"
spawnve = "spawnve"

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.0.260"
version = "0.0.262"
authors.workspace = true
edition.workspace = true
rust-version.workspace = true
@@ -18,9 +18,12 @@ ruff_cache = { path = "../ruff_cache" }
ruff_diagnostics = { path = "../ruff_diagnostics", features = ["serde"] }
ruff_macros = { path = "../ruff_macros" }
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 }
@@ -37,7 +40,7 @@ itertools = { workspace = true }
libcst = { workspace = true }
log = { workspace = true }
natord = { version = "1.0.9" }
nohash-hasher = { version = "0.2.0" }
nohash-hasher = { workspace = true }
num-bigint = { version = "0.4.3" }
num-traits = { version = "0.2.15" }
once_cell = { workspace = true }
@@ -47,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 }
@@ -56,8 +60,9 @@ schemars = { workspace = true }
semver = { version = "1.0.16" }
serde = { workspace = true }
serde_json = { workspace = true }
similar = { workspace = true, features = ["inline"] }
shellexpand = { workspace = true }
smallvec = { version = "1.10.0" }
smallvec = { workspace = true }
strum = { workspace = true }
strum_macros = { workspace = true }
textwrap = { workspace = true }
@@ -67,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 = []

View File

@@ -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

View File

@@ -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

View 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)

View 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")

View File

@@ -0,0 +1,5 @@
def foo(shell):
pass
foo(shell=True)

View 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, ""])

View 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")

View 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")

View File

@@ -28,6 +28,11 @@ except (ValueError, *(RuntimeError, (KeyError, TypeError))): # error
pass
try:
pass
except (*a, *(RuntimeError, (KeyError, TypeError))): # error
pass
try:
pass
except (ValueError, *(RuntimeError, TypeError)): # ok
@@ -38,10 +43,36 @@ try:
except (ValueError, *[RuntimeError, *(TypeError,)]): # ok
pass
try:
pass
except (*a, *b): # ok
pass
try:
pass
except (*a, *(RuntimeError, TypeError)): # ok
pass
try:
pass
except (*a, *(b, c)): # ok
pass
try:
pass
except (*a, *(*b, *c)): # ok
pass
def what_to_catch():
return ...
try:
pass
except what_to_catch(): # ok
pass
pass

View File

@@ -78,6 +78,71 @@ 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)
else:
collect_shop_items(shopper, section_items)
collect_shop_items(shopper, section_items) # B031
for _section, section_items in itertools.groupby(items, key=lambda p: p[1]):
# Mutually exclusive branches shouldn't trigger the warning
if _section == "greens":
collect_shop_items(shopper, section_items)
if _section == "greens":
collect_shop_items(shopper, section_items) # B031
elif _section == "frozen items":
collect_shop_items(shopper, section_items) # B031
else:
collect_shop_items(shopper, section_items) # B031
collect_shop_items(shopper, section_items) # B031
elif _section == "frozen items":
# Mix `match` and `if` statements
match shopper:
case "Jane":
collect_shop_items(shopper, section_items)
if _section == "fourth":
collect_shop_items(shopper, section_items) # B031
case _:
collect_shop_items(shopper, section_items)
else:
collect_shop_items(shopper, section_items)
# Now, it should detect
collect_shop_items(shopper, section_items) # B031
for _section, section_items in itertools.groupby(items, key=lambda p: p[1]):
# Mutually exclusive branches shouldn't trigger the warning
match _section:
case "greens":
collect_shop_items(shopper, section_items)
match shopper:
case "Jane":
collect_shop_items(shopper, section_items) # B031
case _:
collect_shop_items(shopper, section_items) # B031
case "frozen items":
collect_shop_items(shopper, section_items)
collect_shop_items(shopper, section_items) # B031
case _:
collect_shop_items(shopper, section_items)
# Now, it should detect
collect_shop_items(shopper, section_items) # B031
for group in groupby(items, key=lambda p: p[1]):
# This is bad, but not detected currently
collect_shop_items("Jane", group[1])

View File

@@ -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"\

View 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)

View File

@@ -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()])

View 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

View File

@@ -1,3 +1,5 @@
import logging
from distutils import log
logging.warn("Hello World!")
log.warn("Hello world!") # This shouldn't be considered as a logger candidate

View File

@@ -11,3 +11,7 @@ _T = TypeVar("_T") # OK
_TTuple = TypeVarTuple("_TTuple") # OK
_P = ParamSpec("_P") # OK
def f():
T = TypeVar("T") # OK

View File

@@ -11,3 +11,6 @@ _T = TypeVar("_T") # OK
_TTuple = TypeVarTuple("_TTuple") # OK
_P = ParamSpec("_P") # OK
def f():
T = TypeVar("T") # OK

View File

@@ -46,3 +46,41 @@ 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"]

View File

@@ -1,6 +1,6 @@
import builtins
import typing
from typing import TypeAlias, Final
from typing import TypeAlias, Final, NewType, TypeVar, TypeVarTuple, ParamSpec
# We shouldn't emit Y015 for simple default values
field1: int
@@ -26,6 +26,10 @@ field9 = None # Y026 Use typing_extensions.TypeAlias for type aliases, e.g. "fi
Field95: TypeAlias = None
Field96: TypeAlias = int | None
Field97: TypeAlias = None | typing.SupportsInt | builtins.str | float | bool
Field98 = NewType('MyInt', int)
Field99 = TypeVar('Field99')
Field100 = TypeVarTuple('Field100')
Field101 = ParamSpec('Field101')
field19 = [1, 2, 3] # Y052 Need type annotation for "field19"
field191: list[int] = [1, 2, 3]
field20 = (1, 2, 3) # Y052 Need type annotation for "field20"
@@ -49,3 +53,41 @@ 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"]

View 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]

View 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]

View File

@@ -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):
...

View File

@@ -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

View File

@@ -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:
...

View File

@@ -9,6 +9,17 @@ os.environ.get('foo', 'bar')
os.getenv('foo')
env = os.environ.get('foo')
env = os.environ['foo']
if env := os.environ.get('foo'):
pass
if env := os.environ['foo']:
pass
# Good
os.environ['FOO']
@@ -17,3 +28,13 @@ os.environ.get('FOO')
os.environ.get('FOO', 'bar')
os.getenv('FOO')
env = os.getenv('FOO')
if env := os.getenv('FOO'):
pass
env = os.environ['FOO']
if env := os.environ['FOO']:
pass

View File

@@ -0,0 +1,27 @@
# SIM910
{}.get(key, None)
# SIM910
{}.get("key", None)
# OK
{}.get(key)
# OK
{}.get("key")
# OK
{}.get(key, False)
# OK
{}.get("key", False)
# SIM910
if a := {}.get(key, None):
pass
# SIM910
a = {}.get(key, None)
# SIM910
({}).get(key, None)

View File

@@ -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

View File

@@ -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()

View File

@@ -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`.

View File

@@ -0,0 +1,3 @@
"""Hello, world!"""
x = 1

View 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

View File

@@ -39,3 +39,11 @@ class Test(unittest.TestCase):
def testTest(self):
assert True
from typing import override
@override
def BAD_FUNC():
pass

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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

View File

@@ -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,)
)

View File

@@ -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

View File

@@ -0,0 +1,21 @@
from dataclasses import dataclass, field
KNOWINGLY_MUTABLE_DEFAULT = []
@dataclass()
class A:
mutable_default: list[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)
@dataclass
class B:
mutable_default: list[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)

View File

@@ -0,0 +1,28 @@
from dataclasses import dataclass
from typing import NamedTuple
def default_function() -> list[int]:
return []
class ImmutableType(NamedTuple):
something: int = 8
@dataclass()
class A:
hidden_mutable_default: 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

View File

@@ -1,3 +1,4 @@
//! Interface for generating autofix edits from higher-level actions (e.g., "remove an argument").
use anyhow::{bail, Result};
use itertools::Itertools;
use libcst_native::{
@@ -7,12 +8,12 @@ use rustpython_parser::ast::{ExcepthandlerKind, Expr, Keyword, Location, Stmt, S
use rustpython_parser::{lexer, Mode, Tok};
use ruff_diagnostics::Edit;
use ruff_python_ast::context::Context;
use ruff_python_ast::helpers;
use ruff_python_ast::helpers::to_absolute;
use ruff_python_ast::imports::{AnyImport, Import};
use ruff_python_ast::newlines::NewlineWithTrailingNewline;
use ruff_python_ast::source_code::{Indexer, Locator, Stylist};
use ruff_python_semantic::context::Context;
use crate::cst::helpers::compose_module_path;
use crate::cst::matchers::match_module;
@@ -102,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(';') {
@@ -125,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.
@@ -157,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()
}
@@ -360,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;
@@ -531,7 +532,7 @@ mod tests {
use ruff_python_ast::source_code::Locator;
use crate::autofix::helpers::{next_stmt_break, trailing_semicolon};
use crate::autofix::actions::{next_stmt_break, trailing_semicolon};
#[test]
fn find_semicolon() -> Result<()> {

View File

@@ -11,14 +11,19 @@ use ruff_python_ast::types::Range;
use crate::linter::FixTable;
use crate::registry::{AsRule, Rule};
pub mod helpers;
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!(

View File

@@ -1,9 +1,9 @@
use ruff_python_ast::scope::ScopeStack;
use ruff_python_semantic::scope::ScopeStack;
use rustpython_parser::ast::{Expr, Stmt};
use ruff_python_ast::types::Range;
use ruff_python_ast::types::RefEquality;
use ruff_python_ast::visibility::{Visibility, VisibleScope};
use ruff_python_semantic::analyze::visibility::{Visibility, VisibleScope};
use crate::checkers::ast::AnnotationContext;
use crate::docstrings::definition::Definition;

View File

@@ -14,22 +14,22 @@ use rustpython_parser::ast::{
use ruff_diagnostics::Diagnostic;
use ruff_python_ast::all::{extract_all_names, AllNamesFlags};
use ruff_python_ast::binding::{
use ruff_python_ast::helpers::{extract_handled_exceptions, to_module_path};
use ruff_python_ast::source_code::{Indexer, Locator, Stylist};
use ruff_python_ast::types::{Node, Range, RefEquality};
use ruff_python_ast::typing::parse_type_annotation;
use ruff_python_ast::visitor::{walk_excepthandler, walk_pattern, Visitor};
use ruff_python_ast::{branch_detection, cast, helpers, str, visitor};
use ruff_python_semantic::analyze;
use ruff_python_semantic::analyze::typing::{Callable, SubscriptKind};
use ruff_python_semantic::binding::{
Binding, BindingId, BindingKind, Exceptions, ExecutionContext, Export, FromImportation,
Importation, StarImportation, SubmoduleImportation,
};
use ruff_python_ast::context::Context;
use ruff_python_ast::helpers::{extract_handled_exceptions, to_module_path};
use ruff_python_ast::scope::{
use ruff_python_semantic::context::Context;
use ruff_python_semantic::scope::{
ClassDef, FunctionDef, Lambda, Scope, ScopeId, ScopeKind, ScopeStack,
};
use ruff_python_ast::source_code::{Indexer, Locator, Stylist};
use ruff_python_ast::types::{Node, Range, RefEquality};
use ruff_python_ast::typing::{
match_annotated_subscript, parse_type_annotation, Callable, SubscriptKind,
};
use ruff_python_ast::visitor::{walk_excepthandler, walk_pattern, Visitor};
use ruff_python_ast::{branch_detection, cast, helpers, str, typing, visibility, visitor};
use ruff_python_stdlib::builtins::{BUILTINS, MAGIC_GLOBALS};
use ruff_python_stdlib::path::is_python_stub_file;
@@ -344,6 +344,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 +359,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);
@@ -595,14 +598,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 +814,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 +965,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
@@ -1056,6 +1068,21 @@ where
}
}
if self.settings.rules.enabled(Rule::BannedImportAlias) {
if let Some(asname) = &alias.node.asname {
if let Some(diagnostic) =
flake8_import_conventions::rules::check_banned_import(
stmt,
&alias.node.name,
asname,
&self.settings.flake8_import_conventions.banned_aliases,
)
{
self.diagnostics.push(diagnostic);
}
}
}
if self
.settings
.rules
@@ -1123,26 +1150,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 +1306,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,
)
{
@@ -1318,6 +1343,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::check_banned_import(
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
@@ -1547,16 +1592,18 @@ 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);
@@ -1570,7 +1617,6 @@ where
msg.as_deref(),
);
}
if self.settings.rules.enabled(Rule::AssertOnStringLiteral) {
pylint::rules::assert_on_string_literal(self, test);
}
@@ -1729,7 +1775,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 +1791,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 +1831,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 +1856,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 +1880,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,
);
}
}
}
}
@@ -1847,13 +1906,6 @@ where
if self.settings.rules.enabled(Rule::UselessExpression) {
flake8_bugbear::rules::useless_expression(self, value);
}
if self
.settings
.rules
.enabled(Rule::UncapitalizedEnvironmentVariables)
{
flake8_simplify::rules::use_capital_environment_variables(self, value);
}
if self.settings.rules.enabled(Rule::AsyncioDanglingTask) {
if let Some(diagnostic) = ruff::rules::asyncio_dangling_task(value, |expr| {
self.ctx.resolve_call_path(expr)
@@ -2210,6 +2262,14 @@ where
]) {
flake8_2020::rules::subscript(self, value, slice);
}
if self
.settings
.rules
.enabled(Rule::UncapitalizedEnvironmentVariables)
{
flake8_simplify::rules::use_capital_environment_variables(self, expr);
}
}
ExprKind::Tuple { elts, ctx } | ExprKind::List { elts, ctx } => {
if matches!(ctx, ExprContext::Store) {
@@ -2248,7 +2308,7 @@ where
|| (self.settings.target_version >= PythonVersion::Py37
&& self.ctx.annotations_future_enabled
&& self.ctx.in_annotation))
&& typing::is_pep585_builtin(expr, &self.ctx)
&& analyze::typing::is_pep585_builtin(expr, &self.ctx)
{
pyupgrade::rules::use_pep585_annotation(self, expr);
}
@@ -2291,7 +2351,7 @@ where
|| (self.settings.target_version >= PythonVersion::Py37
&& self.ctx.annotations_future_enabled
&& self.ctx.in_annotation))
&& typing::is_pep585_builtin(expr, &self.ctx)
&& analyze::typing::is_pep585_builtin(expr, &self.ctx)
{
pyupgrade::rules::use_pep585_annotation(self, expr);
}
@@ -2527,13 +2587,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) {
@@ -2593,6 +2646,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) {
@@ -2664,7 +2727,7 @@ where
.enabled(Rule::UnnecessaryLiteralWithinTupleCall)
{
flake8_comprehensions::rules::unnecessary_literal_within_tuple_call(
self, expr, func, args,
self, expr, func, args, keywords,
);
}
if self
@@ -2673,7 +2736,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) {
@@ -2715,6 +2787,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
@@ -2910,6 +2991,14 @@ where
}
// flake8-simplify
if self
.settings
.rules
.enabled(Rule::UncapitalizedEnvironmentVariables)
{
flake8_simplify::rules::use_capital_environment_variables(self, expr);
}
if self
.settings
.rules
@@ -2918,6 +3007,10 @@ where
flake8_simplify::rules::open_file_with_context_handler(self, func);
}
if self.settings.rules.enabled(Rule::DictGetWithNoneDefault) {
flake8_simplify::rules::dict_get_with_none_default(self, expr);
}
// flake8-use-pathlib
if self.settings.rules.any_enabled(&[
Rule::OsPathAbspath,
@@ -3165,7 +3258,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);
@@ -3198,6 +3291,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);
@@ -3628,7 +3742,7 @@ where
self.ctx.in_subscript = true;
visitor::walk_expr(self, expr);
} else {
match match_annotated_subscript(
match analyze::typing::match_annotated_subscript(
value,
&self.ctx,
self.settings.typing_modules.iter().map(String::as_str),
@@ -4047,7 +4161,7 @@ impl<'a> Checker<'a> {
&& binding.redefines(existing)
&& (!self.settings.dummy_variable_rgx.is_match(name) || existing_is_import)
&& !(existing.kind.is_function_definition()
&& visibility::is_overload(
&& analyze::visibility::is_overload(
&self.ctx,
cast::decorator_list(existing.source.as_ref().unwrap()),
))
@@ -4081,7 +4195,7 @@ impl<'a> Checker<'a> {
}
} else if existing_is_import && binding.redefines(existing) {
self.ctx
.redefinitions
.shadowed_bindings
.entry(existing_binding_index)
.or_insert_with(Vec::new)
.push(binding_id);
@@ -4123,13 +4237,7 @@ impl<'a> Checker<'a> {
// in scope.
let scope = self.ctx.scope_mut();
if !(binding.kind.is_annotation() && scope.defines(name)) {
if let Some(rebound_index) = scope.add(name, binding_id) {
scope
.rebounds
.entry(name)
.or_insert_with(Vec::new)
.push(rebound_index);
}
scope.add(name, binding_id);
}
self.ctx.bindings.push(binding);
@@ -4380,7 +4488,6 @@ impl<'a> Checker<'a> {
return;
}
// TODO(charlie): Include comprehensions here.
if matches!(
parent.node,
StmtKind::For { .. } | StmtKind::AsyncFor { .. }
@@ -4452,7 +4559,23 @@ impl<'a> Checker<'a> {
}
_ => false,
} {
let (all_names, all_names_flags) = extract_all_names(&self.ctx, parent, current);
let (all_names, all_names_flags) = {
let (mut names, flags) =
extract_all_names(parent, |name| self.ctx.is_builtin(name));
// Grab the existing bound __all__ values.
if let StmtKind::AugAssign { .. } = &parent.node {
if let Some(index) = current.get("__all__") {
if let BindingKind::Export(Export { names: existing }) =
&self.ctx.bindings[*index].kind
{
names.extend_from_slice(existing);
}
}
}
(names, flags)
};
if self.settings.rules.enabled(Rule::InvalidAllFormat) {
if matches!(all_names_flags, AllNamesFlags::INVALID_FORMAT) {
@@ -4749,7 +4872,7 @@ impl<'a> Checker<'a> {
// Mark anything referenced in `__all__` as used.
let all_bindings: Option<(Vec<BindingId>, Range)> = {
let global_scope = self.ctx.global_scope();
let all_names: Option<(&Vec<String>, Range)> = global_scope
let all_names: Option<(&Vec<&str>, Range)> = global_scope
.get("__all__")
.map(|index| &self.ctx.bindings[*index])
.and_then(|binding| match &binding.kind {
@@ -4761,7 +4884,7 @@ impl<'a> Checker<'a> {
(
names
.iter()
.filter_map(|name| global_scope.get(name.as_str()).copied())
.filter_map(|name| global_scope.get(name).copied())
.collect(),
range,
)
@@ -4779,15 +4902,13 @@ impl<'a> Checker<'a> {
}
// Extract `__all__` names from the global scope.
let all_names: Option<(Vec<&str>, Range)> = self
let all_names: Option<(&[&str], Range)> = self
.ctx
.global_scope()
.get("__all__")
.map(|index| &self.ctx.bindings[*index])
.and_then(|binding| match &binding.kind {
BindingKind::Export(Export { names }) => {
Some((names.iter().map(String::as_str).collect(), binding.range))
}
BindingKind::Export(Export { names }) => Some((names.as_slice(), binding.range)),
_ => None,
});
@@ -4847,7 +4968,7 @@ impl<'a> Checker<'a> {
.dedup()
.collect();
if !sources.is_empty() {
for &name in names {
for &name in names.iter() {
if !scope.defines(name) {
diagnostics.push(Diagnostic::new(
pyflakes::rules::UndefinedLocalWithImportStarUsage {
@@ -4905,7 +5026,7 @@ impl<'a> Checker<'a> {
continue;
}
if let Some(indices) = self.ctx.redefinitions.get(index) {
if let Some(indices) = self.ctx.shadowed_bindings.get(index) {
for index in indices {
let rebound = &self.ctx.bindings[*index];
let mut diagnostic = Diagnostic::new(
@@ -5052,7 +5173,7 @@ impl<'a> Checker<'a> {
let fix = if !in_init && !in_except_handler && self.patch(Rule::UnusedImport) {
let deleted: Vec<&Stmt> = self.deletions.iter().map(Into::into).collect();
match autofix::helpers::remove_unused_imports(
match autofix::actions::remove_unused_imports(
unused_imports.iter().map(|(full_name, _)| *full_name),
child,
parent,
@@ -5062,7 +5183,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)

View File

@@ -1,12 +1,15 @@
//! Lint rules based on import analysis.
use std::borrow::Cow;
use std::path::Path;
use rustpython_parser::ast::Suite;
use rustpython_parser::ast::{StmtKind, Suite};
use ruff_diagnostics::Diagnostic;
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;
@@ -14,6 +17,66 @@ use crate::rules::isort;
use crate::rules::isort::track::{Block, ImportTracker};
use crate::settings::{flags, Settings};
fn extract_import_map(path: &Path, package: Option<&Path>, blocks: &[&Block]) -> Option<ImportMap> {
let Some(package) = package else {
return None;
};
let Some(module_path) = to_module_path(package, path) else {
return None;
};
let num_imports = blocks.iter().map(|block| block.imports.len()).sum();
let mut module_imports = Vec::with_capacity(num_imports);
for stmt in blocks.iter().flat_map(|block| &block.imports) {
match &stmt.node {
StmtKind::Import { names } => {
module_imports.extend(names.iter().map(|name| {
ModuleImport::new(
name.node.name.clone(),
stmt.location,
stmt.end_location.unwrap(),
)
}));
}
StmtKind::ImportFrom {
module,
names,
level,
} => {
let level = level.unwrap_or(0);
let module = if let Some(module) = module {
if level == 0 {
Cow::Borrowed(module)
} else {
if module_path.len() <= level {
continue;
}
let prefix = module_path[..module_path.len() - level].join(".");
Cow::Owned(format!("{prefix}.{module}"))
}
} else {
if module_path.len() <= level {
continue;
}
Cow::Owned(module_path[..module_path.len() - level].join("."))
};
module_imports.extend(names.iter().map(|name| {
ModuleImport::new(
format!("{}.{}", module, name.node.name),
name.location,
name.end_location.unwrap(),
)
}));
}
_ => panic!("Expected StmtKind::Import | StmtKind::ImportFrom"),
}
}
let mut import_map = ImportMap::default();
import_map.insert(module_path.join("."), module_imports);
Some(import_map)
}
#[allow(clippy::too_many_arguments)]
pub fn check_imports(
python_ast: &Suite,
@@ -25,10 +88,12 @@ pub fn check_imports(
autofix: flags::Autofix,
path: &Path,
package: Option<&Path>,
) -> Vec<Diagnostic> {
) -> (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
};
@@ -49,8 +114,12 @@ 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,
));
}
diagnostics
// Extract import map.
let imports = extract_import_map(path, package, &blocks);
(diagnostics, imports)
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -263,6 +263,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,
@@ -356,6 +358,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {
(Flake8Simplify, "223") => Rule::ExprAndFalse,
(Flake8Simplify, "300") => Rule::YodaConditions,
(Flake8Simplify, "401") => Rule::IfElseBlockInsteadOfDictGet,
(Flake8Simplify, "910") => Rule::DictGetWithNoneDefault,
// pyupgrade
(Pyupgrade, "001") => Rule::UselessMetaclassType,
@@ -506,6 +509,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,
@@ -524,6 +533,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {
// flake8-import-conventions
(Flake8ImportConventions, "001") => Rule::UnconventionalImportAlias,
(Flake8ImportConventions, "002") => Rule::BannedImportAlias,
// flake8-datetimez
(Flake8Datetimez, "001") => Rule::CallDatetimeWithoutTzinfo,
@@ -572,6 +582,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,
@@ -607,7 +618,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,
@@ -699,6 +709,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

View File

@@ -1,7 +1,8 @@
use ruff_python_ast::visibility::{
use rustpython_parser::ast::{Expr, Stmt};
use ruff_python_semantic::analyze::visibility::{
class_visibility, function_visibility, method_visibility, Modifier, Visibility, VisibleScope,
};
use rustpython_parser::ast::{Expr, Stmt};
#[derive(Debug, Clone)]
pub enum DefinitionKind<'a> {

View File

@@ -2,7 +2,7 @@
use rustpython_parser::ast::{Constant, Expr, ExprKind, Stmt, StmtKind};
use ruff_python_ast::visibility::{Modifier, VisibleScope};
use ruff_python_semantic::analyze::visibility;
use crate::docstrings::definition::{Definition, DefinitionKind, Documentable};
@@ -28,7 +28,7 @@ pub fn docstring_from(suite: &[Stmt]) -> Option<&Expr> {
/// Extract a `Definition` from the AST node defined by a `Stmt`.
pub fn extract<'a>(
scope: VisibleScope,
scope: visibility::VisibleScope,
stmt: &'a Stmt,
body: &'a [Stmt],
kind: Documentable,
@@ -36,22 +36,22 @@ pub fn extract<'a>(
let expr = docstring_from(body);
match kind {
Documentable::Function => match scope {
VisibleScope {
modifier: Modifier::Module,
visibility::VisibleScope {
modifier: visibility::Modifier::Module,
..
} => Definition {
kind: DefinitionKind::Function(stmt),
docstring: expr,
},
VisibleScope {
modifier: Modifier::Class,
visibility::VisibleScope {
modifier: visibility::Modifier::Class,
..
} => Definition {
kind: DefinitionKind::Method(stmt),
docstring: expr,
},
VisibleScope {
modifier: Modifier::Function,
visibility::VisibleScope {
modifier: visibility::Modifier::Function,
..
} => Definition {
kind: DefinitionKind::NestedFunction(stmt),
@@ -59,22 +59,22 @@ pub fn extract<'a>(
},
},
Documentable::Class => match scope {
VisibleScope {
modifier: Modifier::Module,
visibility::VisibleScope {
modifier: visibility::Modifier::Module,
..
} => Definition {
kind: DefinitionKind::Class(stmt),
docstring: expr,
},
VisibleScope {
modifier: Modifier::Class,
visibility::VisibleScope {
modifier: visibility::Modifier::Class,
..
} => Definition {
kind: DefinitionKind::NestedClass(stmt),
docstring: expr,
},
VisibleScope {
modifier: Modifier::Function,
visibility::VisibleScope {
modifier: visibility::Modifier::Function,
..
} => Definition {
kind: DefinitionKind::NestedClass(stmt),

View File

@@ -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,

View File

@@ -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);

View File

@@ -1,4 +1,5 @@
use std::borrow::Cow;
use std::ops::Deref;
use std::path::Path;
use anyhow::{anyhow, Result};
@@ -9,7 +10,8 @@ use rustpython_parser::lexer::LexResult;
use rustpython_parser::ParseError;
use ruff_diagnostics::Diagnostic;
use ruff_python_ast::source_code::{Indexer, Locator, Stylist};
use ruff_python_ast::imports::ImportMap;
use ruff_python_ast::source_code::{Indexer, Locator, SourceFileBuilder, Stylist};
use ruff_python_stdlib::path::is_python_stub_file;
use crate::autofix::fix_file;
@@ -21,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;
@@ -51,6 +53,15 @@ impl<T> LinterResult<T> {
pub type FixTable = FxHashMap<Rule, usize>;
pub struct FixerResult<'a> {
/// The result returned by the linter, after applying any fixes.
pub result: LinterResult<(Vec<Message>, Option<ImportMap>)>,
/// The resulting source code, after applying any fixes.
pub transformed: Cow<'a, str>,
/// The number of fixes applied for each [`Rule`].
pub fixed: FixTable,
}
/// Generate `Diagnostic`s from the source code contents at the
/// given `Path`.
#[allow(clippy::too_many_arguments)]
@@ -66,9 +77,10 @@ pub fn check_path(
settings: &Settings,
noqa: flags::Noqa,
autofix: flags::Autofix,
) -> LinterResult<Vec<Diagnostic>> {
) -> LinterResult<(Vec<Diagnostic>, Option<ImportMap>)> {
// Aggregate all diagnostics.
let mut diagnostics = vec![];
let mut imports = None;
let mut error = None;
// Collect doc lines. This requires a rare mix of tokens (for comments) and AST
@@ -142,7 +154,7 @@ pub fn check_path(
));
}
if use_imports {
diagnostics.extend(check_imports(
let (import_diagnostics, module_imports) = check_imports(
&python_ast,
locator,
indexer,
@@ -152,7 +164,9 @@ pub fn check_path(
autofix,
path,
package,
));
);
imports = module_imports;
diagnostics.extend(import_diagnostics);
}
if use_doc_lines {
doc_lines.extend(doc_lines_from_ast(&python_ast));
@@ -183,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,
));
}
@@ -240,7 +248,7 @@ pub fn check_path(
}
}
LinterResult::new(diagnostics, error)
LinterResult::new((diagnostics, imports), error)
}
const MAX_ITERATIONS: usize = 100;
@@ -297,7 +305,7 @@ pub fn add_noqa_to_path(path: &Path, package: Option<&Path>, settings: &Settings
// Add any missing `# noqa` pragmas.
add_noqa(
path,
&diagnostics,
&diagnostics.0,
&contents,
indexer.commented_lines(),
&directives.noqa_line_for,
@@ -314,7 +322,7 @@ pub fn lint_only(
settings: &Settings,
noqa: flags::Noqa,
autofix: flags::Autofix,
) -> LinterResult<Vec<Message>> {
) -> LinterResult<(Vec<Message>, Option<ImportMap>)> {
// Tokenize once.
let tokens: Vec<LexResult> = ruff_rustpython::tokenize(contents);
@@ -346,25 +354,41 @@ pub fn lint_only(
autofix,
);
// Convert from diagnostics to messages.
let path_lossy = path.to_string_lossy();
result.map(|diagnostics| {
diagnostics
.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()
result.map(|(diagnostics, imports)| {
(
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>(
@@ -373,7 +397,7 @@ pub fn lint_fix<'a>(
package: Option<&Path>,
noqa: flags::Noqa,
settings: &Settings,
) -> Result<(LinterResult<Vec<Message>>, Cow<'a, str>, FixTable)> {
) -> Result<FixerResult<'a>> {
let mut transformed = Cow::Borrowed(contents);
// Track the number of fixed errors across iterations.
@@ -448,7 +472,7 @@ This indicates a bug in `{}`. If you could open an issue at:
}
// Apply autofix.
if let Some((fixed_contents, applied)) = fix_file(&result.data, &locator) {
if let Some((fixed_contents, applied)) = fix_file(&result.data.0, &locator) {
if iterations < MAX_ITERATIONS {
// Count the number of fixed errors.
for (rule, count) in applied {
@@ -486,31 +510,15 @@ This indicates a bug in `{}`. If you could open an issue at:
}
}
// Convert to messages.
let path_lossy = path.to_string_lossy();
return Ok((
result.map(|diagnostics| {
diagnostics
.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()
return Ok(FixerResult {
result: result.map(|(diagnostics, imports)| {
(
diagnostics_to_messages(diagnostics, path, settings, &locator, &directives),
imports,
)
}),
transformed,
fixed,
));
});
}
}

View File

@@ -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,
),
}
}
}

View 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);
}
}

View 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
}

View 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);
}
}

View 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
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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")
}
}

View 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);
}
}

View File

@@ -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`

View File

@@ -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`

View File

@@ -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
}
}
}
]

View File

@@ -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
|

View File

@@ -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
|

View File

@@ -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
}
]

View File

@@ -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